internal/util/responses_tools.go
Rama main · réplica de lectura del repositorio original.
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
}