Add projects
This commit is contained in:
parent
2d3a9ad623
commit
8b607dd700
1802 changed files with 503346 additions and 2 deletions
418
backend/internal/util/responses_tools.go
Normal file
418
backend/internal/util/responses_tools.go
Normal file
|
|
@ -0,0 +1,418 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
// ResponsesToolIdentity represents the resolved identity of a tool in OpenAI Responses format.
|
||||
type ResponsesToolIdentity struct {
|
||||
Name string
|
||||
Namespace string
|
||||
Custom bool
|
||||
}
|
||||
|
||||
// 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 gjson.Result
|
||||
SourcePriority int // 0 for top-level tools, 1 for additional_tools
|
||||
Direct bool // true if declared directly, false if declared as namespace child
|
||||
Order int // original discovery order
|
||||
}
|
||||
|
||||
// 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 gjson.Result) []struct {
|
||||
tools gjson.Result
|
||||
priority int
|
||||
} {
|
||||
var sources []struct {
|
||||
tools gjson.Result
|
||||
priority int
|
||||
}
|
||||
appendSource := func(tools gjson.Result, priority int) {
|
||||
if tools.Exists() && tools.IsArray() {
|
||||
sources = append(sources, struct {
|
||||
tools gjson.Result
|
||||
priority int
|
||||
}{tools: tools, priority: priority})
|
||||
}
|
||||
}
|
||||
appendSource(root.Get("tools"), 0)
|
||||
if input := root.Get("input"); input.Exists() && input.IsArray() {
|
||||
input.ForEach(func(_, item gjson.Result) bool {
|
||||
if item.Get("type").String() == "additional_tools" {
|
||||
appendSource(item.Get("tools"), 1)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
return sources
|
||||
}
|
||||
|
||||
func responsesToolName(tool gjson.Result) string {
|
||||
if name := strings.TrimSpace(tool.Get("name").String()); name != "" {
|
||||
return name
|
||||
}
|
||||
return strings.TrimSpace(tool.Get("function.name").String())
|
||||
}
|
||||
|
||||
func responsesToolDescription(tool gjson.Result) string {
|
||||
if description := tool.Get("description").String(); description != "" {
|
||||
return description
|
||||
}
|
||||
return tool.Get("function.description").String()
|
||||
}
|
||||
|
||||
func responsesToolParameters(tool gjson.Result) gjson.Result {
|
||||
for _, path := range []string{
|
||||
"parameters",
|
||||
"parametersJsonSchema",
|
||||
"input_schema",
|
||||
"function.parameters",
|
||||
"function.parametersJsonSchema",
|
||||
} {
|
||||
if parameters := tool.Get(path); parameters.Exists() {
|
||||
return parameters
|
||||
}
|
||||
}
|
||||
return gjson.Result{}
|
||||
}
|
||||
|
||||
// CollectResponsesToolDescriptors extracts all tool descriptors from a Responses request root.
|
||||
func CollectResponsesToolDescriptors(root gjson.Result) []ResponsesToolDescriptor {
|
||||
var descriptors []ResponsesToolDescriptor
|
||||
appendDescriptor := func(tool gjson.Result, name, localName, namespace string, toolType string, sourcePriority int, direct 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),
|
||||
})
|
||||
}
|
||||
appendNamespaceChildren := func(namespaceTool gjson.Result, sourcePriority int) {
|
||||
namespaceName := strings.TrimSpace(namespaceTool.Get("name").String())
|
||||
children := namespaceTool.Get("tools")
|
||||
if !children.Exists() || !children.IsArray() {
|
||||
return
|
||||
}
|
||||
children.ForEach(func(_, child gjson.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)
|
||||
case "custom":
|
||||
appendDescriptor(child, qualifiedName, childName, namespaceName, "custom", sourcePriority, false)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
for _, source := range responsesToolSources(root) {
|
||||
source.tools.ForEach(func(_, tool gjson.Result) bool {
|
||||
toolType := strings.TrimSpace(tool.Get("type").String())
|
||||
switch toolType {
|
||||
case "", "function":
|
||||
name := responsesToolName(tool)
|
||||
appendDescriptor(tool, name, name, "", "function", source.priority, true)
|
||||
case "custom":
|
||||
name := responsesToolName(tool)
|
||||
appendDescriptor(tool, name, name, "", "custom", source.priority, true)
|
||||
case "namespace":
|
||||
appendNamespaceChildren(tool, source.priority)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
return descriptors
|
||||
}
|
||||
|
||||
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 gjson.Result) map[string]ResponsesToolDescriptor {
|
||||
winners := map[string]ResponsesToolDescriptor{}
|
||||
for _, descriptor := range CollectResponsesToolDescriptors(root) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BuildGeminiFunctionDeclarations builds Gemini function declarations, forward name mapping, and reverse identity mapping.
|
||||
func BuildGeminiFunctionDeclarations(root gjson.Result) ([][]byte, map[string]string, map[string]ResponsesToolIdentity) {
|
||||
descriptors := CollectResponsesToolDescriptors(root)
|
||||
winners := CollectResponsesToolWinners(root)
|
||||
|
||||
seenNames := make(map[string]struct{})
|
||||
var winningList []ResponsesToolDescriptor
|
||||
for _, descriptor := range descriptors {
|
||||
winner, ok := winners[descriptor.Name]
|
||||
if !ok || winner.Order != descriptor.Order {
|
||||
continue
|
||||
}
|
||||
if _, seen := seenNames[descriptor.Name]; seen {
|
||||
continue
|
||||
}
|
||||
seenNames[descriptor.Name] = struct{}{}
|
||||
winningList = append(winningList, descriptor)
|
||||
}
|
||||
|
||||
if len(winningList) == 0 {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
qualifiedNames := make([]string, 0, len(winningList))
|
||||
for _, desc := range winningList {
|
||||
qualifiedNames = append(qualifiedNames, desc.Name)
|
||||
}
|
||||
sanitizedMap := sanitizeResponsesToolNames(qualifiedNames)
|
||||
|
||||
forwardMap := make(map[string]string, len(winningList)*2)
|
||||
reverseMap := make(map[string]ResponsesToolIdentity, len(winningList)*2)
|
||||
var declarations [][]byte
|
||||
|
||||
for _, desc := range winningList {
|
||||
geminiName := desc.Name
|
||||
if mapped, ok := sanitizedMap[desc.Name]; ok && mapped != "" {
|
||||
geminiName = mapped
|
||||
} else {
|
||||
geminiName = SanitizeFunctionName(desc.Name)
|
||||
}
|
||||
|
||||
forwardMap[desc.Name] = geminiName
|
||||
if desc.LocalName != "" && desc.LocalName != desc.Name {
|
||||
if _, exists := forwardMap[desc.LocalName]; !exists {
|
||||
forwardMap[desc.LocalName] = geminiName
|
||||
}
|
||||
}
|
||||
|
||||
identity := ResponsesToolIdentity{
|
||||
Name: desc.LocalName,
|
||||
Namespace: desc.Namespace,
|
||||
Custom: desc.ToolType == "custom",
|
||||
}
|
||||
reverseMap[geminiName] = identity
|
||||
if desc.Name != geminiName {
|
||||
reverseMap[desc.Name] = identity
|
||||
}
|
||||
|
||||
funcDecl := []byte(`{"name":"","description":"","parametersJsonSchema":{}}`)
|
||||
funcDecl, _ = sjson.SetBytes(funcDecl, "name", geminiName)
|
||||
if descStr := responsesToolDescription(desc.Tool); descStr != "" {
|
||||
funcDecl, _ = sjson.SetBytes(funcDecl, "description", descStr)
|
||||
}
|
||||
|
||||
if desc.ToolType == "custom" {
|
||||
funcDecl, _ = sjson.SetRawBytes(funcDecl, "parametersJsonSchema", []byte(`{"type":"object","properties":{"input":{"type":"string"}},"required":["input"]}`))
|
||||
} else {
|
||||
params := responsesToolParameters(desc.Tool)
|
||||
if params.Exists() {
|
||||
funcDecl, _ = sjson.SetRawBytes(funcDecl, "parametersJsonSchema", []byte(CleanJSONSchemaForGemini(params.Raw)))
|
||||
}
|
||||
}
|
||||
declarations = append(declarations, funcDecl)
|
||||
}
|
||||
|
||||
return declarations, forwardMap, reverseMap
|
||||
}
|
||||
|
||||
// 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 || !gjson.ValidBytes(rawJSON) {
|
||||
return nil
|
||||
}
|
||||
root := gjson.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)
|
||||
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)
|
||||
}
|
||||
|
||||
// ConvertResponsesToolChoiceToGemini translates Responses tool_choice into Gemini functionCallingConfig JSON.
|
||||
func ConvertResponsesToolChoiceToGemini(toolChoice gjson.Result, forwardMap map[string]string) ([]byte, bool) {
|
||||
if !toolChoice.Exists() {
|
||||
return nil, false
|
||||
}
|
||||
mode := ""
|
||||
var allowedNames []string
|
||||
if toolChoice.Type == gjson.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 "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
|
||||
}
|
||||
cfg := []byte(`{"mode":""}`)
|
||||
cfg, _ = sjson.SetBytes(cfg, "mode", mode)
|
||||
if len(allowedNames) > 0 {
|
||||
cfg, _ = sjson.SetBytes(cfg, "allowedFunctionNames", allowedNames)
|
||||
}
|
||||
return cfg, 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 gjson.Valid(arguments) {
|
||||
parsed := gjson.Parse(arguments)
|
||||
if v := parsed.Get("input"); v.Exists() {
|
||||
if v.Type == gjson.String {
|
||||
return v.String()
|
||||
}
|
||||
return v.Raw
|
||||
}
|
||||
if parsed.Type == gjson.String {
|
||||
return parsed.String()
|
||||
}
|
||||
}
|
||||
return arguments
|
||||
}
|
||||
Loading…
Reference in a new issue