| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731 |
- package apicompat
- import (
- "encoding/json"
- "fmt"
- "strings"
- )
- // ResponsesToAnthropicRequest converts a Responses API request into an
- // Anthropic Messages request. This is the reverse of AnthropicToResponses and
- // enables Anthropic platform groups to accept OpenAI Responses API requests
- // by converting them to the native /v1/messages format before forwarding upstream.
- func ResponsesToAnthropicRequest(req *ResponsesRequest) (*AnthropicRequest, error) {
- system, messages, err := convertResponsesInputToAnthropic(req.Instructions, req.Input)
- if err != nil {
- return nil, err
- }
- out := &AnthropicRequest{
- Model: req.Model,
- Messages: messages,
- Temperature: req.Temperature,
- TopP: req.TopP,
- Stream: req.Stream,
- }
- if len(system) > 0 {
- out.System = system
- }
- // max_output_tokens → max_tokens
- if req.MaxOutputTokens != nil && *req.MaxOutputTokens > 0 {
- out.MaxTokens = *req.MaxOutputTokens
- }
- if out.MaxTokens == 0 {
- // Anthropic requires max_tokens; default to a sensible value.
- out.MaxTokens = 8192
- }
- // Convert tools
- if len(req.Tools) > 0 {
- out.Tools = convertResponsesToAnthropicTools(req.Tools)
- }
- // Convert tool_choice (reverse of convertAnthropicToolChoiceToResponses)
- if len(req.ToolChoice) > 0 {
- tc, err := convertResponsesToAnthropicToolChoice(req.ToolChoice)
- if err != nil {
- return nil, fmt.Errorf("convert tool_choice: %w", err)
- }
- out.ToolChoice = tc
- }
- // reasoning.effort → output_config.effort + thinking
- if req.Reasoning != nil && req.Reasoning.Effort != "" {
- effort := mapResponsesEffortToAnthropic(req.Reasoning.Effort)
- out.OutputConfig = &AnthropicOutputConfig{Effort: effort}
- // Enable thinking for non-low efforts
- if effort != "low" {
- out.Thinking = &AnthropicThinking{
- Type: "enabled",
- BudgetTokens: defaultThinkingBudget(effort),
- }
- }
- }
- return out, nil
- }
- // defaultThinkingBudget returns a sensible thinking budget based on effort level.
- func defaultThinkingBudget(effort string) int {
- switch effort {
- case "low":
- return 1024
- case "medium":
- return 4096
- case "high":
- return 10240
- case "max":
- return 32768
- default:
- return 10240
- }
- }
- // mapResponsesEffortToAnthropic converts OpenAI Responses reasoning effort to
- // Anthropic effort levels. Reverse of mapAnthropicEffortToResponses.
- //
- // low → low
- // medium → medium
- // high → high
- // xhigh → max
- func mapResponsesEffortToAnthropic(effort string) string {
- if effort == "xhigh" {
- return "max"
- }
- return effort // low→low, medium→medium, high→high, unknown→passthrough
- }
- // convertResponsesInputToAnthropic extracts system prompt and messages from
- // a Responses API instructions + input array. Returns the system as raw JSON
- // (for Anthropic's polymorphic system field) and a list of Anthropic messages.
- func convertResponsesInputToAnthropic(instructions string, inputRaw json.RawMessage) (json.RawMessage, []AnthropicMessage, error) {
- var systemParts []string
- if strings.TrimSpace(instructions) != "" {
- systemParts = append(systemParts, strings.TrimSpace(instructions))
- }
- // Try as plain string input.
- var inputStr string
- if err := json.Unmarshal(inputRaw, &inputStr); err == nil {
- content, _ := json.Marshal(inputStr)
- var system json.RawMessage
- if len(systemParts) > 0 {
- system, _ = json.Marshal(strings.Join(systemParts, "\n\n"))
- }
- return system, []AnthropicMessage{{Role: "user", Content: content}}, nil
- }
- var items []ResponsesInputItem
- if err := json.Unmarshal(inputRaw, &items); err != nil {
- return nil, nil, fmt.Errorf("parse responses input: %w", err)
- }
- var messages []AnthropicMessage
- for _, item := range items {
- switch {
- case item.Role == "system" || item.Role == "developer":
- text := extractTextFromContent(item.Content)
- if text != "" {
- systemParts = append(systemParts, text)
- }
- case item.Type == "function_call":
- // function_call → assistant message with tool_use block
- input := json.RawMessage("{}")
- if item.Arguments != "" {
- input = json.RawMessage(item.Arguments)
- }
- block := AnthropicContentBlock{
- Type: "tool_use",
- ID: fromResponsesCallIDToAnthropic(item.CallID),
- Name: item.Name,
- Input: input,
- }
- blockJSON, _ := json.Marshal([]AnthropicContentBlock{block})
- messages = append(messages, AnthropicMessage{
- Role: "assistant",
- Content: blockJSON,
- })
- case item.Type == "function_call_output":
- // function_call_output → user message with tool_result block
- contentJSON := responsesFunctionOutputToAnthropicContent(item)
- block := AnthropicContentBlock{
- Type: "tool_result",
- ToolUseID: fromResponsesCallIDToAnthropic(item.CallID),
- Content: contentJSON,
- }
- blockJSON, _ := json.Marshal([]AnthropicContentBlock{block})
- messages = append(messages, AnthropicMessage{
- Role: "user",
- Content: blockJSON,
- })
- case item.Type == "reasoning":
- // Anthropic 无法摄入 OpenAI 的 reasoning:encrypted_content 是不透明的,
- // 而 thinking 块的重放需要 Anthropic 自己签发的 signature,无法伪造。
- // Codex 常见形态(只带 summary + encrypted_content)本来就会被丢弃,
- // 这里让带 content 数组的形态保持同样行为——否则 reasoning_text 块会被
- // 原样塞进 Anthropic 请求体,上游直接回 400。
- case item.Role == "user":
- content, err := convertResponsesUserToAnthropicContent(item.Content)
- if err != nil {
- return nil, nil, err
- }
- // 内容里只有网关不认识的分片时,sanitize 会得到空串。Anthropic 拒收
- // 空内容消息("all messages must have non-empty content"),整条丢掉
- // 比发一条必然 400 的消息更可用。
- if anthropicContentIsEmpty(content) {
- continue
- }
- messages = append(messages, AnthropicMessage{
- Role: "user",
- Content: content,
- })
- case item.Role == "assistant":
- content, err := convertResponsesAssistantToAnthropicContent(item.Content)
- if err != nil {
- return nil, nil, err
- }
- // 同上:分片全不认识时会退化成单个空 text 块,而 Anthropic 拒收
- // 空文本块("text content blocks must contain non-whitespace text")。
- if anthropicContentIsEmpty(content) || anthropicContentIsOnlyBlankText(content) {
- continue
- }
- messages = append(messages, AnthropicMessage{
- Role: "assistant",
- Content: content,
- })
- default:
- // 未知 role/type —— 尽量当作 user 消息保留其中的文本/图片。
- // 必须走与真实 user 消息同一套白名单转换:直接透传 item.Content 会把
- // Responses 专有的分片类型(reasoning_text、web_search_call 的载荷等)
- // 原样发给 Anthropic,上游只会回 400 把整轮打挂。
- if item.Content == nil {
- continue
- }
- content, err := convertResponsesUserToAnthropicContent(item.Content)
- if err != nil {
- return nil, nil, err
- }
- if anthropicContentIsEmpty(content) {
- continue
- }
- messages = append(messages, AnthropicMessage{
- Role: "user",
- Content: content,
- })
- }
- }
- // Repair tool_use/tool_result pairing, then merge consecutive same-role
- // messages (Anthropic requires alternating roles). The first merge groups
- // parallel calls (and their results) so the pairing pass sees them together;
- // the pairing pass may re-split a user turn (e.g. when an injected message
- // sat between a call and its output), so a second merge restores alternation.
- messages = mergeConsecutiveMessages(messages)
- messages = normalizeAnthropicToolPairing(messages)
- messages = mergeConsecutiveMessages(messages)
- var system json.RawMessage
- if len(systemParts) > 0 {
- system, _ = json.Marshal(strings.Join(systemParts, "\n\n"))
- }
- return system, messages, nil
- }
- func responsesFunctionOutputToAnthropicContent(item ResponsesInputItem) json.RawMessage {
- if len(item.outputRaw) == 0 {
- output := item.Output
- if output == "" {
- output = "(empty)"
- }
- content, _ := json.Marshal(output)
- return content
- }
- var parts []ResponsesContentPart
- if err := json.Unmarshal(item.outputRaw, &parts); err == nil {
- blocks := make([]AnthropicContentBlock, 0, len(parts))
- for _, part := range parts {
- switch part.Type {
- case "input_text", "output_text", "text":
- if part.Text != "" {
- blocks = append(blocks, AnthropicContentBlock{Type: "text", Text: part.Text})
- }
- case "input_image":
- if source := dataURIToAnthropicImageSource(part.ImageURL); source != nil {
- blocks = append(blocks, AnthropicContentBlock{Type: "image", Source: source})
- }
- }
- }
- if len(blocks) > 0 {
- content, _ := json.Marshal(blocks)
- return content
- }
- if len(parts) == 0 {
- content, _ := json.Marshal("(empty)")
- return content
- }
- }
- content, _ := json.Marshal(item.Output)
- return content
- }
- // normalizeAnthropicToolPairing rebuilds the message sequence so it satisfies
- // Anthropic's tool_use/tool_result invariants, which the naive item-by-item
- // conversion violates whenever the Responses history interleaves anything
- // between a function_call and its function_call_output:
- //
- // - every tool_result block must have a matching tool_use in the immediately
- // preceding assistant message ("tool_result ... must have a corresponding
- // tool_use block in the previous message");
- // - every tool_use block must be answered by a tool_result in the immediately
- // following user message (Anthropic rejects unanswered tool_use ids);
- // - user/assistant turns must alternate.
- //
- // codex (Responses, store:false) re-sends the whole history each turn and
- // frequently injects items between a call and its output — a developer/approval
- // notice, or a sibling parallel call whose output never arrived. The unrepaired
- // converter emits each function_call as its own assistant message and each
- // output as its own user message, so any such interleaving breaks
- // tool_use↔tool_result adjacency and yields an upstream 400.
- //
- // The repair indexes every tool_result by its tool_use id, then for each
- // assistant message carrying tool_use blocks keeps only the answered ones
- // (dropping unanswered/dangling calls — and the assistant message entirely if it
- // has no other content) and emits the matching tool_result blocks, in call
- // order, as the very next user message. Standalone tool_result blocks are
- // dropped from their original position (re-emitted adjacent to their call);
- // orphan tool_results with no announcing tool_use are dropped. Non-tool content
- // passes through in place. This mirrors normalizeChatMessages on the
- // Responses→Chat path.
- func normalizeAnthropicToolPairing(messages []AnthropicMessage) []AnthropicMessage {
- // Index every tool_result block by its tool_use id (last wins on dup).
- results := make(map[string]AnthropicContentBlock)
- for _, m := range messages {
- if m.Role != "user" {
- continue
- }
- for _, b := range parseContentBlocks(m.Content) {
- if b.Type == "tool_result" && b.ToolUseID != "" {
- results[b.ToolUseID] = b
- }
- }
- }
- out := make([]AnthropicMessage, 0, len(messages))
- for _, m := range messages {
- blocks := parseContentBlocks(m.Content)
- switch m.Role {
- case "assistant":
- var toolUses, others []AnthropicContentBlock
- for _, b := range blocks {
- if b.Type == "tool_use" {
- toolUses = append(toolUses, b)
- } else {
- others = append(others, b)
- }
- }
- if len(toolUses) == 0 {
- out = append(out, m)
- continue
- }
- kept := make([]AnthropicContentBlock, 0, len(toolUses))
- for _, tu := range toolUses {
- if _, ok := results[tu.ID]; ok {
- kept = append(kept, tu)
- }
- }
- if len(kept) == 0 {
- // No answered calls: keep any non-tool content, else drop.
- if len(others) > 0 {
- out = append(out, anthropicMessageFromBlocks("assistant", others))
- }
- continue
- }
- asstBlocks := make([]AnthropicContentBlock, 0, len(others)+len(kept))
- asstBlocks = append(asstBlocks, others...)
- asstBlocks = append(asstBlocks, kept...)
- out = append(out, anthropicMessageFromBlocks("assistant", asstBlocks))
- resBlocks := make([]AnthropicContentBlock, 0, len(kept))
- for _, tu := range kept {
- resBlocks = append(resBlocks, results[tu.ID])
- }
- out = append(out, anthropicMessageFromBlocks("user", resBlocks))
- case "user":
- var nonResult []AnthropicContentBlock
- hasResult := false
- for _, b := range blocks {
- if b.Type == "tool_result" {
- hasResult = true
- continue
- }
- nonResult = append(nonResult, b)
- }
- if !hasResult {
- out = append(out, m)
- continue
- }
- // The tool_result blocks are re-emitted next to their call; keep any
- // other content of this user turn in place, drop it if there is none.
- if len(nonResult) > 0 {
- out = append(out, anthropicMessageFromBlocks("user", nonResult))
- }
- default:
- out = append(out, m)
- }
- }
- return out
- }
- // anthropicMessageFromBlocks builds an AnthropicMessage whose content is the
- // marshaled block array.
- func anthropicMessageFromBlocks(role string, blocks []AnthropicContentBlock) AnthropicMessage {
- content, _ := json.Marshal(blocks)
- return AnthropicMessage{Role: role, Content: content}
- }
- // extractTextFromContent extracts text from a content field that may be a
- // plain string or an array of content parts.
- func extractTextFromContent(raw json.RawMessage) string {
- if len(raw) == 0 {
- return ""
- }
- var s string
- if err := json.Unmarshal(raw, &s); err == nil {
- return s
- }
- var parts []ResponsesContentPart
- if err := json.Unmarshal(raw, &parts); err == nil {
- var texts []string
- for _, p := range parts {
- if (p.Type == "input_text" || p.Type == "output_text" || p.Type == "text") && p.Text != "" {
- texts = append(texts, p.Text)
- }
- }
- return strings.Join(texts, "\n\n")
- }
- return ""
- }
- // convertResponsesUserToAnthropicContent converts a Responses user message
- // content field into Anthropic content blocks JSON.
- // anthropicContentIsEmpty 判断转换结果是否为"空内容"。
- // convertResponsesUserToAnthropicContent 在没有任何可识别分片时返回 JSON 空串,
- // 而 Anthropic 拒收空内容消息。
- func anthropicContentIsEmpty(content json.RawMessage) bool {
- trimmed := strings.TrimSpace(string(content))
- switch trimmed {
- case "", "null", `""`, "[]":
- return true
- }
- return false
- }
- // anthropicContentIsOnlyBlankText 判断内容是否只由空白 text 块组成。
- func anthropicContentIsOnlyBlankText(content json.RawMessage) bool {
- blocks := parseContentBlocks(content)
- if len(blocks) == 0 {
- return false
- }
- for _, b := range blocks {
- if b.Type != "text" || strings.TrimSpace(b.Text) != "" {
- return false
- }
- }
- return true
- }
- func convertResponsesUserToAnthropicContent(raw json.RawMessage) (json.RawMessage, error) {
- if len(raw) == 0 {
- return json.Marshal("") // empty string content
- }
- // Try plain string.
- var s string
- if err := json.Unmarshal(raw, &s); err == nil {
- return json.Marshal(s)
- }
- // Array of content parts → Anthropic content blocks.
- var parts []ResponsesContentPart
- if err := json.Unmarshal(raw, &parts); err != nil {
- // Pass through as-is if we can't parse
- return raw, nil
- }
- var blocks []AnthropicContentBlock
- for _, p := range parts {
- switch p.Type {
- case "input_text", "text":
- if p.Text != "" {
- blocks = append(blocks, AnthropicContentBlock{
- Type: "text",
- Text: p.Text,
- })
- }
- case "input_image":
- src := dataURIToAnthropicImageSource(p.ImageURL)
- if src != nil {
- blocks = append(blocks, AnthropicContentBlock{
- Type: "image",
- Source: src,
- })
- }
- }
- }
- if len(blocks) == 0 {
- return json.Marshal("")
- }
- return json.Marshal(blocks)
- }
- // convertResponsesAssistantToAnthropicContent converts a Responses assistant
- // message content field into Anthropic content blocks JSON.
- func convertResponsesAssistantToAnthropicContent(raw json.RawMessage) (json.RawMessage, error) {
- if len(raw) == 0 {
- return json.Marshal([]AnthropicContentBlock{{Type: "text", Text: ""}})
- }
- // Try plain string.
- var s string
- if err := json.Unmarshal(raw, &s); err == nil {
- return json.Marshal([]AnthropicContentBlock{{Type: "text", Text: s}})
- }
- // Array of content parts → Anthropic content blocks.
- var parts []ResponsesContentPart
- if err := json.Unmarshal(raw, &parts); err != nil {
- return raw, nil
- }
- var blocks []AnthropicContentBlock
- for _, p := range parts {
- switch p.Type {
- case "output_text", "text":
- if p.Text != "" {
- blocks = append(blocks, AnthropicContentBlock{
- Type: "text",
- Text: p.Text,
- })
- }
- }
- }
- if len(blocks) == 0 {
- blocks = append(blocks, AnthropicContentBlock{Type: "text", Text: ""})
- }
- return json.Marshal(blocks)
- }
- // fromResponsesCallIDToAnthropic converts an OpenAI function call ID back to
- // Anthropic format. Reverses toResponsesCallID.
- func fromResponsesCallIDToAnthropic(id string) string {
- // If it has our "fc_" prefix wrapping a known Anthropic prefix, strip it
- if after, ok := strings.CutPrefix(id, "fc_"); ok {
- if strings.HasPrefix(after, "toolu_") || strings.HasPrefix(after, "call_") {
- return after
- }
- }
- // Generate a synthetic Anthropic tool ID
- if !strings.HasPrefix(id, "toolu_") && !strings.HasPrefix(id, "call_") {
- return "toolu_" + id
- }
- return id
- }
- // dataURIToAnthropicImageSource parses a data URI into an AnthropicImageSource.
- func dataURIToAnthropicImageSource(dataURI string) *AnthropicImageSource {
- if !strings.HasPrefix(dataURI, "data:") {
- return nil
- }
- // Format: data:<media_type>;base64,<data>
- rest := strings.TrimPrefix(dataURI, "data:")
- semicolonIdx := strings.Index(rest, ";")
- if semicolonIdx < 0 {
- return nil
- }
- mediaType := rest[:semicolonIdx]
- rest = rest[semicolonIdx+1:]
- if !strings.HasPrefix(rest, "base64,") {
- return nil
- }
- data := strings.TrimPrefix(rest, "base64,")
- return &AnthropicImageSource{
- Type: "base64",
- MediaType: mediaType,
- Data: data,
- }
- }
- // mergeConsecutiveMessages merges consecutive messages with the same role
- // because Anthropic requires alternating user/assistant turns.
- func mergeConsecutiveMessages(messages []AnthropicMessage) []AnthropicMessage {
- if len(messages) <= 1 {
- return messages
- }
- var merged []AnthropicMessage
- for _, msg := range messages {
- if len(merged) == 0 || merged[len(merged)-1].Role != msg.Role {
- merged = append(merged, msg)
- continue
- }
- // Same role — merge content arrays
- last := &merged[len(merged)-1]
- lastBlocks := parseContentBlocks(last.Content)
- newBlocks := parseContentBlocks(msg.Content)
- combined := append(lastBlocks, newBlocks...)
- last.Content, _ = json.Marshal(combined)
- }
- return merged
- }
- // parseContentBlocks attempts to parse content as []AnthropicContentBlock.
- // If it's a string, wraps it in a text block.
- func parseContentBlocks(raw json.RawMessage) []AnthropicContentBlock {
- var blocks []AnthropicContentBlock
- if err := json.Unmarshal(raw, &blocks); err == nil {
- return blocks
- }
- var s string
- if err := json.Unmarshal(raw, &s); err == nil {
- return []AnthropicContentBlock{{Type: "text", Text: s}}
- }
- return nil
- }
- // convertResponsesToAnthropicTools maps Responses API tools to Anthropic format.
- // Reverse of convertAnthropicToolsToResponses.
- func convertResponsesToAnthropicTools(tools []ResponsesTool) []AnthropicTool {
- var out []AnthropicTool
- for _, t := range tools {
- switch t.Type {
- case "web_search", "google_search", "web_search_20250305":
- out = append(out, AnthropicTool{
- Type: "web_search_20250305",
- Name: "web_search",
- })
- case "function":
- out = append(out, AnthropicTool{
- Name: t.Name,
- Description: t.Description,
- InputSchema: normalizeAnthropicInputSchema(t.Parameters),
- })
- case "custom":
- out = append(out, AnthropicTool{
- Name: t.Name,
- Description: t.Description,
- InputSchema: normalizeAnthropicInputSchema(t.Parameters),
- })
- default:
- // Pass through unknown tool types
- out = append(out, AnthropicTool{
- Type: t.Type,
- Name: t.Name,
- Description: t.Description,
- InputSchema: normalizeAnthropicInputSchema(t.Parameters),
- })
- }
- }
- return out
- }
- // normalizeAnthropicInputSchema ensures input_schema is a valid object schema.
- func normalizeAnthropicInputSchema(schema json.RawMessage) json.RawMessage {
- const emptyObjectSchema = `{"type":"object","properties":{}}`
- trimmed := strings.TrimSpace(string(schema))
- if trimmed == "" || trimmed == "null" {
- return json.RawMessage(emptyObjectSchema)
- }
- var m map[string]json.RawMessage
- if err := json.Unmarshal(schema, &m); err != nil {
- return json.RawMessage(`{"type":"object","properties":{}}`)
- }
- typeRaw, ok := m["type"]
- if !ok || strings.TrimSpace(string(typeRaw)) == "" || string(typeRaw) == "null" {
- m["type"] = json.RawMessage(`"object"`)
- } else {
- var typ string
- if err := json.Unmarshal(typeRaw, &typ); err != nil || typ != "object" {
- return json.RawMessage(emptyObjectSchema)
- }
- }
- if _, ok := m["properties"]; !ok {
- m["properties"] = json.RawMessage(`{}`)
- }
- out, err := json.Marshal(m)
- if err != nil {
- return json.RawMessage(emptyObjectSchema)
- }
- return out
- }
- // convertResponsesToAnthropicToolChoice maps Responses tool_choice to Anthropic format.
- // Reverse of convertAnthropicToolChoiceToResponses.
- //
- // "auto" → {"type":"auto"}
- // "required" → {"type":"any"}
- // "none" → {"type":"none"}
- // {"type":"function","name":"X"} → {"type":"tool","name":"X"}
- // {"type":"function","function":{"name":"X"}} → {"type":"tool","name":"X"} // legacy
- func convertResponsesToAnthropicToolChoice(raw json.RawMessage) (json.RawMessage, error) {
- // Try as string first
- var s string
- if err := json.Unmarshal(raw, &s); err == nil {
- switch s {
- case "auto":
- return json.Marshal(map[string]string{"type": "auto"})
- case "required":
- return json.Marshal(map[string]string{"type": "any"})
- case "none":
- return json.Marshal(map[string]string{"type": "none"})
- default:
- return raw, nil
- }
- }
- // Try as object with type=function
- var tc struct {
- Type string `json:"type"`
- Name string `json:"name"`
- Function struct {
- Name string `json:"name"`
- } `json:"function"`
- }
- if err := json.Unmarshal(raw, &tc); err == nil && tc.Type == "function" {
- name := strings.TrimSpace(tc.Name)
- if name == "" {
- name = strings.TrimSpace(tc.Function.Name)
- }
- if name == "" {
- return raw, nil
- }
- return json.Marshal(map[string]string{
- "type": "tool",
- "name": name,
- })
- }
- // Pass through unknown
- return raw, nil
- }
|