package util import ( "crypto/sha256" "encoding/hex" "fmt" "sort" "strings" "github.com/MoeForge/moego/ojson" applypatch "github.com/router-for-me/CLIProxyAPI/v8/internal/client/codex/apply-patch" ) // ResponsesToolIdentity represents the resolved identity of a tool in OpenAI Responses format. type ResponsesToolIdentity struct { Name string Namespace string Custom bool ApplyPatch bool // Resolved from the winning original declaration, never from the upstream name. ToolSearch bool // Marks the function shim standing in for a client-executed tool_search declaration. } const ( // ResponsesToolSearchType is the Responses tool type for deferred tool discovery. ResponsesToolSearchType = "tool_search" // ResponsesToolSearchExecutionClient marks a tool_search that the client executes itself. ResponsesToolSearchExecutionClient = "client" // ResponsesToolSearchFunctionName is the function name exposed to providers // that have no native tool_search support. ResponsesToolSearchFunctionName = "tool_search" // ResponsesClientToolSearchKey is the internal forwardMap key for the // client-executed tool_search shim. The leading NUL byte cannot appear in a // JSON identifier, preventing collisions with user-declared tool names. ResponsesClientToolSearchKey = "\x00client_tool_search" // ResponsesToolSearchCallType is the Responses item type for a tool_search invocation. ResponsesToolSearchCallType = "tool_search_call" // ResponsesToolSearchOutputType is the Responses input item carrying discovered tools. ResponsesToolSearchOutputType = "tool_search_output" // ResponsesDiscoveredToolPriority ranks tools loaded by tool_search_output after declared tools. ResponsesDiscoveredToolPriority = 2 responsesToolSearchDefaultParameters = `{"type":"object","properties":{"query":{"type":"string","description":"Search query for deferred tools."},"limit":{"type":"number","description":"Maximum number of tools to return."}},"required":["query"],"additionalProperties":false}` ) // ResponsesToolDescriptor is an internal representation of a tool declaration in a Responses request. type ResponsesToolDescriptor struct { Name string // Qualified name (e.g. "functions__exec" or "exec") LocalName string // Local name without namespace (e.g. "exec") Namespace string // Namespace if any (e.g. "functions") ToolType string // "function", "custom", etc. Tool ojson.Result SourcePriority int // 0 for top-level tools, 1 for additional_tools, 2 for tool_search_output Direct bool // true if declared directly, false if declared as namespace child Order int // original discovery order ClientSearch bool // true for a client-executed tool_search declaration } // QualifyResponsesNamespaceToolName qualifies a child tool name with its namespace. func QualifyResponsesNamespaceToolName(namespaceName, childName string) string { childName = strings.TrimSpace(childName) namespaceName = strings.TrimSpace(namespaceName) if childName == "" || namespaceName == "" || strings.HasPrefix(childName, "mcp__") { return childName } if childName == namespaceName || strings.HasPrefix(childName, namespaceName+"__") { return childName } if strings.HasSuffix(namespaceName, "__") { return namespaceName + childName } return namespaceName + "__" + childName } func responsesToolSources(root ojson.Result, includeClientToolSearch bool) []struct { tools ojson.Result priority int } { return responsesToolSourcesFromGetter(root.Get, includeClientToolSearch) } func responsesToolSourcesFromGetter(get func(string) ojson.Result, includeClientToolSearch bool) []struct { tools ojson.Result priority int } { if get == nil { return nil } var sources []struct { tools ojson.Result priority int } appendSource := func(tools ojson.Result, priority int) { if tools.Exists() && tools.IsArray() { sources = append(sources, struct { tools ojson.Result priority int }{tools: tools, priority: priority}) } } appendSource(get("tools"), 0) if input := get("input"); input.Exists() && input.IsArray() { input.ForEach(func(_, item ojson.Result) bool { if item.Get("type").String() == "additional_tools" { appendSource(item.Get("tools"), 1) } return true }) if includeClientToolSearch { input.ForEach(func(_, item ojson.Result) bool { if item.Get("type").String() == ResponsesToolSearchOutputType && IsResponsesClientToolSearchItem(item) { appendSource(item.Get("tools"), ResponsesDiscoveredToolPriority) } return true }) } } return sources } // IsResponsesClientToolSearchDeclaration reports whether a tool declaration is // a client-executed Responses tool_search. func IsResponsesClientToolSearchDeclaration(tool ojson.Result) bool { return strings.TrimSpace(tool.Get("type").String()) == ResponsesToolSearchType && strings.TrimSpace(tool.Get("execution").String()) == ResponsesToolSearchExecutionClient } // IsResponsesClientToolSearchItem reports whether a tool_search_call or // tool_search_output input item belongs to client-executed tool search. // Hosted server search explicitly sets execution to "server". func IsResponsesClientToolSearchItem(item ojson.Result) bool { execution := strings.TrimSpace(item.Get("execution").String()) return execution == "" || execution == ResponsesToolSearchExecutionClient } func responsesToolName(tool ojson.Result) string { if name := strings.TrimSpace(tool.Get("name").String()); name != "" { return name } return strings.TrimSpace(tool.Get("function.name").String()) } // ResponsesToolDescription extracts the description from a tool or function object. func ResponsesToolDescription(tool ojson.Result) string { if description := tool.Get("description").String(); description != "" { return description } return tool.Get("function.description").String() } func responsesToolDescription(tool ojson.Result) string { return ResponsesToolDescription(tool) } // ResponsesToolParameters extracts the schema/parameters from a tool or function object. func ResponsesToolParameters(tool ojson.Result) ojson.Result { for _, path := range []string{ "parameters", "parametersJsonSchema", "input_schema", "function.parameters", "function.parametersJsonSchema", } { if parameters := tool.Get(path); parameters.Exists() { return parameters } } return ojson.Result{} } func responsesToolParameters(tool ojson.Result) ojson.Result { return ResponsesToolParameters(tool) } // CollectResponsesToolDescriptors extracts all tool descriptors from a Responses request root. func CollectResponsesToolDescriptors(root ojson.Result) []ResponsesToolDescriptor { return collectResponsesToolDescriptors(root, false) } // CollectResponsesToolDescriptorsDoc extracts all tool descriptors from a Responses request doc. func CollectResponsesToolDescriptorsDoc(doc *ojson.Doc) []ResponsesToolDescriptor { if doc == nil || doc.Len() == 0 { return nil } return collectResponsesToolDescriptorsFromGetter(doc.Get, false) } func collectResponsesToolDescriptors(root ojson.Result, includeClientToolSearch bool) []ResponsesToolDescriptor { return collectResponsesToolDescriptorsFromGetter(root.Get, includeClientToolSearch) } func collectResponsesToolDescriptorsFromGetter(get func(string) ojson.Result, includeClientToolSearch bool) []ResponsesToolDescriptor { var descriptors []ResponsesToolDescriptor appendDescriptor := func(tool ojson.Result, name, localName, namespace string, toolType string, sourcePriority int, direct bool, clientSearch bool) { if name == "" { return } descriptors = append(descriptors, ResponsesToolDescriptor{ Name: name, LocalName: localName, Namespace: namespace, ToolType: toolType, Tool: tool, SourcePriority: sourcePriority, Direct: direct, Order: len(descriptors), ClientSearch: clientSearch, }) } appendNamespaceChildren := func(namespaceTool ojson.Result, sourcePriority int) { namespaceName := strings.TrimSpace(namespaceTool.Get("name").String()) children := namespaceTool.Get("tools") if !children.Exists() || !children.IsArray() { children = namespaceTool.Get("children") } if !children.Exists() || !children.IsArray() { return } children.ForEach(func(_, child ojson.Result) bool { childName := responsesToolName(child) if childName == "" { return true } qualifiedName := QualifyResponsesNamespaceToolName(namespaceName, childName) switch strings.TrimSpace(child.Get("type").String()) { case "", "function": appendDescriptor(child, qualifiedName, childName, namespaceName, "function", sourcePriority, false, false) case "custom": appendDescriptor(child, qualifiedName, childName, namespaceName, "custom", sourcePriority, false, false) } return true }) } for _, source := range responsesToolSourcesFromGetter(get, includeClientToolSearch) { source.tools.ForEach(func(_, tool ojson.Result) bool { toolType := strings.TrimSpace(tool.Get("type").String()) switch toolType { case "", "function": name := responsesToolName(tool) appendDescriptor(tool, name, name, "", "function", source.priority, true, false) case "custom": name := responsesToolName(tool) appendDescriptor(tool, name, name, "", "custom", source.priority, true, false) case "namespace": appendNamespaceChildren(tool, source.priority) case ResponsesToolSearchType: if includeClientToolSearch && source.priority < ResponsesDiscoveredToolPriority && IsResponsesClientToolSearchDeclaration(tool) { appendDescriptor(tool, ResponsesToolSearchFunctionName, ResponsesToolSearchFunctionName, "", ResponsesToolSearchType, source.priority, true, true) } } return true }) } if !includeClientToolSearch || !responsesHasClientToolSearch(descriptors) { return descriptors } kept := make([]ResponsesToolDescriptor, 0, len(descriptors)) for _, descriptor := range descriptors { if !descriptor.ClientSearch && descriptor.SourcePriority != ResponsesDiscoveredToolPriority && descriptor.Tool.Get("defer_loading").Bool() { continue } descriptor.Order = len(kept) kept = append(kept, descriptor) } return kept } func responsesHasClientToolSearch(descriptors []ResponsesToolDescriptor) bool { for i := range descriptors { if descriptors[i].ClientSearch { return true } } return false } func responsesToolDescriptorPrecedes(left, right ResponsesToolDescriptor) bool { if left.SourcePriority != right.SourcePriority { return left.SourcePriority < right.SourcePriority } if left.Direct != right.Direct { return left.Direct } return left.Order < right.Order } // CollectResponsesToolWinners collects deduplicated winning descriptors for each qualified tool name. func CollectResponsesToolWinners(root ojson.Result) map[string]ResponsesToolDescriptor { return collectResponsesToolWinnersFromDescriptors(CollectResponsesToolDescriptors(root)) } // CollectResponsesToolWinnersDoc collects deduplicated winning descriptors from doc. func CollectResponsesToolWinnersDoc(doc *ojson.Doc) map[string]ResponsesToolDescriptor { return collectResponsesToolWinnersFromDescriptors(CollectResponsesToolDescriptorsDoc(doc)) } func collectResponsesToolWinnersFromDescriptors(descriptors []ResponsesToolDescriptor) map[string]ResponsesToolDescriptor { winners := map[string]ResponsesToolDescriptor{} for _, descriptor := range descriptors { current, exists := winners[descriptor.Name] if !exists || responsesToolDescriptorPrecedes(descriptor, current) { winners[descriptor.Name] = descriptor } } return winners } func sanitizeResponsesToolNames(names []string) map[string]string { if len(names) == 0 { return nil } uniqueNames := make(map[string]struct{}, len(names)) baseCounts := make(map[string]int, len(names)) for _, name := range names { if name == "" { continue } if _, exists := uniqueNames[name]; exists { continue } uniqueNames[name] = struct{}{} baseCounts[SanitizeFunctionName(name)]++ } sortedNames := make([]string, 0, len(uniqueNames)) for name := range uniqueNames { sortedNames = append(sortedNames, name) } sort.Strings(sortedNames) out := make(map[string]string, len(sortedNames)) used := make(map[string]string, len(sortedNames)) for _, name := range sortedNames { base := SanitizeFunctionName(name) mapped := base _, baseUsed := used[base] if baseCounts[base] > 1 || baseUsed { mapped = disambiguateResponsesSanitizedName(base, name, used) } out[name] = mapped used[mapped] = name } return out } func disambiguateResponsesSanitizedName(base, original string, used map[string]string) string { for attempt := 0; ; attempt++ { digest := sha256.Sum256([]byte(fmt.Sprintf("%s\x00%d", original, attempt))) suffix := "_" + hex.EncodeToString(digest[:6]) prefix := base if maxPrefix := 64 - len(suffix); len(prefix) > maxPrefix { prefix = prefix[:maxPrefix] } candidate := prefix + suffix if _, exists := used[candidate]; !exists { return candidate } } } type responsesToolWinnerKey struct { name string clientSearch bool } // BuildGeminiFunctionDeclarations builds Gemini function declarations, forward name mapping, and reverse identity mapping. func BuildGeminiFunctionDeclarations(root ojson.Result) ([][]byte, map[string]string, map[string]ResponsesToolIdentity) { return buildGeminiFunctionDeclarations(root, true) } // buildGeminiFunctionDeclarations skips per-tool declaration construction and schema // cleanup when the caller only needs the name and identity maps. func buildGeminiFunctionDeclarations(root ojson.Result, withDeclarations bool) ([][]byte, map[string]string, map[string]ResponsesToolIdentity) { descriptors := collectResponsesToolDescriptors(root, true) winners := make(map[responsesToolWinnerKey]ResponsesToolDescriptor, len(descriptors)) for _, descriptor := range descriptors { key := responsesToolWinnerKey{name: descriptor.Name, clientSearch: descriptor.ClientSearch} current, exists := winners[key] if !exists || responsesToolDescriptorPrecedes(descriptor, current) { winners[key] = descriptor } } seenKeys := make(map[responsesToolWinnerKey]struct{}, len(winners)) var winningList []ResponsesToolDescriptor for _, descriptor := range descriptors { key := responsesToolWinnerKey{name: descriptor.Name, clientSearch: descriptor.ClientSearch} winner, ok := winners[key] if !ok || winner.Order != descriptor.Order { continue } if _, seen := seenKeys[key]; seen { continue } seenKeys[key] = struct{}{} winningList = append(winningList, descriptor) } hasClientSearchHistory := false if include := root.Get("input"); include.Exists() && include.IsArray() { include.ForEach(func(_, item ojson.Result) bool { typ := item.Get("type").String() if (typ == ResponsesToolSearchCallType || typ == ResponsesToolSearchOutputType) && IsResponsesClientToolSearchItem(item) { hasClientSearchHistory = true return false } return true }) } if len(winningList) == 0 { if !hasClientSearchHistory { return nil, nil, nil } forwardMap := map[string]string{ ResponsesClientToolSearchKey: ResponsesToolSearchFunctionName, ResponsesToolSearchFunctionName: ResponsesToolSearchFunctionName, } reverseMap := map[string]ResponsesToolIdentity{ ResponsesToolSearchFunctionName: { Name: ResponsesToolSearchFunctionName, ToolSearch: true, }, } return nil, forwardMap, reverseMap } hasClientSearch := false userOwnsExactToolSearch := false qualifiedNames := make([]string, 0, len(winningList)) for _, desc := range winningList { if desc.ClientSearch { hasClientSearch = true continue } if desc.Name == ResponsesToolSearchFunctionName { userOwnsExactToolSearch = true } qualifiedNames = append(qualifiedNames, desc.Name) } sanitizedMap := sanitizeResponsesToolNames(qualifiedNames) if sanitizedMap == nil { sanitizedMap = make(map[string]string, len(winningList)) } clientSearchGeminiName := "" if hasClientSearch || hasClientSearchHistory { used := make(map[string]string, len(sanitizedMap)+1) for origName, mappedName := range sanitizedMap { used[mappedName] = origName } if !userOwnsExactToolSearch { // Reserve "tool_search" for the client tool_search shim and // disambiguate any non-exact tool (e.g. "tool.search") that sanitized to it. if collidingOrig, taken := used[ResponsesToolSearchFunctionName]; taken && collidingOrig != ResponsesToolSearchFunctionName { used[ResponsesToolSearchFunctionName] = ResponsesClientToolSearchKey newMapped := disambiguateResponsesSanitizedName(ResponsesToolSearchFunctionName, collidingOrig, used) sanitizedMap[collidingOrig] = newMapped used[newMapped] = collidingOrig } else { used[ResponsesToolSearchFunctionName] = ResponsesClientToolSearchKey } clientSearchGeminiName = ResponsesToolSearchFunctionName } else { clientSearchGeminiName = disambiguateResponsesSanitizedName(ResponsesToolSearchFunctionName, ResponsesClientToolSearchKey, used) used[clientSearchGeminiName] = ResponsesClientToolSearchKey } } forwardMap := make(map[string]string, len(winningList)*2+1) reverseMap := make(map[string]ResponsesToolIdentity, len(winningList)*2+1) if clientSearchGeminiName != "" { forwardMap[ResponsesClientToolSearchKey] = clientSearchGeminiName forwardMap[clientSearchGeminiName] = clientSearchGeminiName if !userOwnsExactToolSearch { forwardMap[ResponsesToolSearchFunctionName] = clientSearchGeminiName } reverseMap[clientSearchGeminiName] = ResponsesToolIdentity{ Name: ResponsesToolSearchFunctionName, ToolSearch: true, } } var declarations [][]byte for _, desc := range winningList { geminiName := desc.Name if desc.ClientSearch { geminiName = clientSearchGeminiName } else if mapped, ok := sanitizedMap[desc.Name]; ok && mapped != "" { geminiName = mapped } else { geminiName = SanitizeFunctionName(desc.Name) } if !desc.ClientSearch { forwardMap[desc.Name] = geminiName } forwardMap[geminiName] = geminiName identity := ResponsesToolIdentity{ Name: desc.LocalName, Namespace: desc.Namespace, Custom: desc.ToolType == "custom", ApplyPatch: applypatch.IsCustomTool(desc.Tool), ToolSearch: desc.ClientSearch, } reverseMap[geminiName] = identity if !desc.ClientSearch && desc.Name != geminiName { reverseMap[desc.Name] = identity } if !withDeclarations { continue } funcDeclDoc := ojson.NewDoc([]byte(`{"name":"","description":"","parametersJsonSchema":{}}`)) _ = funcDeclDoc.Set("name", geminiName) if descStr := responsesToolDescription(desc.Tool); descStr != "" { _ = funcDeclDoc.Set("description", descStr) } if applypatch.IsCustomTool(desc.Tool) { _ = funcDeclDoc.Set("description", applypatch.Description(desc.Tool)) _ = funcDeclDoc.SetRawBytes("parametersJsonSchema", applypatch.Parameters()) } else if desc.ToolType == "custom" { _ = funcDeclDoc.SetRaw("parametersJsonSchema", `{"type":"object","properties":{"input":{"type":"string"}},"required":["input"]}`) } else if desc.ClientSearch { _ = funcDeclDoc.SetRawBytes("parametersJsonSchema", buildGeminiToolSearchParametersSchema(desc.Tool)) } else { params := responsesToolParameters(desc.Tool) if params.Exists() { _ = funcDeclDoc.SetRaw("parametersJsonSchema", CleanJSONSchemaForGeminiJSONSchema(params.Raw)) } } declarations = append(declarations, funcDeclDoc.Bytes()) } // Populate local-name aliases after all direct/qualified names are registered // so a namespace child's local name never shadows a direct tool or the search shim. for _, desc := range winningList { if !desc.ClientSearch && desc.LocalName != "" && desc.LocalName != desc.Name { if _, exists := forwardMap[desc.LocalName]; !exists { forwardMap[desc.LocalName] = forwardMap[desc.Name] } } } return declarations, forwardMap, reverseMap } func buildGeminiToolSearchParametersSchema(tool ojson.Result) []byte { if params := responsesToolParameters(tool); params.Exists() && params.IsObject() { cleaned := CleanJSONSchemaForGeminiJSONSchema(params.Raw) if ojson.Valid(cleaned) && ojson.Parse(cleaned).IsObject() { return []byte(cleaned) } } return []byte(responsesToolSearchDefaultParameters) } // ResponsesToolReverseIdentityMap builds a Gemini function name -> ResponsesToolIdentity map from a Responses request raw JSON. func ResponsesToolReverseIdentityMap(rawJSON []byte) map[string]ResponsesToolIdentity { if len(rawJSON) == 0 || !ojson.ValidBytes(rawJSON) { return nil } root := ojson.ParseBytes(rawJSON) if req := root.Get("request"); req.Exists() && (req.Get("model").Exists() || req.Get("input").Exists() || req.Get("tools").Exists()) { root = req } _, _, reverseMap := buildGeminiFunctionDeclarations(root, false) return reverseMap } // MapResponsesToolName returns the mapped Gemini function name if present in forwardMap, else sanitized name. func MapResponsesToolName(forwardMap map[string]string, name string) string { if mapped, ok := forwardMap[name]; ok && mapped != "" { return mapped } return SanitizeFunctionName(name) } // MapResponsesClientToolSearchName returns the mapped Gemini function name for // the client-executed tool_search shim. func MapResponsesClientToolSearchName(forwardMap map[string]string) string { if mapped, ok := forwardMap[ResponsesClientToolSearchKey]; ok && mapped != "" { return mapped } return ResponsesToolSearchFunctionName } // ConvertResponsesToolChoiceToGemini translates Responses tool_choice into Gemini functionCallingConfig JSON. func ConvertResponsesToolChoiceToGemini(toolChoice ojson.Result, forwardMap map[string]string) ([]byte, bool) { if !toolChoice.Exists() { return nil, false } mode := "" var allowedNames []string if toolChoice.Type == ojson.String { switch strings.ToLower(strings.TrimSpace(toolChoice.String())) { case "none": mode = "NONE" case "auto": mode = "AUTO" case "required", "any": mode = "ANY" } } else if toolChoice.IsObject() { toolType := strings.ToLower(strings.TrimSpace(toolChoice.Get("type").String())) switch toolType { case "none": mode = "NONE" case "auto": mode = "AUTO" case "required", "any": mode = "ANY" case ResponsesToolSearchType: mode = "ANY" allowedNames = append(allowedNames, MapResponsesClientToolSearchName(forwardMap)) case "function", "custom", "tool", "": mode = "ANY" name := strings.TrimSpace(toolChoice.Get("name").String()) if name == "" { name = strings.TrimSpace(toolChoice.Get("function.name").String()) } if name == "" { name = strings.TrimSpace(toolChoice.Get("custom.name").String()) } namespace := strings.TrimSpace(toolChoice.Get("namespace").String()) if namespace == "" { namespace = strings.TrimSpace(toolChoice.Get("function.namespace").String()) } if namespace == "" { namespace = strings.TrimSpace(toolChoice.Get("custom.namespace").String()) } if namespace != "" { name = QualifyResponsesNamespaceToolName(namespace, name) } if name != "" { geminiName := MapResponsesToolName(forwardMap, name) allowedNames = append(allowedNames, geminiName) } } } if mode == "" { return nil, false } cfgDoc := ojson.NewDoc([]byte(`{"mode":""}`)) _ = cfgDoc.Set("mode", mode) if len(allowedNames) > 0 { _ = cfgDoc.Set("allowedFunctionNames", allowedNames) } return cfgDoc.Bytes(), true } // UnwrapResponsesCustomToolInput extracts the raw input string from custom tool arguments JSON or plain string. func UnwrapResponsesCustomToolInput(arguments string) string { arguments = strings.TrimSpace(arguments) if arguments == "" || arguments == "{}" { return "" } if ojson.Valid(arguments) { parsed := ojson.Parse(arguments) if v := parsed.Get("input"); v.Exists() { if v.Type == ojson.String { return v.String() } return v.Raw } if parsed.Type == ojson.String { return parsed.String() } } return arguments }