package apicompat import ( "bytes" "encoding/json" "fmt" "strings" ) // ResponsesClientToolMapping records the reversible lowering applied before a // native Responses request is sent to an upstream that only understands // function tools. type ResponsesClientToolMapping struct { CustomTools map[string]bool ToolSearch bool NamespaceTools map[string]ResponsesNamespaceName } // AdaptResponsesClientTools lowers Codex client-only tools in req to // ordinary function tools. It mutates req and returns the mapping required to // restore the upstream response. func AdaptResponsesClientTools(req map[string]any) (ResponsesClientToolMapping, bool, error) { if req == nil { return ResponsesClientToolMapping{}, false, nil } tools, ok := req["tools"].([]any) if !ok || len(tools) == 0 { return ResponsesClientToolMapping{}, false, nil } discovered, err := promoteResponsesToolSearchDiscoveries(req) if err != nil { return ResponsesClientToolMapping{}, false, err } if discovered { tools, _ = req["tools"].([]any) } adapter := ResponsesClientToolMapping{CustomTools: make(map[string]bool)} functionNames := make(map[string]bool) customNames := make(map[string]bool) for _, raw := range tools { tool, ok := raw.(map[string]any) if !ok { continue } name := strings.TrimSpace(stringValue(tool["name"])) switch strings.TrimSpace(stringValue(tool["type"])) { case "function": if name != "" { functionNames[name] = true } case "custom": if name != "" { customNames[name] = true } case "tool_search": adapter.ToolSearch = true } } for name := range customNames { if functionNames[name] { return ResponsesClientToolMapping{}, false, fmt.Errorf("custom tool %q conflicts with a function tool of the same name; this upstream cannot disambiguate them, rename one of the tools", name) } } if adapter.ToolSearch && (functionNames[toolSearchProxyName] || customNames[toolSearchProxyName]) { return ResponsesClientToolMapping{}, false, fmt.Errorf("built-in tool_search conflicts with a declared tool named %q; this upstream cannot disambiguate them, rename the tool", toolSearchProxyName) } // Namespace flattening also rewrites namespace-qualified history and choice. names, flattened, err := FlattenResponsesNamespaces(req) if err != nil { return ResponsesClientToolMapping{}, false, err } adapter.NamespaceTools = names if adapter.ToolSearch { if _, exists := names[toolSearchProxyName]; exists { return ResponsesClientToolMapping{}, false, fmt.Errorf("built-in tool_search conflicts with namespace tool flattened as %q; this upstream cannot disambiguate them, rename the tool", toolSearchProxyName) } } tools, _ = req["tools"].([]any) lowered := make([]any, 0, len(tools)) changed := discovered || flattened seenSearch := false for _, raw := range tools { tool, ok := raw.(map[string]any) if !ok { lowered = append(lowered, raw) continue } typ := strings.TrimSpace(stringValue(tool["type"])) name := strings.TrimSpace(stringValue(tool["name"])) switch typ { case "custom": if name == "" { lowered = append(lowered, raw) continue } copy := copyClientTool(tool) copy["type"] = "function" copy["parameters"] = json.RawMessage(customToolInputSchema) delete(copy, "format") adapter.CustomTools[name] = true lowered = append(lowered, copy) changed = true case "tool_search": if seenSearch { changed = true continue } seenSearch = true lowered = append(lowered, map[string]any{ "type": "function", "name": toolSearchProxyName, "description": "Search and load Codex tools, plugins, connectors, and MCP namespaces for the current task.", "parameters": json.RawMessage(toolSearchProxySchema), }) changed = true default: lowered = append(lowered, raw) } } if changed { req["tools"] = lowered } historyChanged, err := rewriteClientToolHistory(req["input"], &adapter) if err != nil { return ResponsesClientToolMapping{}, false, err } if historyChanged { changed = true } if rewriteClientToolChoice(req, &adapter) { changed = true } if len(adapter.CustomTools) == 0 { adapter.CustomTools = nil } if len(adapter.NamespaceTools) == 0 { adapter.NamespaceTools = nil } return adapter, changed, nil } // AdaptResponsesClientToolsWithInheritedMapping lowers client-tool history on // a follow-up request that omits the session-level tools declaration. An // explicitly present tools field, including an empty or malformed value, // always replaces the inherited mapping and is handled by the ordinary // declaration-driven adapter. func AdaptResponsesClientToolsWithInheritedMapping( req map[string]any, inherited ResponsesClientToolMapping, inheritedLoweredTools ...[]any, ) (ResponsesClientToolMapping, bool, error) { if req == nil { return ResponsesClientToolMapping{}, false, nil } if _, toolsPresent := req["tools"]; toolsPresent { return AdaptResponsesClientTools(req) } if len(inherited.CustomTools) == 0 && !inherited.ToolSearch && len(inherited.NamespaceTools) == 0 { return ResponsesClientToolMapping{}, false, nil } if len(inheritedLoweredTools) > 0 && len(inheritedLoweredTools[0]) > 0 { req["tools"] = restoreInheritedResponsesClientToolDeclarations(inheritedLoweredTools[0], inherited) return AdaptResponsesClientTools(req) } changed, err := rewriteClientToolHistory(req["input"], &inherited) if err != nil { return ResponsesClientToolMapping{}, false, err } if len(inherited.NamespaceTools) > 0 { before := changed rewriteNamespaceQualifiedCalls(req["input"], inherited.NamespaceTools) // Namespace rewriting does not currently report whether it changed a // value. A retained namespace mapping is only used for follow-up // history, so conservatively rebuild the request when input exists. if _, inputPresent := req["input"]; inputPresent && !before { changed = true } } if rewriteClientToolChoice(req, &inherited) { changed = true } return inherited, changed, nil } func copyClientTool(tool map[string]any) map[string]any { copy := make(map[string]any, len(tool)) for key, value := range tool { copy[key] = value } return copy } func rewriteClientToolHistory(value any, adapter *ResponsesClientToolMapping) (bool, error) { changed := false var visit func(any) error visit = func(value any) error { switch typed := value.(type) { case []any: for _, item := range typed { if err := visit(item); err != nil { return err } } case map[string]any: typ := strings.TrimSpace(stringValue(typed["type"])) switch typ { case "custom_tool_call": if adapter.CustomTools[strings.TrimSpace(stringValue(typed["name"]))] { typed["type"] = "function_call" typed["arguments"] = customToolCallArguments(stringValue(typed["input"])) delete(typed, "input") dropInvalidLoweredFunctionItemID(typed) changed = true } case "custom_tool_call_output": typed["type"] = "function_call_output" dropInvalidLoweredFunctionItemID(typed) normalizeClientToolOutput(typed) changed = true case "tool_search_call": if adapter.ToolSearch { typed["type"] = "function_call" typed["name"] = toolSearchProxyName typed["arguments"] = rawObjectString(typed["arguments"]) delete(typed, "execution") dropInvalidLoweredFunctionItemID(typed) changed = true } case "tool_search_output": if adapter.ToolSearch { callID := strings.TrimSpace(stringValue(typed["call_id"])) if callID == "" { return fmt.Errorf("tool_search_output requires a non-empty string call_id before it can be lowered to function_call_output") } typed["type"] = "function_call_output" dropInvalidLoweredFunctionItemID(typed) if err := normalizeToolSearchOutput(typed); err != nil { return err } changed = true } } for _, child := range typed { if err := visit(child); err != nil { return err } } } return nil } if err := visit(value); err != nil { return false, err } return changed, nil } // dropInvalidLoweredFunctionItemID removes Codex client-only item IDs such as // ctc_*, ctco_*, tsc_*, and tso_* after their item type is lowered to the // function protocol. Function upstreams validate these IDs with the fc prefix; // call_id, which is preserved separately, is the tool call/output pairing key. func dropInvalidLoweredFunctionItemID(item map[string]any) { id := strings.TrimSpace(stringValue(item["id"])) if id != "" && !strings.HasPrefix(id, "fc") { delete(item, "id") } } func normalizeClientToolOutput(item map[string]any) { output, exists := item["output"] if !exists { return } if _, ok := output.(string); ok { return } if output == nil { item["output"] = "" return } encoded, err := json.Marshal(output) if err != nil { item["output"] = "" return } item["output"] = string(encoded) } // normalizeToolSearchOutput converts both tool_search output wire shapes into // the string output required by function_call_output. Older clients send an // output field directly; newer Codex clients return discovered definitions in // a top-level tools field. Codex treats that field's value as the tool output, // so serialize the value directly rather than wrapping it in another object. func normalizeToolSearchOutput(item map[string]any) error { if output, hasOutput := item["output"]; hasOutput { switch typed := output.(type) { case string: item["output"] = typed case nil: item["output"] = "" default: encoded, err := json.Marshal(typed) if err != nil { return fmt.Errorf("tool_search_output output cannot be encoded as function_call_output output: %w", err) } item["output"] = string(encoded) } dropToolSearchOutputPrivateFields(item) return nil } tools, hasTools := item["tools"] if !hasTools { return fmt.Errorf("tool_search_output requires output or tools before it can be lowered to function_call_output") } encoded, err := json.Marshal(tools) if err != nil { return fmt.Errorf("tool_search_output tools cannot be encoded as function_call_output output: %w", err) } item["output"] = string(encoded) dropToolSearchOutputPrivateFields(item) return nil } func dropToolSearchOutputPrivateFields(item map[string]any) { delete(item, "tools") delete(item, "status") delete(item, "execution") } func rewriteClientToolChoice(req map[string]any, adapter *ResponsesClientToolMapping) bool { choice, ok := req["tool_choice"].(map[string]any) if !ok { return false } typ := strings.TrimSpace(stringValue(choice["type"])) name := strings.TrimSpace(stringValue(choice["name"])) if typ == "custom" && adapter.CustomTools[name] { choice["type"] = "function" return true } if typ == "tool_search" && adapter.ToolSearch { req["tool_choice"] = map[string]any{"type": "function", "name": toolSearchProxyName} return true } return false } func customToolCallArguments(input string) string { encoded, _ := json.Marshal(map[string]string{"input": input}) return string(encoded) } func rawObjectString(value any) string { if text, ok := value.(string); ok { return text } encoded, err := json.Marshal(value) if err != nil { return "{}" } return string(encoded) } // RestoreResponsesClientToolPayload restores client tool calls in a non-stream // native Responses JSON payload. func RestoreResponsesClientToolPayload(payload []byte, mapping ResponsesClientToolMapping) ([]byte, bool, error) { if len(payload) == 0 { return payload, false, nil } var value any if err := json.Unmarshal(payload, &value); err != nil { return payload, false, err } changed := restoreClientToolValue(value, &mapping) if !changed { if len(mapping.NamespaceTools) == 0 { return payload, false, nil } return RestoreResponsesNamespaceCalls(payload, mapping.NamespaceTools) } var rebuilt bytes.Buffer encoder := json.NewEncoder(&rebuilt) encoder.SetEscapeHTML(false) if err := encoder.Encode(value); err != nil { return payload, false, err } rebuiltPayload := bytes.TrimSuffix(rebuilt.Bytes(), []byte("\n")) if len(mapping.NamespaceTools) == 0 { return rebuiltPayload, true, nil } restored, _, err := RestoreResponsesNamespaceCalls(rebuiltPayload, mapping.NamespaceTools) if err != nil { return payload, false, err } return restored, true, nil } func restoreClientToolValue(value any, adapter *ResponsesClientToolMapping) bool { changed := false switch typed := value.(type) { case []any: for _, item := range typed { changed = restoreClientToolValue(item, adapter) || changed } case map[string]any: if strings.TrimSpace(stringValue(typed["type"])) == "function_call" { name := strings.TrimSpace(stringValue(typed["name"])) if adapter.CustomTools[name] { typed["type"] = "custom_tool_call" typed["input"] = extractCustomToolCallInput(rawObjectString(typed["arguments"])) delete(typed, "arguments") delete(typed, "namespace") changed = true } else if adapter.ToolSearch && name == toolSearchProxyName { typed["type"] = "tool_search_call" typed["execution"] = "client" typed["arguments"] = json.RawMessage(toolSearchCallArgumentsJSON(rawObjectString(typed["arguments"]))) delete(typed, "name") delete(typed, "namespace") changed = true } } for _, child := range typed { changed = restoreClientToolValue(child, adapter) || changed } } return changed } // ResponsesClientToolStreamRestorer restores client tool stream lifecycles. // It is intentionally stateful because custom tools need their function // arguments buffered until the upstream signals the call is complete. type ResponsesClientToolStreamRestorer struct { adapter ResponsesClientToolMapping nextSeq int seenSeq bool calls map[string]*responsesClientToolStreamCall byOutput map[int]*responsesClientToolStreamCall } type responsesClientToolStreamCall struct { kind string name string callID string itemID string outputIdx int arguments strings.Builder } func NewResponsesClientToolStreamRestorer(mapping ResponsesClientToolMapping) *ResponsesClientToolStreamRestorer { return &ResponsesClientToolStreamRestorer{adapter: mapping, calls: make(map[string]*responsesClientToolStreamCall), byOutput: make(map[int]*responsesClientToolStreamCall)} } // Restore transforms one upstream SSE event into zero or more client events. // Returned sequence numbers are continuous even when function argument events // are suppressed or a custom completion expands into two events. func (r *ResponsesClientToolStreamRestorer) Restore(event ResponsesStreamEvent) []ResponsesStreamEvent { if r == nil { return []ResponsesStreamEvent{event} } if !r.seenSeq { r.nextSeq = event.SequenceNumber r.seenSeq = true } var out []ResponsesStreamEvent emit := func(event ResponsesStreamEvent) { event.SequenceNumber = r.nextSeq r.nextSeq++ out = append(out, event) } switch event.Type { case "response.output_item.added": if call := r.recordItem(event); call != nil { if call.kind == "custom" { event.Item.Type = "custom_tool_call" event.Item.Input = "" event.Item.Arguments = "" event.Item.Namespace = "" } else { event.Item.Type = "tool_search_call" event.Item.Name = "" event.Item.Arguments = "{}" event.Item.Namespace = "" } } emit(r.restoreNamespaceEvent(event)) case "response.function_call_arguments.delta": if call := r.callFor(event); call != nil { _, _ = call.arguments.WriteString(event.Delta) return nil } emit(r.restoreNamespaceEvent(event)) case "response.function_call_arguments.done": if call := r.callFor(event); call != nil { if event.Arguments != "" { call.arguments.Reset() _, _ = call.arguments.WriteString(event.Arguments) } if call.kind == "custom" { input := extractCustomToolCallInput(call.arguments.String()) if input != "" { emit(ResponsesStreamEvent{Type: "response.custom_tool_call_input.delta", OutputIndex: call.outputIdx, ItemID: call.itemID, Delta: input}) } emit(ResponsesStreamEvent{Type: "response.custom_tool_call_input.done", OutputIndex: call.outputIdx, ItemID: call.itemID, CallID: call.callID, Name: call.name, Input: input}) } return out } emit(r.restoreNamespaceEvent(event)) case "response.output_item.done": if call := r.recordItem(event); call != nil { if call.kind == "custom" { event.Item.Type = "custom_tool_call" event.Item.Input = extractCustomToolCallInput(call.arguments.String()) event.Item.Arguments = "" event.Item.Namespace = "" } else { event.Item.Type = "tool_search_call" event.Item.Name = "" event.Item.Arguments = call.arguments.String() if strings.TrimSpace(event.Item.Arguments) == "" { event.Item.Arguments = "{}" } event.Item.Namespace = "" } delete(r.calls, call.itemID) delete(r.calls, call.callID) delete(r.byOutput, call.outputIdx) } emit(r.restoreNamespaceEvent(event)) default: // response.completed carries the non-stream representation. if event.Response != nil { restoreResponsesOutputClientTools(event.Response.Output, &r.adapter) } emit(r.restoreNamespaceEvent(event)) } return out } // RestoreEvent restores one Responses SSE JSON data payload. Custom tool // completions can expand to multiple payloads and proxy argument deltas can be // intentionally dropped, hence the slice return value. func (r *ResponsesClientToolStreamRestorer) RestoreEvent(payload []byte) ([][]byte, bool, error) { if len(payload) == 0 { return nil, false, nil } var wire struct { Type string `json:"type"` Sequence int `json:"sequence_number"` } if err := json.Unmarshal(payload, &wire); err != nil { return nil, false, err } if isResponsesClientToolTerminalEvent(wire.Type) { restored, changed, err := RestoreResponsesClientToolPayload(payload, r.adapter) if err != nil { return nil, false, err } return r.resequenceRaw(restored, wire.Sequence, changed) } if !clientToolLifecycleEvent(wire.Type) { return r.resequenceRaw(payload, wire.Sequence, false) } if !r.clientToolEventPayload(payload) { return r.resequenceRaw(payload, wire.Sequence, false) } var event ResponsesStreamEvent if err := json.Unmarshal(payload, &event); err != nil { return nil, false, err } events := r.Restore(event) if len(events) == 1 { unchanged, err := json.Marshal(events[0]) if err == nil && bytes.Equal(bytes.TrimSpace(unchanged), bytes.TrimSpace(payload)) { return [][]byte{payload}, false, nil } } result := make([][]byte, 0, len(events)) for _, restored := range events { encoded, err := json.Marshal(restored) if err != nil { return nil, false, err } result = append(result, encoded) } return result, true, nil } func isResponsesClientToolTerminalEvent(typ string) bool { switch strings.TrimSpace(typ) { case "response.completed", "response.done", "response.incomplete", "response.failed", "response.cancelled", "response.canceled": return true default: return false } } func (r *ResponsesClientToolStreamRestorer) clientToolEventPayload(payload []byte) bool { var raw struct { ItemID string `json:"item_id"` CallID string `json:"call_id"` Name string `json:"name"` OutputIndex int `json:"output_index"` Item *struct { Type string `json:"type"` ID string `json:"id"` CallID string `json:"call_id"` Name string `json:"name"` } `json:"item"` } if err := json.Unmarshal(payload, &raw); err != nil { return false } if raw.Item != nil { if raw.Item.Type != "function_call" { return false } _, namespaceTool := r.adapter.NamespaceTools[raw.Item.Name] return r.adapter.CustomTools[raw.Item.Name] || (r.adapter.ToolSearch && raw.Item.Name == toolSearchProxyName) || namespaceTool || r.calls[raw.Item.ID] != nil || r.calls[raw.Item.CallID] != nil } if _, namespaceTool := r.adapter.NamespaceTools[raw.Name]; namespaceTool { return true } if r.calls[raw.ItemID] != nil || r.calls[raw.CallID] != nil || r.byOutput[raw.OutputIndex] != nil { return true } return false } func clientToolLifecycleEvent(typ string) bool { switch typ { case "response.output_item.added", "response.output_item.done", "response.function_call_arguments.delta", "response.function_call_arguments.done": return true default: return false } } // resequenceRaw deliberately keeps opaque upstream event fields untouched. func (r *ResponsesClientToolStreamRestorer) resequenceRaw(payload []byte, sequence int, changed bool) ([][]byte, bool, error) { if !r.seenSeq { r.nextSeq, r.seenSeq = sequence, true } if r.nextSeq == sequence && !changed { r.nextSeq++ return [][]byte{payload}, false, nil } var raw map[string]any if err := json.Unmarshal(payload, &raw); err != nil { return nil, false, err } raw["sequence_number"] = r.nextSeq r.nextSeq++ encoded, err := json.Marshal(raw) if err != nil { return nil, false, err } return [][]byte{encoded}, true, nil } func (r *ResponsesClientToolStreamRestorer) recordItem(event ResponsesStreamEvent) *responsesClientToolStreamCall { if event.Item == nil || event.Item.Type != "function_call" { return nil } name := event.Item.Name kind := "" if r.adapter.CustomTools[name] { kind = "custom" } else if r.adapter.ToolSearch && name == toolSearchProxyName { kind = "tool_search" } if kind == "" { return nil } key := event.Item.ID if key == "" { key = event.Item.CallID } call := r.calls[key] if call == nil { call = &responsesClientToolStreamCall{kind: kind, name: name, callID: event.Item.CallID, itemID: event.Item.ID, outputIdx: event.OutputIndex} r.calls[key] = call if call.callID != "" { r.calls[call.callID] = call } r.byOutput[call.outputIdx] = call } if event.Item.Arguments != "" { call.arguments.Reset() _, _ = call.arguments.WriteString(event.Item.Arguments) } return call } func (r *ResponsesClientToolStreamRestorer) callFor(event ResponsesStreamEvent) *responsesClientToolStreamCall { if call := r.calls[event.ItemID]; call != nil { return call } if call := r.byOutput[event.OutputIndex]; call != nil { return call } for _, call := range r.calls { if (event.CallID != "" && call.callID == event.CallID) || (event.ItemID == "" && event.Name != "" && call.name == event.Name) { return call } } return nil } func (r *ResponsesClientToolStreamRestorer) restoreNamespaceEvent(event ResponsesStreamEvent) ResponsesStreamEvent { if len(r.adapter.NamespaceTools) == 0 { return event } if event.Item != nil && event.Item.Type == "function_call" { if name, ok := r.adapter.NamespaceTools[event.Item.Name]; ok { event.Item.Name, event.Item.Namespace = name.Name, name.Namespace } } if event.Type == "response.function_call_arguments.delta" || event.Type == "response.function_call_arguments.done" { if name, ok := r.adapter.NamespaceTools[event.Name]; ok { event.Name = name.Name } } return event } func restoreResponsesOutputClientTools(outputs []ResponsesOutput, adapter *ResponsesClientToolMapping) { for index := range outputs { output := &outputs[index] if output.Type != "function_call" { continue } if adapter.CustomTools[output.Name] { output.Type = "custom_tool_call" output.Input = extractCustomToolCallInput(output.Arguments) output.Arguments = "" output.Namespace = "" } else if adapter.ToolSearch && output.Name == toolSearchProxyName { output.Type = "tool_search_call" output.Name = "" output.Namespace = "" } if name, ok := adapter.NamespaceTools[output.Name]; ok && output.Type == "function_call" { output.Name, output.Namespace = name.Name, name.Namespace } } }