Honor deferred tools across providers
- commit
- 8f1edc1c52de390fd507e6350c55febbd160b6bf
- author
- patriceckhart · · 2026-07-21 16:32 UTC
- parent
- dfc299e
- signature
- unsigned
7 files · +123 -31
M packages/provider/amazon_bedrock.go +3 -2
| @@ -411,11 +411,12 @@ func (c *bedrockClient) buildRequest(req Request) (*bedrockRequest, error) { | |||
| 411 | 411 | } | |
| 412 | 412 | out.Messages = append(out.Messages, bm) | |
| 413 | 413 | } | |
| 414 | - | if len(req.Tools) > 0 { | |
| 414 | + | activeTools := activeToolDefinitions(req.Tools, req.Messages) | |
| 415 | + | if len(activeTools) > 0 { | |
| 415 | 416 | tc := struct { | |
| 416 | 417 | Tools []bedrockToolSpec `json:"tools"` | |
| 417 | 418 | }{} | |
| 418 | - | for _, t := range req.Tools { | |
| 419 | + | for _, t := range activeTools { | |
| 419 | 420 | var ts bedrockToolSpec | |
| 420 | 421 | ts.ToolSpec.Name = t.Name | |
| 421 | 422 | ts.ToolSpec.Description = t.Description | |
| @@ -411,11 +411,12 @@ func (c *bedrockClient) buildRequest(req Request) (*bedrockRequest, error) { | |||||
| 411 | } | 411 | } | ||
| 412 | out.Messages = append(out.Messages, bm) | 412 | out.Messages = append(out.Messages, bm) | ||
| 413 | } | 413 | } | ||
| 414 | - | if len(req.Tools) > 0 { | 414 | + | activeTools := activeToolDefinitions(req.Tools, req.Messages) |
| 415 | + | if len(activeTools) > 0 { | |||
| 415 | tc := struct { | 416 | tc := struct { | ||
| 416 | Tools []bedrockToolSpec `json:"tools"` | 417 | Tools []bedrockToolSpec `json:"tools"` | ||
| 417 | }{} | 418 | }{} | ||
| 418 | - | for _, t := range req.Tools { | 419 | + | for _, t := range activeTools { |
| 419 | var ts bedrockToolSpec | 420 | var ts bedrockToolSpec | ||
| 420 | ts.ToolSpec.Name = t.Name | 421 | ts.ToolSpec.Name = t.Name | ||
| 421 | ts.ToolSpec.Description = t.Description | 422 | ts.ToolSpec.Description = t.Description | ||
M packages/provider/anthropic.go +1 -1
| @@ -312,7 +312,7 @@ func (c *anthropicClient) buildRequest(req Request) (*anthRequest, error) { | |||
| 312 | 312 | } | |
| 313 | 313 | } | |
| 314 | 314 | ||
| 315 | - | for _, t := range req.Tools { | |
| 315 | + | for _, t := range activeToolDefinitions(req.Tools, req.Messages) { | |
| 316 | 316 | name := t.Name | |
| 317 | 317 | if c.oauthTok != "" { | |
| 318 | 318 | name = toClaudeCodeToolName(name) | |
| @@ -312,7 +312,7 @@ func (c *anthropicClient) buildRequest(req Request) (*anthRequest, error) { | |||||
| 312 | } | 312 | } | ||
| 313 | } | 313 | } | ||
| 314 | 314 | ||||
| 315 | - | for _, t := range req.Tools { | 315 | + | for _, t := range activeToolDefinitions(req.Tools, req.Messages) { |
| 316 | name := t.Name | 316 | name := t.Name | ||
| 317 | if c.oauthTok != "" { | 317 | if c.oauthTok != "" { | ||
| 318 | name = toClaudeCodeToolName(name) | 318 | name = toClaudeCodeToolName(name) | ||
M packages/provider/deferred_tools_test.go +87 -17
| @@ -38,29 +38,99 @@ func TestKimiK3DeferredToolsLoadAtToolResult(t *testing.T) { | |||
| 38 | 38 | } | |
| 39 | 39 | ||
| 40 | 40 | func TestDeferredToolsFallbackToActiveTopLevelDefinitions(t *testing.T) { | |
| 41 | - | client := NewOpenAI("token", "").(*openaiClient) | |
| 42 | 41 | tools := []Tool{ | |
| 43 | 42 | {Name: "base", Schema: json.RawMessage(`{"type":"object"}`)}, | |
| 44 | 43 | {Name: "late", Schema: json.RawMessage(`{"type":"object"}`), Deferred: true}, | |
| 45 | 44 | } | |
| 46 | - | wire, err := client.buildRequest(Request{Model: "gpt-5", Tools: tools}) | |
| 47 | - | if err != nil { | |
| 48 | - | t.Fatal(err) | |
| 49 | - | } | |
| 50 | - | if len(wire.Tools) != 1 || wire.Tools[0].Function.Name != "base" { | |
| 51 | - | t.Fatalf("initial tools = %+v", wire.Tools) | |
| 45 | + | builders := map[string]func(Request) ([]string, error){ | |
| 46 | + | "openai": func(req Request) ([]string, error) { | |
| 47 | + | wire, err := NewOpenAI("token", "").(*openaiClient).buildRequest(req) | |
| 48 | + | if err != nil { | |
| 49 | + | return nil, err | |
| 50 | + | } | |
| 51 | + | names := make([]string, 0, len(wire.Tools)) | |
| 52 | + | for _, tool := range wire.Tools { | |
| 53 | + | names = append(names, tool.Function.Name) | |
| 54 | + | } | |
| 55 | + | return names, nil | |
| 56 | + | }, | |
| 57 | + | "anthropic": func(req Request) ([]string, error) { | |
| 58 | + | req.Model = "claude-sonnet-4-6" | |
| 59 | + | wire, err := NewAnthropic("token", "").(*anthropicClient).buildRequest(req) | |
| 60 | + | if err != nil { | |
| 61 | + | return nil, err | |
| 62 | + | } | |
| 63 | + | names := make([]string, 0, len(wire.Tools)) | |
| 64 | + | for _, tool := range wire.Tools { | |
| 65 | + | names = append(names, tool.Name) | |
| 66 | + | } | |
| 67 | + | return names, nil | |
| 68 | + | }, | |
| 69 | + | "bedrock": func(req Request) ([]string, error) { | |
| 70 | + | req.Model = "anthropic.claude-sonnet-4-5-20250929-v1:0" | |
| 71 | + | wire, err := (&bedrockClient{region: "us-east-1"}).buildRequest(req) | |
| 72 | + | if err != nil { | |
| 73 | + | return nil, err | |
| 74 | + | } | |
| 75 | + | if wire.ToolConfig == nil { | |
| 76 | + | return nil, nil | |
| 77 | + | } | |
| 78 | + | names := make([]string, 0, len(wire.ToolConfig.Tools)) | |
| 79 | + | for _, tool := range wire.ToolConfig.Tools { | |
| 80 | + | names = append(names, tool.ToolSpec.Name) | |
| 81 | + | } | |
| 82 | + | return names, nil | |
| 83 | + | }, | |
| 84 | + | "gemini": func(req Request) ([]string, error) { | |
| 85 | + | req.Model = "gemini-2.5-pro" | |
| 86 | + | wire, _, err := NewGemini("token", "").(*geminiClient).buildRequest(req) | |
| 87 | + | if err != nil { | |
| 88 | + | return nil, err | |
| 89 | + | } | |
| 90 | + | var names []string | |
| 91 | + | for _, group := range wire.Tools { | |
| 92 | + | for _, tool := range group.FunctionDeclarations { | |
| 93 | + | names = append(names, tool.Name) | |
| 94 | + | } | |
| 95 | + | } | |
| 96 | + | return names, nil | |
| 97 | + | }, | |
| 98 | + | "openai-codex": func(req Request) ([]string, error) { | |
| 99 | + | req.Model = "gpt-5.5" | |
| 100 | + | wire, err := NewOpenAICodex("token", "account", "").(*codexClient).buildRequest(req) | |
| 101 | + | if err != nil { | |
| 102 | + | return nil, err | |
| 103 | + | } | |
| 104 | + | names := make([]string, 0, len(wire.Tools)) | |
| 105 | + | for _, tool := range wire.Tools { | |
| 106 | + | names = append(names, tool.Name) | |
| 107 | + | } | |
| 108 | + | return names, nil | |
| 109 | + | }, | |
| 52 | 110 | } | |
| 53 | 111 | ||
| 54 | - | wire, err = client.buildRequest(Request{ | |
| 55 | - | Model: "gpt-5", | |
| 56 | - | Tools: tools, | |
| 57 | - | Messages: []Message{{Role: RoleTool, AddedToolNames: []string{"late"}}}, | |
| 58 | - | }) | |
| 59 | - | if err != nil { | |
| 60 | - | t.Fatal(err) | |
| 61 | - | } | |
| 62 | - | if len(wire.Tools) != 2 { | |
| 63 | - | t.Fatalf("activated tools = %+v, want both definitions", wire.Tools) | |
| 112 | + | for name, build := range builders { | |
| 113 | + | t.Run(name, func(t *testing.T) { | |
| 114 | + | initial, err := build(Request{Model: "gpt-5", Tools: tools}) | |
| 115 | + | if err != nil { | |
| 116 | + | t.Fatal(err) | |
| 117 | + | } | |
| 118 | + | if len(initial) != 1 || initial[0] != "base" { | |
| 119 | + | t.Fatalf("initial tools = %v, want [base]", initial) | |
| 120 | + | } | |
| 121 | + | ||
| 122 | + | activated, err := build(Request{ | |
| 123 | + | Model: "gpt-5", | |
| 124 | + | Tools: tools, | |
| 125 | + | Messages: []Message{{Role: RoleTool, AddedToolNames: []string{"late"}}}, | |
| 126 | + | }) | |
| 127 | + | if err != nil { | |
| 128 | + | t.Fatal(err) | |
| 129 | + | } | |
| 130 | + | if len(activated) != 2 || activated[0] != "base" || activated[1] != "late" { | |
| 131 | + | t.Fatalf("activated tools = %v, want [base late]", activated) | |
| 132 | + | } | |
| 133 | + | }) | |
| 64 | 134 | } | |
| 65 | 135 | } | |
| 66 | 136 | ||
| @@ -38,29 +38,99 @@ func TestKimiK3DeferredToolsLoadAtToolResult(t *testing.T) { | |||||
| 38 | } | 38 | } | ||
| 39 | 39 | ||||
| 40 | func TestDeferredToolsFallbackToActiveTopLevelDefinitions(t *testing.T) { | 40 | func TestDeferredToolsFallbackToActiveTopLevelDefinitions(t *testing.T) { | ||
| 41 | - | client := NewOpenAI("token", "").(*openaiClient) | |||
| 42 | tools := []Tool{ | 41 | tools := []Tool{ | ||
| 43 | {Name: "base", Schema: json.RawMessage(`{"type":"object"}`)}, | 42 | {Name: "base", Schema: json.RawMessage(`{"type":"object"}`)}, | ||
| 44 | {Name: "late", Schema: json.RawMessage(`{"type":"object"}`), Deferred: true}, | 43 | {Name: "late", Schema: json.RawMessage(`{"type":"object"}`), Deferred: true}, | ||
| 45 | } | 44 | } | ||
| 46 | - | wire, err := client.buildRequest(Request{Model: "gpt-5", Tools: tools}) | 45 | + | builders := map[string]func(Request) ([]string, error){ |
| 47 | - | if err != nil { | 46 | + | "openai": func(req Request) ([]string, error) { |
| 48 | - | t.Fatal(err) | 47 | + | wire, err := NewOpenAI("token", "").(*openaiClient).buildRequest(req) |
| 49 | - | } | 48 | + | if err != nil { |
| 50 | - | if len(wire.Tools) != 1 || wire.Tools[0].Function.Name != "base" { | 49 | + | return nil, err |
| 51 | - | t.Fatalf("initial tools = %+v", wire.Tools) | 50 | + | } |
| 51 | + | names := make([]string, 0, len(wire.Tools)) | |||
| 52 | + | for _, tool := range wire.Tools { | |||
| 53 | + | names = append(names, tool.Function.Name) | |||
| 54 | + | } | |||
| 55 | + | return names, nil | |||
| 56 | + | }, | |||
| 57 | + | "anthropic": func(req Request) ([]string, error) { | |||
| 58 | + | req.Model = "claude-sonnet-4-6" | |||
| 59 | + | wire, err := NewAnthropic("token", "").(*anthropicClient).buildRequest(req) | |||
| 60 | + | if err != nil { | |||
| 61 | + | return nil, err | |||
| 62 | + | } | |||
| 63 | + | names := make([]string, 0, len(wire.Tools)) | |||
| 64 | + | for _, tool := range wire.Tools { | |||
| 65 | + | names = append(names, tool.Name) | |||
| 66 | + | } | |||
| 67 | + | return names, nil | |||
| 68 | + | }, | |||
| 69 | + | "bedrock": func(req Request) ([]string, error) { | |||
| 70 | + | req.Model = "anthropic.claude-sonnet-4-5-20250929-v1:0" | |||
| 71 | + | wire, err := (&bedrockClient{region: "us-east-1"}).buildRequest(req) | |||
| 72 | + | if err != nil { | |||
| 73 | + | return nil, err | |||
| 74 | + | } | |||
| 75 | + | if wire.ToolConfig == nil { | |||
| 76 | + | return nil, nil | |||
| 77 | + | } | |||
| 78 | + | names := make([]string, 0, len(wire.ToolConfig.Tools)) | |||
| 79 | + | for _, tool := range wire.ToolConfig.Tools { | |||
| 80 | + | names = append(names, tool.ToolSpec.Name) | |||
| 81 | + | } | |||
| 82 | + | return names, nil | |||
| 83 | + | }, | |||
| 84 | + | "gemini": func(req Request) ([]string, error) { | |||
| 85 | + | req.Model = "gemini-2.5-pro" | |||
| 86 | + | wire, _, err := NewGemini("token", "").(*geminiClient).buildRequest(req) | |||
| 87 | + | if err != nil { | |||
| 88 | + | return nil, err | |||
| 89 | + | } | |||
| 90 | + | var names []string | |||
| 91 | + | for _, group := range wire.Tools { | |||
| 92 | + | for _, tool := range group.FunctionDeclarations { | |||
| 93 | + | names = append(names, tool.Name) | |||
| 94 | + | } | |||
| 95 | + | } | |||
| 96 | + | return names, nil | |||
| 97 | + | }, | |||
| 98 | + | "openai-codex": func(req Request) ([]string, error) { | |||
| 99 | + | req.Model = "gpt-5.5" | |||
| 100 | + | wire, err := NewOpenAICodex("token", "account", "").(*codexClient).buildRequest(req) | |||
| 101 | + | if err != nil { | |||
| 102 | + | return nil, err | |||
| 103 | + | } | |||
| 104 | + | names := make([]string, 0, len(wire.Tools)) | |||
| 105 | + | for _, tool := range wire.Tools { | |||
| 106 | + | names = append(names, tool.Name) | |||
| 107 | + | } | |||
| 108 | + | return names, nil | |||
| 109 | + | }, | |||
| 52 | } | 110 | } | ||
| 53 | 111 | ||||
| 54 | - | wire, err = client.buildRequest(Request{ | 112 | + | for name, build := range builders { |
| 55 | - | Model: "gpt-5", | 113 | + | t.Run(name, func(t *testing.T) { |
| 56 | - | Tools: tools, | 114 | + | initial, err := build(Request{Model: "gpt-5", Tools: tools}) |
| 57 | - | Messages: []Message{{Role: RoleTool, AddedToolNames: []string{"late"}}}, | 115 | + | if err != nil { |
| 58 | - | }) | 116 | + | t.Fatal(err) |
| 59 | - | if err != nil { | 117 | + | } |
| 60 | - | t.Fatal(err) | 118 | + | if len(initial) != 1 || initial[0] != "base" { |
| 61 | - | } | 119 | + | t.Fatalf("initial tools = %v, want [base]", initial) |
| 62 | - | if len(wire.Tools) != 2 { | 120 | + | } |
| 63 | - | t.Fatalf("activated tools = %+v, want both definitions", wire.Tools) | 121 | + | |
| 122 | + | activated, err := build(Request{ | |||
| 123 | + | Model: "gpt-5", | |||
| 124 | + | Tools: tools, | |||
| 125 | + | Messages: []Message{{Role: RoleTool, AddedToolNames: []string{"late"}}}, | |||
| 126 | + | }) | |||
| 127 | + | if err != nil { | |||
| 128 | + | t.Fatal(err) | |||
| 129 | + | } | |||
| 130 | + | if len(activated) != 2 || activated[0] != "base" || activated[1] != "late" { | |||
| 131 | + | t.Fatalf("activated tools = %v, want [base late]", activated) | |||
| 132 | + | } | |||
| 133 | + | }) | |||
| 64 | } | 134 | } | ||
| 65 | } | 135 | } | ||
| 66 | 136 | ||||
M packages/provider/gemini.go +4 -3
| @@ -151,13 +151,14 @@ func (c *geminiClient) buildRequest(req Request) (*gemRequest, string, error) { | |||
| 151 | 151 | } | |
| 152 | 152 | ||
| 153 | 153 | functionsEnabled := geminiSupportsFunctionCalling(m.ID) | |
| 154 | + | activeTools := activeToolDefinitions(req.Tools, req.Messages) | |
| 154 | 155 | ||
| 155 | 156 | // Convert tool defs. Gemini image-generation models reject function | |
| 156 | 157 | // declarations with "Function calling is not enabled for this model"; | |
| 157 | 158 | // for those models, send a direct multimodal prompt instead. | |
| 158 | - | if functionsEnabled && len(req.Tools) > 0 { | |
| 159 | - | decls := make([]gemFunctionDecl, 0, len(req.Tools)) | |
| 160 | - | for _, t := range req.Tools { | |
| 159 | + | if functionsEnabled && len(activeTools) > 0 { | |
| 160 | + | decls := make([]gemFunctionDecl, 0, len(activeTools)) | |
| 161 | + | for _, t := range activeTools { | |
| 161 | 162 | schema := sanitizeGeminiToolSchema(t.Schema) | |
| 162 | 163 | decls = append(decls, gemFunctionDecl{ | |
| 163 | 164 | Name: t.Name, | |
| @@ -151,13 +151,14 @@ func (c *geminiClient) buildRequest(req Request) (*gemRequest, string, error) { | |||||
| 151 | } | 151 | } | ||
| 152 | 152 | ||||
| 153 | functionsEnabled := geminiSupportsFunctionCalling(m.ID) | 153 | functionsEnabled := geminiSupportsFunctionCalling(m.ID) | ||
| 154 | + | activeTools := activeToolDefinitions(req.Tools, req.Messages) | |||
| 154 | 155 | ||||
| 155 | // Convert tool defs. Gemini image-generation models reject function | 156 | // Convert tool defs. Gemini image-generation models reject function | ||
| 156 | // declarations with "Function calling is not enabled for this model"; | 157 | // declarations with "Function calling is not enabled for this model"; | ||
| 157 | // for those models, send a direct multimodal prompt instead. | 158 | // for those models, send a direct multimodal prompt instead. | ||
| 158 | - | if functionsEnabled && len(req.Tools) > 0 { | 159 | + | if functionsEnabled && len(activeTools) > 0 { |
| 159 | - | decls := make([]gemFunctionDecl, 0, len(req.Tools)) | 160 | + | decls := make([]gemFunctionDecl, 0, len(activeTools)) |
| 160 | - | for _, t := range req.Tools { | 161 | + | for _, t := range activeTools { |
| 161 | schema := sanitizeGeminiToolSchema(t.Schema) | 162 | schema := sanitizeGeminiToolSchema(t.Schema) | ||
| 162 | decls = append(decls, gemFunctionDecl{ | 163 | decls = append(decls, gemFunctionDecl{ | ||
| 163 | Name: t.Name, | 164 | Name: t.Name, | ||
M packages/provider/openai.go +1 -6
| @@ -270,15 +270,10 @@ func (c *openaiClient) buildRequest(req Request) (*oaiRequest, error) { | |||
| 270 | 270 | ||
| 271 | 271 | deferredMode := isKimiDeferredModel(req.Model) | |
| 272 | 272 | toolByName := make(map[string]Tool, len(req.Tools)) | |
| 273 | - | activatedTools := make(map[string]bool) | |
| 274 | 273 | for _, t := range req.Tools { | |
| 275 | 274 | toolByName[t.Name] = t | |
| 276 | 275 | } | |
| 277 | - | for _, msg := range req.Messages { | |
| 278 | - | for _, name := range msg.AddedToolNames { | |
| 279 | - | activatedTools[name] = true | |
| 280 | - | } | |
| 281 | - | } | |
| 276 | + | activatedTools := activatedToolNames(req.Messages) | |
| 282 | 277 | ||
| 283 | 278 | // DeepSeek's chat-completions API rejects the multimodal content | |
| 284 | 279 | // schema (parts arrays containing image_url). Force every user/tool | |
| @@ -270,15 +270,10 @@ func (c *openaiClient) buildRequest(req Request) (*oaiRequest, error) { | |||||
| 270 | 270 | ||||
| 271 | deferredMode := isKimiDeferredModel(req.Model) | 271 | deferredMode := isKimiDeferredModel(req.Model) | ||
| 272 | toolByName := make(map[string]Tool, len(req.Tools)) | 272 | toolByName := make(map[string]Tool, len(req.Tools)) | ||
| 273 | - | activatedTools := make(map[string]bool) | |||
| 274 | for _, t := range req.Tools { | 273 | for _, t := range req.Tools { | ||
| 275 | toolByName[t.Name] = t | 274 | toolByName[t.Name] = t | ||
| 276 | } | 275 | } | ||
| 277 | - | for _, msg := range req.Messages { | 276 | + | activatedTools := activatedToolNames(req.Messages) |
| 278 | - | for _, name := range msg.AddedToolNames { | |||
| 279 | - | activatedTools[name] = true | |||
| 280 | - | } | |||
| 281 | - | } | |||
| 282 | 277 | ||||
| 283 | // DeepSeek's chat-completions API rejects the multimodal content | 278 | // DeepSeek's chat-completions API rejects the multimodal content | ||
| 284 | // schema (parts arrays containing image_url). Force every user/tool | 279 | // schema (parts arrays containing image_url). Force every user/tool | ||
M packages/provider/openai_codex.go +3 -2
| @@ -191,9 +191,10 @@ func (c *codexClient) buildRequest(req Request) (*codexRequest, error) { | |||
| 191 | 191 | body.Reasoning = &codexReasoningConfig{Effort: effort} | |
| 192 | 192 | } | |
| 193 | 193 | } | |
| 194 | - | if len(req.Tools) > 0 { | |
| 194 | + | activeTools := activeToolDefinitions(req.Tools, req.Messages) | |
| 195 | + | if len(activeTools) > 0 { | |
| 195 | 196 | body.ToolChoice = "auto" | |
| 196 | - | for _, t := range req.Tools { | |
| 197 | + | for _, t := range activeTools { | |
| 197 | 198 | params := t.Schema | |
| 198 | 199 | if len(params) == 0 { | |
| 199 | 200 | params = json.RawMessage(`{"type":"object","properties":{}}`) | |
| @@ -191,9 +191,10 @@ func (c *codexClient) buildRequest(req Request) (*codexRequest, error) { | |||||
| 191 | body.Reasoning = &codexReasoningConfig{Effort: effort} | 191 | body.Reasoning = &codexReasoningConfig{Effort: effort} | ||
| 192 | } | 192 | } | ||
| 193 | } | 193 | } | ||
| 194 | - | if len(req.Tools) > 0 { | 194 | + | activeTools := activeToolDefinitions(req.Tools, req.Messages) |
| 195 | + | if len(activeTools) > 0 { | |||
| 195 | body.ToolChoice = "auto" | 196 | body.ToolChoice = "auto" | ||
| 196 | - | for _, t := range req.Tools { | 197 | + | for _, t := range activeTools { |
| 197 | params := t.Schema | 198 | params := t.Schema | ||
| 198 | if len(params) == 0 { | 199 | if len(params) == 0 { | ||
| 199 | params = json.RawMessage(`{"type":"object","properties":{}}`) | 200 | params = json.RawMessage(`{"type":"object","properties":{}}`) | ||
M packages/provider/provider.go +24 -0
| @@ -130,6 +130,30 @@ type Tool struct { | |||
| 130 | 130 | Deferred bool `json:"deferred,omitempty"` | |
| 131 | 131 | } | |
| 132 | 132 | ||
| 133 | + | // activeToolDefinitions returns eager tools plus deferred tools activated by | |
| 134 | + | // prior tool results. Providers with a native position-sensitive deferred-tool | |
| 135 | + | // format can use the activation markers directly instead. | |
| 136 | + | func activeToolDefinitions(tools []Tool, messages []Message) []Tool { | |
| 137 | + | activated := activatedToolNames(messages) | |
| 138 | + | active := make([]Tool, 0, len(tools)) | |
| 139 | + | for _, tool := range tools { | |
| 140 | + | if !tool.Deferred || activated[tool.Name] { | |
| 141 | + | active = append(active, tool) | |
| 142 | + | } | |
| 143 | + | } | |
| 144 | + | return active | |
| 145 | + | } | |
| 146 | + | ||
| 147 | + | func activatedToolNames(messages []Message) map[string]bool { | |
| 148 | + | activated := make(map[string]bool) | |
| 149 | + | for _, message := range messages { | |
| 150 | + | for _, name := range message.AddedToolNames { | |
| 151 | + | activated[name] = true | |
| 152 | + | } | |
| 153 | + | } | |
| 154 | + | return activated | |
| 155 | + | } | |
| 156 | + | ||
| 133 | 157 | // Usage aggregates token counts and cost for a turn. | |
| 134 | 158 | type Usage struct { | |
| 135 | 159 | InputTokens int `json:"input_tokens"` | |
| @@ -130,6 +130,30 @@ type Tool struct { | |||||
| 130 | Deferred bool `json:"deferred,omitempty"` | 130 | Deferred bool `json:"deferred,omitempty"` | ||
| 131 | } | 131 | } | ||
| 132 | 132 | ||||
| 133 | + | // activeToolDefinitions returns eager tools plus deferred tools activated by | |||
| 134 | + | // prior tool results. Providers with a native position-sensitive deferred-tool | |||
| 135 | + | // format can use the activation markers directly instead. | |||
| 136 | + | func activeToolDefinitions(tools []Tool, messages []Message) []Tool { | |||
| 137 | + | activated := activatedToolNames(messages) | |||
| 138 | + | active := make([]Tool, 0, len(tools)) | |||
| 139 | + | for _, tool := range tools { | |||
| 140 | + | if !tool.Deferred || activated[tool.Name] { | |||
| 141 | + | active = append(active, tool) | |||
| 142 | + | } | |||
| 143 | + | } | |||
| 144 | + | return active | |||
| 145 | + | } | |||
| 146 | + | ||||
| 147 | + | func activatedToolNames(messages []Message) map[string]bool { | |||
| 148 | + | activated := make(map[string]bool) | |||
| 149 | + | for _, message := range messages { | |||
| 150 | + | for _, name := range message.AddedToolNames { | |||
| 151 | + | activated[name] = true | |||
| 152 | + | } | |||
| 153 | + | } | |||
| 154 | + | return activated | |||
| 155 | + | } | |||
| 156 | + | ||||
| 133 | // Usage aggregates token counts and cost for a turn. | 157 | // Usage aggregates token counts and cost for a turn. | ||
| 134 | type Usage struct { | 158 | type Usage struct { | ||
| 135 | InputTokens int `json:"input_tokens"` | 159 | InputTokens int `json:"input_tokens"` | ||