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 == nil || 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 == nil || loaded[0].Function.Name != "lookup_weather" {
36 t.Fatalf("loaded tools = %+v, want lookup_weather", loaded)
37 }
38}
39
40// The deferred-tools handshake is a Moonshot API extension. Another provider
41// serving the same model id must get the standard wire instead: no tools-bearing
42// system message (it carries no content, which strict schemas reject), and the
43// activated tool in the top-level definitions where every provider reads it.
44func TestKimiK3DeferredToolsSkippedForNonMoonshotProvider(t *testing.T) {
45 client := NewGondola("token", "").(*openaiClient)
46 tools := []Tool{
47 {Name: "search_tools", Schema: json.RawMessage(`{"type":"object"}`)},
48 {Name: "lookup_weather", Schema: json.RawMessage(`{"type":"object"}`), Deferred: true},
49 }
50 wire, err := client.buildRequest(Request{
51 Model: "kimi-k3",
52 Tools: tools,
53 Messages: []Message{
54 {Role: RoleUser, Content: []Content{TextBlock{Text: "weather"}}},
55 {Role: RoleAssistant, Content: []Content{ToolCallBlock{ID: "call-1", Name: "search_tools", Arguments: json.RawMessage(`{}`)}}},
56 {Role: RoleTool, Content: []Content{ToolResultBlock{CallID: "call-1", Content: []Content{TextBlock{Text: "found"}}}}, AddedToolNames: []string{"lookup_weather"}},
57 },
58 })
59 if err != nil {
60 t.Fatal(err)
61 }
62 for _, message := range wire.Messages {
63 if len(message.Tools) > 0 {
64 t.Fatalf("message %q carries tools; deferred injection must not reach a non-Moonshot provider", message.Role)
65 }
66 if message.Content == nil && len(message.ToolCalls) == 0 {
67 t.Fatalf("message %q has neither content nor tool calls", message.Role)
68 }
69 }
70 var names []string
71 for _, tool := range wire.Tools {
72 if tool.Function != nil {
73 names = append(names, tool.Function.Name)
74 }
75 }
76 if len(names) != 2 || names[0] != "search_tools" || names[1] != "lookup_weather" {
77 t.Fatalf("top-level tools = %v, want [search_tools lookup_weather]", names)
78 }
79}
80
81func TestDeferredToolsFallbackToActiveTopLevelDefinitions(t *testing.T) {
82 tools := []Tool{
83 {Name: "base", Schema: json.RawMessage(`{"type":"object"}`)},
84 {Name: "late", Schema: json.RawMessage(`{"type":"object"}`), Deferred: true},
85 }
86 builders := map[string]func(Request) ([]string, error){
87 "openai": func(req Request) ([]string, error) {
88 wire, err := NewOpenAI("token", "").(*openaiClient).buildRequest(req)
89 if err != nil {
90 return nil, err
91 }
92 names := make([]string, 0, len(wire.Tools))
93 for _, tool := range wire.Tools {
94 if tool.Function != nil {
95 names = append(names, tool.Function.Name)
96 }
97 }
98 return names, nil
99 },
100 "anthropic": func(req Request) ([]string, error) {
101 req.Model = "claude-sonnet-4-6"
102 wire, err := NewAnthropic("token", "").(*anthropicClient).buildRequest(req)
103 if err != nil {
104 return nil, err
105 }
106 names := make([]string, 0, len(wire.Tools))
107 for _, tool := range wire.Tools {
108 names = append(names, tool.Name)
109 }
110 return names, nil
111 },
112 "bedrock": func(req Request) ([]string, error) {
113 req.Model = "anthropic.claude-sonnet-4-5-20250929-v1:0"
114 wire, err := (&bedrockClient{region: "us-east-1"}).buildRequest(req)
115 if err != nil {
116 return nil, err
117 }
118 if wire.ToolConfig == nil {
119 return nil, nil
120 }
121 names := make([]string, 0, len(wire.ToolConfig.Tools))
122 for _, tool := range wire.ToolConfig.Tools {
123 names = append(names, tool.ToolSpec.Name)
124 }
125 return names, nil
126 },
127 "gemini": func(req Request) ([]string, error) {
128 req.Model = "gemini-2.5-pro"
129 wire, _, err := NewGemini("token", "").(*geminiClient).buildRequest(req)
130 if err != nil {
131 return nil, err
132 }
133 var names []string
134 for _, group := range wire.Tools {
135 for _, tool := range group.FunctionDeclarations {
136 names = append(names, tool.Name)
137 }
138 }
139 return names, nil
140 },
141 "openai-codex": func(req Request) ([]string, error) {
142 req.Model = "gpt-5.5"
143 wire, err := NewOpenAICodex("token", "account", "").(*codexClient).buildRequest(req)
144 if err != nil {
145 return nil, err
146 }
147 names := make([]string, 0, len(wire.Tools))
148 for _, tool := range wire.Tools {
149 names = append(names, tool.Name)
150 }
151 return names, nil
152 },
153 }
154
155 for name, build := range builders {
156 t.Run(name, func(t *testing.T) {
157 initial, err := build(Request{Model: "gpt-5", Tools: tools})
158 if err != nil {
159 t.Fatal(err)
160 }
161 if len(initial) != 1 || initial[0] != "base" {
162 t.Fatalf("initial tools = %v, want [base]", initial)
163 }
164
165 activated, err := build(Request{
166 Model: "gpt-5",
167 Tools: tools,
168 Messages: []Message{{Role: RoleTool, AddedToolNames: []string{"late"}}},
169 })
170 if err != nil {
171 t.Fatal(err)
172 }
173 if len(activated) != 2 || activated[0] != "base" || activated[1] != "late" {
174 t.Fatalf("activated tools = %v, want [base late]", activated)
175 }
176 })
177 }
178}
179
180func TestKimiK3AndGrok45CatalogMetadata(t *testing.T) {
181 for _, tc := range []struct {
182 provider string
183 model string
184 }{
185 {"kimi", "k3"},
186 {"moonshotai", "kimi-k3"},
187 {"moonshotai-cn", "kimi-k3"},
188 {"openrouter", "moonshotai/kimi-k3"},
189 {"vercel-ai-gateway", "moonshotai/kimi-k3"},
190 } {
191 m, err := FindModel(tc.provider, tc.model)
192 if err != nil {
193 t.Fatalf("%s/%s: %v", tc.provider, tc.model, err)
194 }
195 if m.MaxOutput != 131072 || !m.Reasoning {
196 t.Fatalf("%s/%s metadata = %+v", tc.provider, tc.model, m)
197 }
198 }
199 m, err := FindModel("xai", "grok-4.5")
200 if err != nil {
201 t.Fatal(err)
202 }
203 if !m.Reasoning || m.MaxOutput != 30000 || m.API != APIResponses {
204 t.Fatalf("xai/grok-4.5 metadata = %+v", m)
205 }
206}