1package provider
2
3import (
4 "encoding/json"
5 "testing"
6)
7
8func TestKimiK3DeferredToolsLoadAtToolResult(t *testing.T) {
9 client := NewMoonshot("token", "").(*openaiClient)
10 tools := []Tool{
11 {Name: "search_tools", Schema: json.RawMessage(`{"type":"object"}`)},
12 {Name: "lookup_weather", Schema: json.RawMessage(`{"type":"object"}`), Deferred: true},
13 }
14 wire, err := client.buildRequest(Request{
15 Model: "kimi-k3",
16 Tools: tools,
17 Messages: []Message{
18 {Role: RoleUser, Content: []Content{TextBlock{Text: "weather"}}},
19 {Role: RoleAssistant, Content: []Content{ToolCallBlock{ID: "call-1", Name: "search_tools", Arguments: json.RawMessage(`{}`)}}},
20 {Role: RoleTool, Content: []Content{ToolResultBlock{CallID: "call-1", Content: []Content{TextBlock{Text: "found"}}}}, AddedToolNames: []string{"lookup_weather"}},
21 },
22 })
23 if err != nil {
24 t.Fatal(err)
25 }
26 if len(wire.Tools) != 1 || wire.Tools[0].Function.Name != "search_tools" {
27 t.Fatalf("top-level tools = %+v, want only search_tools", wire.Tools)
28 }
29 var loaded []oaiTool
30 for _, message := range wire.Messages {
31 if message.Role == "system" && len(message.Tools) > 0 {
32 loaded = message.Tools
33 }
34 }
35 if len(loaded) != 1 || loaded[0].Function.Name != "lookup_weather" {
36 t.Fatalf("loaded tools = %+v, want lookup_weather", loaded)
37 }
38}
39
40func TestDeferredToolsFallbackToActiveTopLevelDefinitions(t *testing.T) {
41 tools := []Tool{
42 {Name: "base", Schema: json.RawMessage(`{"type":"object"}`)},
43 {Name: "late", Schema: json.RawMessage(`{"type":"object"}`), Deferred: true},
44 }
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 },
110 }
111
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 })
134 }
135}
136
137func TestKimiK3AndGrok45CatalogMetadata(t *testing.T) {
138 for _, tc := range []struct {
139 provider string
140 model string
141 }{
142 {"kimi", "k3"},
143 {"moonshotai", "kimi-k3"},
144 {"moonshotai-cn", "kimi-k3"},
145 {"openrouter", "moonshotai/kimi-k3"},
146 {"vercel-ai-gateway", "moonshotai/kimi-k3"},
147 } {
148 m, err := FindModel(tc.provider, tc.model)
149 if err != nil {
150 t.Fatalf("%s/%s: %v", tc.provider, tc.model, err)
151 }
152 if m.MaxOutput != 131072 || !m.Reasoning {
153 t.Fatalf("%s/%s metadata = %+v", tc.provider, tc.model, m)
154 }
155 }
156 m, err := FindModel("xai", "grok-4.5")
157 if err != nil {
158 t.Fatal(err)
159 }
160 if !m.Reasoning || m.MaxOutput != 30000 || m.API != APIResponses {
161 t.Fatalf("xai/grok-4.5 metadata = %+v", m)
162 }
163}