| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883 |
- 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 stripResponsesDeferredToolFlags(lowered) {
- changed = true
- }
- 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
- }
- // stripResponsesDeferredToolFlags removes defer_loading only when the final
- // declaration list no longer contains the built-in tool_search it requires.
- func stripResponsesDeferredToolFlags(tools []any) bool {
- if hasResponsesToolSearchDeclaration(tools) {
- return false
- }
- changed := false
- for _, raw := range tools {
- tool, ok := raw.(map[string]any)
- if !ok {
- continue
- }
- if _, exists := tool["defer_loading"]; exists {
- delete(tool, "defer_loading")
- changed = true
- }
- }
- return changed
- }
- // 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")
- normalizeLoweredFunctionItemID(typed)
- changed = true
- }
- case "custom_tool_call_output":
- typed["type"] = "function_call_output"
- normalizeLoweredFunctionItemID(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")
- normalizeLoweredFunctionItemID(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"
- normalizeLoweredFunctionItemID(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
- }
- // normalizeLoweredFunctionItemID reconciles Codex client-only item IDs such as
- // ctc_*, ctco_*, tsc_*, and tso_* after their item type is lowered to the
- // function protocol, which validates these IDs with the fc prefix. A ctc_/tsc_
- // call ID maps back to the fc_ ID it was raised from; the remaining IDs have no
- // function-protocol counterpart and are dropped. call_id, which is preserved
- // separately, stays the tool call/output pairing key either way.
- func normalizeLoweredFunctionItemID(item map[string]any) {
- id := strings.TrimSpace(stringValue(item["id"]))
- if id == "" || strings.HasPrefix(id, "fc") {
- return
- }
- // A ctc_/tsc_ call ID on a lowered call item is the one we minted from the
- // upstream's own fc_ ID when the item was raised, so map it back instead of
- // dropping it. Anything else has no known function-protocol counterpart.
- if recovered := retypedResponsesToolCallItemID(id, "function_call"); recovered != id {
- item["id"] = recovered
- return
- }
- delete(item, "id")
- }
- // responsesToolCallItemIDPrefixes lists the Responses item ID prefixes that are
- // tied to a specific tool-call item type.
- var responsesToolCallItemIDPrefixes = []string{"fc_", "ctc_", "tsc_"}
- // responsesToolCallItemIDPrefix reports the ID prefix the Responses API
- // validates for itemType, or "" when the type constrains no prefix.
- func responsesToolCallItemIDPrefix(itemType string) string {
- switch itemType {
- case "custom_tool_call":
- return "ctc_"
- case "tool_search_call":
- return "tsc_"
- case "function_call":
- return "fc_"
- default:
- return ""
- }
- }
- // retypedResponsesToolCallItemID re-prefixes an upstream item ID so it agrees
- // with the item type we raise it back to. A function-only upstream answers a
- // lowered custom tool with an fc_ item ID; emitting that ID on the restored
- // custom_tool_call poisons the client's history, because a later replay of the
- // same item to an upstream that validates IDs fails with
- // "Invalid 'input[N].id' ... Expected an ID that begins with 'ctc'".
- // The suffix is preserved so the ID stays stable and unique per upstream item.
- // IDs that carry no known tool-call prefix are left alone rather than guessed at.
- func retypedResponsesToolCallItemID(id, itemType string) string {
- want := responsesToolCallItemIDPrefix(itemType)
- if want == "" || id == "" || strings.HasPrefix(id, want) {
- return id
- }
- for _, known := range responsesToolCallItemIDPrefixes {
- if known != want && strings.HasPrefix(id, known) {
- return want + strings.TrimPrefix(id, known)
- }
- }
- return id
- }
- // retypeResponsesToolCallItemID applies retypedResponsesToolCallItemID to a
- // decoded item map.
- func retypeResponsesToolCallItemID(item map[string]any, itemType string) {
- id := strings.TrimSpace(stringValue(item["id"]))
- if retyped := retypedResponsesToolCallItemID(id, itemType); retyped != id {
- item["id"] = retyped
- }
- }
- func normalizeClientToolOutput(item map[string]any) {
- output, exists := item["output"]
- if !exists {
- return
- }
- if _, ok := output.(string); ok {
- return
- }
- if isResponsesToolOutputContent(output) {
- return
- }
- if output == nil {
- item["output"] = ""
- return
- }
- encoded, err := json.Marshal(output)
- if err != nil {
- item["output"] = ""
- return
- }
- item["output"] = string(encoded)
- }
- func isResponsesToolOutputContent(output any) bool {
- parts, ok := output.([]any)
- if !ok || len(parts) == 0 {
- return false
- }
- for _, part := range parts {
- typed, ok := part.(map[string]any)
- if !ok {
- return false
- }
- switch stringValue(typed["type"]) {
- case "input_text", "input_image", "input_file":
- default:
- return false
- }
- }
- return true
- }
- // 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"
- retypeResponsesToolCallItemID(typed, "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"
- retypeResponsesToolCallItemID(typed, "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 and itemID stay as the upstream sent them so later upstream
- // events keep matching this call; clientItemID is what we emit.
- callID string
- itemID string
- clientItemID 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 = ""
- }
- if call.clientItemID != "" {
- event.Item.ID = call.clientItemID
- }
- }
- 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.clientItemID, Delta: input})
- }
- emit(ResponsesStreamEvent{Type: "response.custom_tool_call_input.done", OutputIndex: call.outputIdx, ItemID: call.clientItemID, 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 = ""
- }
- if call.clientItemID != "" {
- event.Item.ID = call.clientItemID
- }
- 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
- }
- // responsesClientToolItemType maps a restorer call kind to the item type the
- // client sees.
- func responsesClientToolItemType(kind string) string {
- if kind == "custom" {
- return "custom_tool_call"
- }
- return "tool_search_call"
- }
- 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,
- clientItemID: retypedResponsesToolCallItemID(event.Item.ID, responsesClientToolItemType(kind)),
- 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.ID = retypedResponsesToolCallItemID(output.ID, output.Type)
- output.Input = extractCustomToolCallInput(output.Arguments)
- output.Arguments = ""
- output.Namespace = ""
- } else if adapter.ToolSearch && output.Name == toolSearchProxyName {
- output.Type = "tool_search_call"
- output.ID = retypedResponsesToolCallItemID(output.ID, output.Type)
- output.Name = ""
- output.Namespace = ""
- }
- if name, ok := adapter.NamespaceTools[output.Name]; ok && output.Type == "function_call" {
- output.Name, output.Namespace = name.Name, name.Namespace
- }
- }
- }
|