package openai import ( "bytes" "encoding/json" "errors" "fmt" "strings" edgeservice "iop/apps/edge/internal/service" "iop/packages/go/config" ) // providerRequestRequirements is the caller-neutral request shape used for // provider operation selection. Agent or SDK identity is intentionally absent. type providerRequestRequirements struct { Effort string HasTools bool HasTokenBudget bool Stream bool StructuredOutput bool } type providerOperationPlan struct { Operation config.ProtocolOperation Effort string EffortWire string } type openAIResponsesBridgeResponse struct { ID string `json:"id"` Model string `json:"model"` Status string `json:"status"` Output []struct { Type string `json:"type"` ID string `json:"id"` CallID string `json:"call_id"` Name string `json:"name"` Arguments string `json:"arguments"` Content []struct { Type string `json:"type"` Text string `json:"text"` } `json:"content"` Summary []struct { Type string `json:"type"` Text string `json:"text"` } `json:"summary"` } `json:"output"` Usage struct { InputTokens int `json:"input_tokens"` OutputTokens int `json:"output_tokens"` InputDetails struct { CachedTokens int `json:"cached_tokens"` } `json:"input_tokens_details"` } `json:"usage"` IncompleteDetails struct { Reason string `json:"reason"` } `json:"incomplete_details"` } // singleRequestChatBridgeResponse admits the portable Chat Completions response // fields needed by private stages. Provider-selected bookkeeping such as // service_tier, system_fingerprint, annotations, and logprobs is intentionally // left outside the canonical stage envelope. type singleRequestChatBridgeResponse struct { ID string `json:"id"` Object string `json:"object"` Created int64 `json:"created"` Model string `json:"model"` Choices []singleRequestChatBridgeChoice `json:"choices"` Usage json.RawMessage `json:"usage"` } type singleRequestChatBridgeChoice struct { Index int `json:"index"` FinishReason string `json:"finish_reason"` Message singleRequestChatBridgeMessage `json:"message"` } type singleRequestChatBridgeMessage struct { Role string `json:"role"` Content *string `json:"content"` ToolCalls json.RawMessage `json:"tool_calls"` ReasoningContent *string `json:"reasoning_content"` ExtraContent json.RawMessage `json:"extra_content"` Refusal json.RawMessage `json:"refusal"` } func (v *singleRequestChatBridgeResponse) UnmarshalJSON(data []byte) error { if err := validateSingleRequestObjectFields(data, "id", "object", "created", "model", "choices", "usage", "service_tier", "system_fingerprint", "timings"); err != nil { return err } type alias singleRequestChatBridgeResponse var decoded alias if err := json.Unmarshal(data, &decoded); err != nil { return err } *v = singleRequestChatBridgeResponse(decoded) return nil } func (v *singleRequestChatBridgeChoice) UnmarshalJSON(data []byte) error { if err := validateSingleRequestObjectFields(data, "index", "finish_reason", "message", "logprobs"); err != nil { return err } type alias singleRequestChatBridgeChoice var decoded alias if err := json.Unmarshal(data, &decoded); err != nil { return err } *v = singleRequestChatBridgeChoice(decoded) return nil } func (v *singleRequestChatBridgeMessage) UnmarshalJSON(data []byte) error { if err := validateSingleRequestObjectFields(data, "role", "content", "tool_calls", "reasoning_content", "extra_content", "refusal", "annotations"); err != nil { return err } type alias singleRequestChatBridgeMessage var decoded alias if err := json.Unmarshal(data, &decoded); err != nil { return err } *v = singleRequestChatBridgeMessage(decoded) return nil } func singleRequestProviderRequirements(options map[string]any, tools []any, responseFormat *singleRequestProviderResponseFormat) providerRequestRequirements { requirements := providerRequestRequirements{HasTools: len(tools) > 0, StructuredOutput: responseFormat != nil} if effort, ok := options["reasoning_effort"].(string); ok { requirements.Effort = strings.TrimSpace(effort) } return requirements } func singleRequestProviderCandidatePredicate(frozen *edgeservice.SingleRequestStageDispatchBinding, requirements providerRequestRequirements) edgeservice.ProviderPoolCandidatePredicate { return func(candidate edgeservice.ProviderPoolCandidate) bool { if frozen == nil || candidate.ProtocolProfile == nil || candidate.ExecutionPath != string(edgeservice.ProviderPoolPathTunnel) { return false } if frozen.CandidatePredicate != nil && !frozen.CandidatePredicate(candidate) { return false } _, err := selectProviderOperation(*candidate.ProtocolProfile, config.OperationChatCompletions, requirements) return err == nil } } func singleRequestProviderTunnelPreparer(requirements providerRequestRequirements, buildChat singleRequestProviderBodyBuilder) func(edgeservice.SubmitProviderTunnelRequest, edgeservice.ProviderPoolCandidate) (edgeservice.SubmitProviderTunnelRequest, error) { return func(tunnel edgeservice.SubmitProviderTunnelRequest, candidate edgeservice.ProviderPoolCandidate) (edgeservice.SubmitProviderTunnelRequest, error) { if candidate.ProtocolProfile == nil || buildChat == nil { return tunnel, errProviderStageMissingBinding } plan, err := selectProviderOperation(*candidate.ProtocolProfile, config.OperationChatCompletions, requirements) if err != nil { return tunnel, err } tunnel.Operation = string(plan.Operation) switch plan.Operation { case config.OperationChatCompletions: tunnel.Path = "/v1/chat/completions" case config.OperationResponses: tunnel.Path = "/v1/responses" default: return tunnel, errProviderStageMissingBinding } tunnel.BuildBody = func(target string) ([]byte, error) { body, err := buildChat(target) if err != nil { return nil, err } return normalizeSingleRequestProviderRequest(body, plan) } return tunnel, nil } } func normalizeSingleRequestProviderRequest(chatBody []byte, plan providerOperationPlan) ([]byte, error) { var body map[string]any if err := json.Unmarshal(chatBody, &body); err != nil { return nil, errProviderStageMalformed } if plan.Operation == config.OperationChatCompletions { if plan.Effort != "" { body["reasoning_effort"] = plan.Effort } return json.Marshal(body) } if plan.Operation != config.OperationResponses { return nil, errProviderStageMalformed } return singleRequestChatToResponses(body, plan) } func singleRequestChatToResponses(chat map[string]any, plan providerOperationPlan) ([]byte, error) { responses := map[string]any{"model": chat["model"], "stream": false} for _, key := range []string{"temperature", "top_p", "service_tier"} { if value, exists := chat[key]; exists { responses[key] = value } } for _, key := range []string{"max_completion_tokens", "max_tokens"} { if value, exists := chat[key]; exists { responses["max_output_tokens"] = value break } } messages, ok := chat["messages"].([]any) if !ok { return nil, errProviderStageMalformed } input := make([]any, 0, len(messages)) instructions := make([]string, 0, 1) for _, raw := range messages { message, ok := raw.(map[string]any) if !ok { return nil, errProviderStageMalformed } role, _ := message["role"].(string) content, _ := message["content"].(string) switch role { case "system": if content != "" { instructions = append(instructions, content) } case "user": input = append(input, map[string]any{"type": "message", "role": "user", "content": []any{map[string]any{"type": "input_text", "text": content}}}) case "assistant": if content != "" { input = append(input, map[string]any{"type": "message", "role": "assistant", "content": []any{map[string]any{"type": "output_text", "text": content}}}) } for _, rawCall := range anySlice(message["tool_calls"]) { call, ok := rawCall.(map[string]any) function, functionOK := call["function"].(map[string]any) if !ok || !functionOK { return nil, errProviderStageMalformed } input = append(input, map[string]any{"type": "function_call", "call_id": call["id"], "name": function["name"], "arguments": function["arguments"]}) } case "tool": input = append(input, map[string]any{"type": "function_call_output", "call_id": message["tool_call_id"], "output": content}) default: return nil, errProviderStageMalformed } } if len(instructions) > 0 { responses["instructions"] = strings.Join(instructions, "\n\n") } responses["input"] = input if plan.Effort != "" { responses["reasoning"] = map[string]any{"effort": plan.Effort} } if rawTools := anySlice(chat["tools"]); len(rawTools) > 0 { tools := make([]any, 0, len(rawTools)) for _, rawTool := range rawTools { tool, ok := rawTool.(map[string]any) function, functionOK := tool["function"].(map[string]any) if !ok || !functionOK { return nil, errProviderStageMalformed } converted := map[string]any{"type": "function", "name": function["name"], "parameters": function["parameters"]} for _, key := range []string{"description", "strict"} { if value, exists := function[key]; exists { converted[key] = value } } tools = append(tools, converted) } responses["tools"] = tools } if choice, ok := chat["tool_choice"]; ok { responses["tool_choice"] = singleRequestResponsesToolChoice(choice) } if parallel, ok := chat["parallel_tool_calls"]; ok { responses["parallel_tool_calls"] = parallel } if format, ok := chat["response_format"].(map[string]any); ok { if schema, schemaOK := format["json_schema"].(map[string]any); schemaOK { converted := map[string]any{"type": "json_schema"} for _, key := range []string{"name", "strict", "schema"} { converted[key] = schema[key] } responses["text"] = map[string]any{"format": converted} } } return json.Marshal(responses) } func anySlice(value any) []any { if value == nil { return nil } items, _ := value.([]any) return items } func singleRequestResponsesToolChoice(choice any) any { if object, ok := choice.(map[string]any); ok { if function, functionOK := object["function"].(map[string]any); functionOK { return map[string]any{"type": "function", "name": function["name"]} } } return choice } func normalizeSingleRequestProviderResponse(body []byte, dispatch edgeservice.RunDispatch) ([]byte, error) { switch dispatch.ProfileOperation { case string(config.OperationResponses): return normalizeSingleRequestResponsesResponse(body) case string(config.OperationChatCompletions), "": return normalizeSingleRequestChatResponse(body) default: return nil, errors.Join(errProviderStageGeneric, errProviderStageMalformed) } } func normalizeSingleRequestChatResponse(body []byte) ([]byte, error) { if err := validateSingleRequestJSON(body); err != nil { return nil, errors.Join(errProviderStageGeneric, errProviderStageMalformed) } var response singleRequestChatBridgeResponse if err := json.Unmarshal(body, &response); err != nil { return nil, errors.Join(errProviderStageGeneric, errProviderStageMalformed) } choices := make([]any, 0, len(response.Choices)) for _, choice := range response.Choices { if refusal := bytes.TrimSpace(choice.Message.Refusal); len(refusal) > 0 && !bytes.Equal(refusal, []byte("null")) { return nil, errors.Join(errProviderStageGeneric, errProviderStageMalformed) } message := map[string]any{ "role": choice.Message.Role, "content": choice.Message.Content, } if len(choice.Message.ToolCalls) > 0 { message["tool_calls"] = choice.Message.ToolCalls } if choice.Message.ReasoningContent != nil { message["reasoning_content"] = choice.Message.ReasoningContent } if len(choice.Message.ExtraContent) > 0 { message["extra_content"] = choice.Message.ExtraContent } choices = append(choices, map[string]any{ "index": choice.Index, "finish_reason": choice.FinishReason, "message": message, }) } canonical := map[string]any{ "id": response.ID, "object": response.Object, "created": response.Created, "model": response.Model, "choices": choices, } if len(response.Usage) > 0 && !bytes.Equal(bytes.TrimSpace(response.Usage), []byte("null")) { canonical["usage"] = response.Usage } encoded, err := json.Marshal(canonical) if err != nil { return nil, errors.Join(errProviderStageGeneric, errProviderStageMalformed) } return encoded, nil } func normalizeSingleRequestResponsesResponse(body []byte) ([]byte, error) { var response openAIResponsesBridgeResponse if err := json.Unmarshal(body, &response); err != nil || strings.TrimSpace(response.ID) == "" { return nil, errors.Join(errProviderStageGeneric, errProviderStageMalformed) } content := "" toolCalls := make([]any, 0) for _, item := range response.Output { switch item.Type { case "message": for _, part := range item.Content { if part.Type == "output_text" { content += part.Text } } case "function_call": if item.CallID == "" || item.Name == "" || !json.Valid([]byte(item.Arguments)) { return nil, errors.Join(errProviderStageGeneric, errProviderStageMalformed) } toolCalls = append(toolCalls, map[string]any{"id": item.CallID, "type": "function", "function": map[string]any{"name": item.Name, "arguments": item.Arguments}}) } } finishReason := "stop" if len(toolCalls) > 0 { finishReason = "tool_calls" } else if response.Status == "incomplete" && response.IncompleteDetails.Reason == "max_output_tokens" { finishReason = "length" } var contentValue any = content if len(toolCalls) > 0 && content == "" { contentValue = nil } chat := map[string]any{ "id": response.ID, "object": "chat.completion", "created": int64(0), "model": response.Model, "choices": []any{map[string]any{"index": 0, "finish_reason": finishReason, "message": map[string]any{"role": "assistant", "content": contentValue, "tool_calls": toolCalls}}}, } return json.Marshal(chat) } func anthropicProviderRequirements(req anthropicMessageRequest) providerRequestRequirements { requirements := providerRequestRequirements{ HasTools: len(req.Tools) > 0, Stream: req.Stream, HasTokenBudget: req.Thinking != nil && req.Thinking.Type == "enabled", StructuredOutput: req.OutputConfig != nil && req.OutputConfig.Format != nil, } if req.OutputConfig != nil { requirements.Effort = strings.TrimSpace(req.OutputConfig.Effort) } return requirements } func decodeAnthropicProviderRequirements(body []byte) (providerRequestRequirements, error) { var request struct { Stream bool `json:"stream"` Tools []json.RawMessage `json:"tools"` Thinking *anthropicThinkingConfig `json:"thinking"` OutputConfig *anthropicOutputConfig `json:"output_config"` } if err := json.Unmarshal(body, &request); err != nil { return providerRequestRequirements{}, fmt.Errorf("decode Messages request") } return anthropicProviderRequirements(anthropicMessageRequest{ Stream: request.Stream, Tools: make([]anthropicTool, len(request.Tools)), Thinking: request.Thinking, OutputConfig: request.OutputConfig, }), nil } func decodeResponsesProviderRequirements(body []byte) (providerRequestRequirements, error) { var request struct { Stream bool `json:"stream"` Tools []json.RawMessage `json:"tools"` Reasoning *struct { Effort string `json:"effort"` } `json:"reasoning"` MaxOutputTokens *int `json:"max_output_tokens"` Text *responsesTextFormat `json:"text"` } if err := json.Unmarshal(body, &request); err != nil { return providerRequestRequirements{}, fmt.Errorf("decode Responses request") } requirements := providerRequestRequirements{HasTools: len(request.Tools) > 0, Stream: request.Stream} if request.MaxOutputTokens != nil { requirements.HasTokenBudget = true } if request.Text != nil && request.Text.Format != nil { requirements.StructuredOutput = true } if request.Reasoning != nil { requirements.Effort = strings.TrimSpace(request.Reasoning.Effort) } return requirements, nil } // responsesTextFormat mirrors the Responses request text.format selector that // carries structured-output constraints. It is intentionally minimal: only the // presence of a format block is needed for admission decisions. type responsesTextFormat struct { Format json.RawMessage `json:"format,omitempty"` } func responsesCandidatePredicate(requirements providerRequestRequirements) edgeservice.ProviderPoolCandidatePredicate { return func(candidate edgeservice.ProviderPoolCandidate) bool { // A nil profile is the legacy tunnel contract: operation resolution is // deferred to the existing dispatch path, which must remain compatible. if candidate.ProtocolProfile == nil { return true } if candidate.ExecutionPath != "" && candidate.ExecutionPath != string(edgeservice.ProviderPoolPathTunnel) { return requirements.Effort == "" && !requirements.HasTools && !requirements.HasTokenBudget } _, err := selectProviderOperation(*candidate.ProtocolProfile, config.OperationResponses, requirements) return err == nil } } func rewriteResponsesProviderControls(body []byte, target string, plan providerOperationPlan) ([]byte, error) { patches := make([]topLevelJSONPatch, 0, 2) if strings.TrimSpace(target) != "" { modelJSON, err := json.Marshal(target) if err != nil { return nil, err } patches = append(patches, topLevelJSONPatch{name: "model", value: modelJSON}) } if plan.Effort != "" { var root map[string]json.RawMessage if err := json.Unmarshal(body, &root); err != nil { return nil, fmt.Errorf("decode Responses request") } var reasoning map[string]any if raw := root["reasoning"]; len(raw) > 0 && string(raw) != "null" { if err := json.Unmarshal(raw, &reasoning); err != nil { return nil, fmt.Errorf("reasoning must be an object") } } if reasoning == nil { reasoning = make(map[string]any) } reasoning["effort"] = plan.Effort reasoningJSON, err := json.Marshal(reasoning) if err != nil { return nil, err } patches = append(patches, topLevelJSONPatch{name: "reasoning", value: reasoningJSON}) } if len(patches) == 0 { return body, nil } patchPlan, err := planTopLevelJSONPatches(body, patches) if err != nil { return nil, err } return patchPlan.apply(), nil } // prepareResponsesMessagesBridge and prepareResponsesChatBridge deliberately // accept only the portable subset below. A Responses tunnel is otherwise // lossless, but a bridge must never silently discard a caller control. func prepareResponsesMessagesBridge(body []byte, target string, profile config.ConcreteProtocolProfile, plan providerOperationPlan) ([]byte, error) { if plan.Operation != config.OperationMessages || (plan.Effort != "" && plan.EffortWire != config.ProtocolEffortWireAnthropicMessage) { return nil, fmt.Errorf("selected Messages profile cannot preserve the requested Responses controls") } root, err := decodeResponsesBridgeRoot(body) if err != nil { return nil, err } messages, system, err := responsesInputToBridgeMessages(root["input"], true) if err != nil { return nil, err } if instructions, _ := root["instructions"].(string); strings.TrimSpace(instructions) != "" { system = append([]string{instructions}, system...) } maxTokens, ok := root["max_output_tokens"] if !ok { return nil, fmt.Errorf("max_output_tokens is required for the Messages bridge") } maxTokensNumber, ok := maxTokens.(float64) if !ok || maxTokensNumber <= 0 || maxTokensNumber != float64(int(maxTokensNumber)) { return nil, fmt.Errorf("max_output_tokens must be a positive integer for the Messages bridge") } request := map[string]any{"model": target, "messages": messages, "max_tokens": maxTokens} if len(system) > 0 { request["system"] = strings.Join(system, "\n\n") } copyResponsesBridgeOption(root, request, "stream", "temperature", "top_p") if plan.Effort != "" { request["output_config"] = map[string]any{"effort": plan.Effort} } if err := copyResponsesToolsToMessages(root, request); err != nil { return nil, err } if format, ok := responsesStructuredFormat(root); ok { output, _ := request["output_config"].(map[string]any) if output == nil { output = map[string]any{} } output["format"] = format request["output_config"] = output } return json.Marshal(request) } func prepareResponsesChatBridge(body []byte, target string, profile config.ConcreteProtocolProfile, plan providerOperationPlan) ([]byte, error) { if plan.Operation != config.OperationChatCompletions || (plan.Effort != "" && plan.EffortWire != config.ProtocolEffortWireOpenAIChat && plan.EffortWire != config.ProtocolEffortWireGeminiChat) { return nil, fmt.Errorf("selected Chat profile cannot preserve the requested Responses controls") } root, err := decodeResponsesBridgeRoot(body) if err != nil { return nil, err } messages, system, err := responsesInputToBridgeMessages(root["input"], false) if err != nil { return nil, err } if instructions, _ := root["instructions"].(string); strings.TrimSpace(instructions) != "" { system = append([]string{instructions}, system...) } if len(system) > 0 { messages = append([]any{map[string]any{"role": "system", "content": strings.Join(system, "\n\n")}}, messages...) } request := map[string]any{"model": target, "messages": messages} copyResponsesBridgeOption(root, request, "stream", "temperature", "top_p", "parallel_tool_calls") if max, ok := root["max_output_tokens"]; ok { field := "max_tokens" if mapping, ok := profile.EffortMapping(config.OperationChatCompletions); ok && mapping.Wire == config.ProtocolEffortWireOpenAIChat { field = "max_completion_tokens" } request[field] = max } if plan.Effort != "" { request["reasoning_effort"] = plan.Effort } if err := copyResponsesToolsToChat(root, request); err != nil { return nil, err } if format, ok := responsesStructuredFormat(root); ok { request["response_format"] = map[string]any{"type": "json_schema", "json_schema": format} } return json.Marshal(request) } func decodeResponsesBridgeRoot(body []byte) (map[string]any, error) { var root map[string]any if err := json.Unmarshal(body, &root); err != nil { return nil, fmt.Errorf("decode Responses bridge request") } allowed := map[string]bool{"model": true, "input": true, "instructions": true, "stream": true, "max_output_tokens": true, "temperature": true, "top_p": true, "tools": true, "tool_choice": true, "parallel_tool_calls": true, "reasoning": true, "text": true} for key := range root { if !allowed[key] { return nil, fmt.Errorf("Responses field %q is not representable by the selected provider wire", key) } } if _, ok := root["input"]; !ok { return nil, fmt.Errorf("input is required") } return root, nil } func validateResponsesBridgeControls(body []byte) error { _, err := decodeResponsesBridgeRoot(body) return err } func copyResponsesBridgeOption(source, target map[string]any, keys ...string) { for _, key := range keys { if value, ok := source[key]; ok { target[key] = value } } } func responsesInputToBridgeMessages(input any, messagesWire bool) ([]any, []string, error) { if text, ok := input.(string); ok { return []any{map[string]any{"role": "user", "content": text}}, nil, nil } items, ok := input.([]any) if !ok { return nil, nil, fmt.Errorf("input must be a string or an item array") } var messages []any var system []string for _, raw := range items { item, ok := raw.(map[string]any) if !ok { return nil, nil, fmt.Errorf("input item is invalid") } typ, _ := item["type"].(string) switch typ { case "message": role, _ := item["role"].(string) text, err := responsesMessageText(item["content"]) if err != nil { return nil, nil, err } if role == "system" { system = append(system, text) continue } if role != "user" && role != "assistant" { return nil, nil, fmt.Errorf("message role %q is not representable", role) } messages = append(messages, map[string]any{"role": role, "content": text}) case "function_call": id, _ := item["call_id"].(string) name, _ := item["name"].(string) args, _ := item["arguments"].(string) if id == "" || name == "" || !json.Valid([]byte(args)) { return nil, nil, fmt.Errorf("function_call is invalid") } if messagesWire { messages = append(messages, map[string]any{"role": "assistant", "content": []any{map[string]any{"type": "tool_use", "id": id, "name": name, "input": json.RawMessage(args)}}}) } else { messages = append(messages, map[string]any{"role": "assistant", "content": nil, "tool_calls": []any{map[string]any{"id": id, "type": "function", "function": map[string]any{"name": name, "arguments": args}}}}) } case "function_call_output": id, _ := item["call_id"].(string) output, ok := item["output"].(string) if id == "" || !ok { return nil, nil, fmt.Errorf("function_call_output is invalid") } if messagesWire { messages = append(messages, map[string]any{"role": "user", "content": []any{map[string]any{"type": "tool_result", "tool_use_id": id, "content": output}}}) } else { original, signature, encoded, err := decodeGeminiThoughtSignatureToolID(id) if err != nil { return nil, nil, err } tool := map[string]any{"role": "tool", "tool_call_id": original, "content": output} if encoded { tool["extra_content"] = openAIChatThoughtSignature(signature) } messages = append(messages, tool) } default: return nil, nil, fmt.Errorf("input item type %q is not representable", typ) } } return messages, system, nil } func responsesMessageText(raw any) (string, error) { if text, ok := raw.(string); ok { return text, nil } parts, ok := raw.([]any) if !ok { return "", fmt.Errorf("message content is invalid") } var out []string for _, rawPart := range parts { part, ok := rawPart.(map[string]any) if !ok { return "", fmt.Errorf("message content part is invalid") } typ, _ := part["type"].(string) text, _ := part["text"].(string) if (typ != "input_text" && typ != "output_text" && typ != "text") || text == "" { return "", fmt.Errorf("message content part %q is not representable", typ) } out = append(out, text) } return strings.Join(out, "\n"), nil } func copyResponsesToolsToChat(root, request map[string]any) error { tools, exists := root["tools"] if !exists { return copyResponsesToolChoice(root, request, false) } items, ok := tools.([]any) if !ok { return fmt.Errorf("tools must be an array") } out := make([]any, 0, len(items)) for _, raw := range items { tool, ok := raw.(map[string]any) if !ok || tool["type"] != "function" { return fmt.Errorf("tool is not representable by the Chat bridge") } name, _ := tool["name"].(string) params, ok := tool["parameters"] if name == "" || !ok { return fmt.Errorf("tool is invalid") } fn := map[string]any{"name": name, "parameters": params} if description, ok := tool["description"]; ok { fn["description"] = description } out = append(out, map[string]any{"type": "function", "function": fn}) } request["tools"] = out return copyResponsesToolChoice(root, request, false) } func copyResponsesToolsToMessages(root, request map[string]any) error { tools, exists := root["tools"] if !exists { return copyResponsesToolChoice(root, request, true) } items, ok := tools.([]any) if !ok { return fmt.Errorf("tools must be an array") } out := make([]any, 0, len(items)) for _, raw := range items { tool, ok := raw.(map[string]any) if !ok || tool["type"] != "function" { return fmt.Errorf("tool is not representable by the Messages bridge") } name, _ := tool["name"].(string) schema, ok := tool["parameters"] if name == "" || !ok { return fmt.Errorf("tool is invalid") } converted := map[string]any{"name": name, "input_schema": schema} if description, ok := tool["description"]; ok { converted["description"] = description } out = append(out, converted) } request["tools"] = out return copyResponsesToolChoice(root, request, true) } func copyResponsesToolChoice(root, request map[string]any, messagesWire bool) error { choice, exists := root["tool_choice"] if !exists { return nil } if text, ok := choice.(string); ok { if text != "auto" && text != "required" && text != "none" { return fmt.Errorf("tool_choice %q is not representable", text) } if messagesWire { if text == "none" { return fmt.Errorf("tool_choice none is not representable by Messages") } request["tool_choice"] = map[string]any{"type": text} } else { request["tool_choice"] = text } return nil } selected, ok := choice.(map[string]any) if !ok || selected["type"] != "function" { return fmt.Errorf("tool_choice is invalid") } name, _ := selected["name"].(string) if name == "" { return fmt.Errorf("tool_choice function name is required") } if messagesWire { request["tool_choice"] = map[string]any{"type": "tool", "name": name} } else { request["tool_choice"] = map[string]any{"type": "function", "function": map[string]any{"name": name}} } return nil } func responsesStructuredFormat(root map[string]any) (map[string]any, bool) { text, ok := root["text"].(map[string]any) if !ok { return nil, false } format, ok := text["format"].(map[string]any) return format, ok } // selectProviderOperation chooses an operation solely from normalized request // requirements and the selected provider profile. The order prefers the // closest wire surface, but only an operation that preserves every declared // requirement is eligible. func selectProviderOperation(profile config.ConcreteProtocolProfile, ingress config.ProtocolOperation, requirements providerRequestRequirements) (providerOperationPlan, error) { operations := []config.ProtocolOperation{ingress} switch ingress { case config.OperationResponses: switch profile.Driver { case config.ProtocolDriverOpenAIResponses: operations = []config.ProtocolOperation{config.OperationResponses} case config.ProtocolDriverAnthropicMessages: operations = []config.ProtocolOperation{config.OperationMessages} case config.ProtocolDriverOpenAIChat: // A Chat driver may still declare its native Responses operation // (the OpenAI profile does). Prefer that lossless wire; profiles such // as Gemini that do not declare it use the Chat bridge. if _, ok := profile.Operations[string(config.OperationResponses)]; ok { operations = []config.ProtocolOperation{config.OperationResponses} } else { operations = []config.ProtocolOperation{config.OperationChatCompletions} } default: operations = nil } case config.OperationMessages: switch profile.Driver { case config.ProtocolDriverAnthropicMessages: operations = []config.ProtocolOperation{config.OperationMessages} case config.ProtocolDriverOpenAIChat: operations = []config.ProtocolOperation{config.OperationChatCompletions, config.OperationResponses} case config.ProtocolDriverOpenAIResponses: operations = []config.ProtocolOperation{config.OperationResponses} default: operations = nil } case config.OperationChatCompletions: switch profile.Driver { case config.ProtocolDriverOpenAIChat: operations = []config.ProtocolOperation{config.OperationChatCompletions, config.OperationResponses} case config.ProtocolDriverOpenAIResponses: operations = []config.ProtocolOperation{config.OperationResponses} default: operations = nil } } for _, operation := range operations { if _, ok := profile.Operations[string(operation)]; !ok { continue } if required := operationRequiredCapability(operation); required != "" && !profile.HasCapability(required) { continue } if requirements.Stream && !profile.HasCapability("streaming") { continue } if requirements.HasTools && !profile.HasCapability("tool_calling") { continue } plan := providerOperationPlan{Operation: operation} mapping, hasMapping := profile.EffortMapping(operation) supportsTokenBudget := hasMapping && mapping.TokenBudget if operation == config.OperationChatCompletions && profileSupportsAnthropicThinking(profile) { supportsTokenBudget = true } if requirements.HasTokenBudget && operation != ingress && !supportsTokenBudget { continue } // Native ingress wire already carries this control verbatim. Capability // mapping is required only when a bridge must translate it. // Token output limits have direct fields on bridge wires; only an // explicit reasoning effort needs the profile's token-budget mapping. if requirements.Effort != "" { mapped, ok := profile.MapReasoningEffort(operation, requirements.Effort, requirements.HasTools) if !ok { continue } plan.Effort = mapped plan.EffortWire = mapping.Wire } return plan, nil } return providerOperationPlan{}, fmt.Errorf("protocol profile %q cannot preserve the requested operation, tools, and reasoning controls", profile.ID) } func operationRequiredCapability(operation config.ProtocolOperation) string { switch operation { case config.OperationMessages: return "messages" case config.OperationChatCompletions: return "chat" case config.OperationResponses: return "responses" case config.OperationCountTokens: return "count_tokens" default: return "" } } func prepareAnthropicResponsesBridge(body []byte, target string, profile config.ConcreteProtocolProfile, plan providerOperationPlan) ([]byte, anthropicMessageRequest, error) { req, err := decodeAnthropicMessageRequest(body, true) if err != nil { return nil, req, err } if plan.Operation != config.OperationResponses || (plan.Effort != "" && plan.EffortWire != config.ProtocolEffortWireOpenAIResponses) { return nil, req, fmt.Errorf("selected operation has incompatible Responses effort normalization") } if req.TopK != nil { return nil, req, fmt.Errorf("top_k is not supported by the Responses bridge") } if req.Thinking != nil && req.Thinking.Type == "enabled" { return nil, req, fmt.Errorf("selected Responses profile does not support an explicit thinking token budget") } input := make([]map[string]any, 0, len(req.Messages)) for index, message := range req.Messages { blocks, err := decodeAnthropicContent(message.Content) if err != nil { return nil, req, fmt.Errorf("messages[%d].content: %w", index, err) } converted, err := anthropicMessageToResponses(message.Role, blocks) if err != nil { return nil, req, fmt.Errorf("messages[%d]: %w", index, err) } input = append(input, converted...) } responses := map[string]any{ "model": target, "input": input, "max_output_tokens": *req.MaxTokens, "stream": req.Stream, } if system, err := decodeAnthropicSystem(req.System); err != nil { return nil, req, err } else if len(system) > 0 { parts := make([]string, 0, len(system)) for _, block := range system { parts = append(parts, block.Text) } responses["instructions"] = strings.Join(parts, "\n") } if plan.Effort != "" { responses["reasoning"] = map[string]any{"effort": plan.Effort} } if req.Temperature != nil { responses["temperature"] = *req.Temperature } if req.TopP != nil { responses["top_p"] = *req.TopP } if len(req.StopSequences) > 0 { return nil, req, fmt.Errorf("stop_sequences is not supported by the Responses bridge") } if len(req.Tools) > 0 { tools := make([]map[string]any, 0, len(req.Tools)) for _, tool := range req.Tools { var schema map[string]any if err := json.Unmarshal(tool.InputSchema, &schema); err != nil { return nil, req, fmt.Errorf("tool %q input_schema is invalid", tool.Name) } converted := map[string]any{"type": "function", "name": tool.Name, "parameters": schema} if tool.Description != "" { converted["description"] = tool.Description } tools = append(tools, converted) } responses["tools"] = tools } if req.ToolChoice != nil { choice, parallel := anthropicToolChoiceToResponses(*req.ToolChoice) responses["tool_choice"] = choice if parallel != nil { responses["parallel_tool_calls"] = *parallel } } if req.OutputConfig != nil && req.OutputConfig.Format != nil { var schema map[string]any if err := json.Unmarshal(req.OutputConfig.Format.Schema, &schema); err != nil { return nil, req, fmt.Errorf("decode output_config.format.schema: %w", err) } responses["text"] = map[string]any{"format": map[string]any{ "type": "json_schema", "name": "response", "strict": true, "schema": schema, }} } encoded, err := json.Marshal(responses) if err != nil { return nil, req, fmt.Errorf("encode Responses bridge request: %w", err) } return encoded, req, nil } func anthropicMessageToResponses(role string, blocks []anthropicContentBlock) ([]map[string]any, error) { if role == "assistant" { out := make([]map[string]any, 0, len(blocks)) for _, block := range blocks { switch block.Type { case "text": out = append(out, map[string]any{"type": "message", "role": "assistant", "content": []map[string]any{{"type": "output_text", "text": block.Text}}}) case "thinking": if block.Signature != "" { return nil, fmt.Errorf("signed thinking blocks cannot be represented by the Responses bridge") } case "tool_use": callID, _, _ := decodeAnthropicBridgeToolID(block.ID) out = append(out, map[string]any{"type": "function_call", "call_id": callID, "name": block.Name, "arguments": string(block.Input)}) default: return nil, fmt.Errorf("content block %q is invalid for an assistant message", block.Type) } } return out, nil } if role == "system" { content := make([]map[string]any, 0, len(blocks)) for _, block := range blocks { if block.Type != "text" { return nil, fmt.Errorf("content block %q is invalid for a system message", block.Type) } content = append(content, map[string]any{"type": "input_text", "text": block.Text}) } if len(content) == 0 { return nil, fmt.Errorf("system message content is empty") } return []map[string]any{{"type": "message", "role": "system", "content": content}}, nil } out := make([]map[string]any, 0, len(blocks)) content := make([]map[string]any, 0, len(blocks)) flushContent := func() { if len(content) > 0 { out = append(out, map[string]any{"type": "message", "role": "user", "content": content}) content = nil } } for _, block := range blocks { switch block.Type { case "text": content = append(content, map[string]any{"type": "input_text", "text": block.Text}) case "image": imageURL := block.Source.URL if block.Source.Type == "base64" { imageURL = "data:" + block.Source.MediaType + ";base64," + block.Source.Data } content = append(content, map[string]any{"type": "input_image", "image_url": imageURL}) case "tool_result": flushContent() result, err := anthropicToolResultText(block.Content) if err != nil { return nil, err } if block.IsError { result = "Error: " + result } callID, _, _ := decodeAnthropicBridgeToolID(block.ToolUseID) out = append(out, map[string]any{"type": "function_call_output", "call_id": callID, "output": result}) default: return nil, fmt.Errorf("content block %q is invalid for a user message", block.Type) } } flushContent() if len(out) == 0 { return nil, fmt.Errorf("user message content is empty") } return out, nil } func anthropicToolChoiceToResponses(choice anthropicToolChoice) (any, *bool) { parallel := !choice.DisableParallelToolUse switch choice.Type { case "auto": return "auto", ¶llel case "any": return "required", ¶llel case "none": return "none", ¶llel default: return map[string]any{"type": "function", "name": choice.Name}, ¶llel } } func convertResponsesResponseToAnthropic(body []byte, requestModel string) (anthropicMessageResponse, error) { var response openAIResponsesBridgeResponse if err := json.Unmarshal(body, &response); err != nil { return anthropicMessageResponse{}, fmt.Errorf("decode Responses response: %w", err) } if strings.TrimSpace(response.ID) == "" { return anthropicMessageResponse{}, fmt.Errorf("Responses response has no id") } content := make([]map[string]any, 0, len(response.Output)) hasTools := false for _, item := range response.Output { switch item.Type { case "message": for _, part := range item.Content { if part.Type == "output_text" && part.Text != "" { content = append(content, map[string]any{"type": "text", "text": part.Text}) } } case "reasoning": for _, part := range item.Summary { if part.Text != "" { content = append(content, map[string]any{"type": "thinking", "thinking": part.Text, "signature": ""}) } } case "function_call": if item.CallID == "" || item.Name == "" || !json.Valid([]byte(item.Arguments)) { return anthropicMessageResponse{}, fmt.Errorf("Responses function call has invalid call_id, name, or arguments") } var input any if err := json.Unmarshal([]byte(item.Arguments), &input); err != nil { return anthropicMessageResponse{}, fmt.Errorf("decode Responses function arguments: %w", err) } content = append(content, map[string]any{ "type": "tool_use", "id": encodeAnthropicBridgeToolID(item.CallID, openAIChatToolExtraContent{}), "name": item.Name, "input": input, }) hasTools = true } } stopReason := "end_turn" if hasTools { stopReason = "tool_use" } else if response.Status == "incomplete" && response.IncompleteDetails.Reason == "max_output_tokens" { stopReason = "max_tokens" } model := requestModel if model == "" { model = response.Model } return anthropicMessageResponse{ ID: response.ID, Type: "message", Role: "assistant", Model: model, Content: content, StopReason: &stopReason, Usage: anthropicUsage{ InputTokens: response.Usage.InputTokens, OutputTokens: response.Usage.OutputTokens, CacheReadInputTokens: response.Usage.InputDetails.CachedTokens, }, }, nil }