main 4509ec7

deferred_tools_test.go RawHistoryFolder

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
40// The deferred-tools handshake is a Moonshot API extension. Another
41// provider serving the same model id must get the standard wire instead:
42// no tools-bearing system message (it carries no content, which strict
43// schemas reject), and the activated tool in the top-level definitions
44// where every provider reads it.
45func TestKimiK3DeferredToolsSkippedForNonMoonshotProvider(t *testing.T) {
46	client := NewOpenAICompat("custom", "token", "https://example.test/v1", "").(*openaiClient)
47	tools := []Tool{
48		{Name: "search_tools", Schema: json.RawMessage(`{"type":"object"}`)},
49		{Name: "lookup_weather", Schema: json.RawMessage(`{"type":"object"}`), Deferred: true},
50	}
51	wire, err := client.buildRequest(Request{
52		Model: "kimi-k3",
53		Tools: tools,
54		Messages: []Message{
55			{Role: RoleUser, Content: []Content{TextBlock{Text: "weather"}}},
56			{Role: RoleAssistant, Content: []Content{ToolCallBlock{ID: "call-1", Name: "search_tools", Arguments: json.RawMessage(`{}`)}}},
57			{Role: RoleTool, Content: []Content{ToolResultBlock{CallID: "call-1", Content: []Content{TextBlock{Text: "found"}}}}, AddedToolNames: []string{"lookup_weather"}},
58		},
59	})
60	if err != nil {
61		t.Fatal(err)
62	}
63	for _, message := range wire.Messages {
64		if len(message.Tools) > 0 {
65			t.Fatalf("message %q carries tools; deferred injection must not reach a non-Moonshot provider", message.Role)
66		}
67		if message.Content == nil && len(message.ToolCalls) == 0 {
68			t.Fatalf("message %q has neither content nor tool calls", message.Role)
69		}
70	}
71	var names []string
72	for _, tool := range wire.Tools {
73		names = append(names, tool.Function.Name)
74	}
75	if len(names) != 2 || names[0] != "search_tools" || names[1] != "lookup_weather" {
76		t.Fatalf("top-level tools = %v, want [search_tools lookup_weather]", names)
77	}
78}
79
80func TestMoonshotCNKimiK3DeferredToolsLoadAtToolResult(t *testing.T) {
81	client := NewMoonshotCN("token", "").(*openaiClient)
82	tools := []Tool{
83		{Name: "search_tools", Schema: json.RawMessage(`{"type":"object"}`)},
84		{Name: "lookup_weather", Schema: json.RawMessage(`{"type":"object"}`), Deferred: true},
85	}
86	wire, err := client.buildRequest(Request{
87		Model: "kimi-k3",
88		Tools: tools,
89		Messages: []Message{
90			{Role: RoleUser, Content: []Content{TextBlock{Text: "weather"}}},
91			{Role: RoleAssistant, Content: []Content{ToolCallBlock{ID: "call-1", Name: "search_tools", Arguments: json.RawMessage(`{}`)}}},
92			{Role: RoleTool, Content: []Content{ToolResultBlock{CallID: "call-1", Content: []Content{TextBlock{Text: "found"}}}}, AddedToolNames: []string{"lookup_weather"}},
93		},
94	})
95	if err != nil {
96		t.Fatal(err)
97	}
98	if len(wire.Tools) != 1 || wire.Tools[0].Function.Name != "search_tools" {
99		t.Fatalf("top-level tools = %+v, want only search_tools", wire.Tools)
100	}
101	var loaded []oaiTool
102	for _, message := range wire.Messages {
103		if message.Role == "system" && len(message.Tools) > 0 {
104			loaded = message.Tools
105		}
106	}
107	if len(loaded) != 1 || loaded[0].Function.Name != "lookup_weather" {
108		t.Fatalf("loaded tools = %+v, want lookup_weather", loaded)
109	}
110}
111
112func TestDeferredToolsFallbackToActiveTopLevelDefinitions(t *testing.T) {
113	tools := []Tool{
114		{Name: "base", Schema: json.RawMessage(`{"type":"object"}`)},
115		{Name: "late", Schema: json.RawMessage(`{"type":"object"}`), Deferred: true},
116	}
117	builders := map[string]func(Request) ([]string, error){
118		"openai": func(req Request) ([]string, error) {
119			wire, err := NewOpenAI("token", "").(*openaiClient).buildRequest(req)
120			if err != nil {
121				return nil, err
122			}
123			names := make([]string, 0, len(wire.Tools))
124			for _, tool := range wire.Tools {
125				names = append(names, tool.Function.Name)
126			}
127			return names, nil
128		},
129		"anthropic": func(req Request) ([]string, error) {
130			req.Model = "claude-sonnet-4-6"
131			wire, err := NewAnthropic("token", "").(*anthropicClient).buildRequest(req)
132			if err != nil {
133				return nil, err
134			}
135			names := make([]string, 0, len(wire.Tools))
136			for _, tool := range wire.Tools {
137				names = append(names, tool.Name)
138			}
139			return names, nil
140		},
141		"bedrock": func(req Request) ([]string, error) {
142			req.Model = "anthropic.claude-sonnet-4-5-20250929-v1:0"
143			wire, err := (&bedrockClient{region: "us-east-1"}).buildRequest(req)
144			if err != nil {
145				return nil, err
146			}
147			if wire.ToolConfig == nil {
148				return nil, nil
149			}
150			names := make([]string, 0, len(wire.ToolConfig.Tools))
151			for _, tool := range wire.ToolConfig.Tools {
152				names = append(names, tool.ToolSpec.Name)
153			}
154			return names, nil
155		},
156		"gemini": func(req Request) ([]string, error) {
157			req.Model = "gemini-2.5-pro"
158			wire, _, err := NewGemini("token", "").(*geminiClient).buildRequest(req)
159			if err != nil {
160				return nil, err
161			}
162			var names []string
163			for _, group := range wire.Tools {
164				for _, tool := range group.FunctionDeclarations {
165					names = append(names, tool.Name)
166				}
167			}
168			return names, nil
169		},
170		"openai-codex": func(req Request) ([]string, error) {
171			req.Model = "gpt-5.5"
172			wire, err := newOpenAICodexClient("token", "account", "").buildRequest(req)
173			if err != nil {
174				return nil, err
175			}
176			names := make([]string, 0, len(wire.Tools))
177			for _, tool := range wire.Tools {
178				names = append(names, tool.Name)
179			}
180			return names, nil
181		},
182	}
183
184	for name, build := range builders {
185		t.Run(name, func(t *testing.T) {
186			initial, err := build(Request{Model: "gpt-5", Tools: tools})
187			if err != nil {
188				t.Fatal(err)
189			}
190			if len(initial) != 1 || initial[0] != "base" {
191				t.Fatalf("initial tools = %v, want [base]", initial)
192			}
193
194			activated, err := build(Request{
195				Model:    "gpt-5",
196				Tools:    tools,
197				Messages: []Message{{Role: RoleTool, AddedToolNames: []string{"late"}}},
198			})
199			if err != nil {
200				t.Fatal(err)
201			}
202			if len(activated) != 2 || activated[0] != "base" || activated[1] != "late" {
203				t.Fatalf("activated tools = %v, want [base late]", activated)
204			}
205		})
206	}
207}
208
209func TestKimiK3AndGrok45CatalogMetadata(t *testing.T) {
210	for _, tc := range []struct {
211		provider string
212		model    string
213	}{
214		{"kimi", "k3"},
215		{"moonshotai", "kimi-k3"},
216		{"moonshotai-cn", "kimi-k3"},
217		{"openrouter", "moonshotai/kimi-k3"},
218		{"vercel-ai-gateway", "moonshotai/kimi-k3"},
219	} {
220		m, err := FindModel(tc.provider, tc.model)
221		if err != nil {
222			t.Fatalf("%s/%s: %v", tc.provider, tc.model, err)
223		}
224		if m.MaxOutput != 131072 || !m.Reasoning {
225			t.Fatalf("%s/%s metadata = %+v", tc.provider, tc.model, m)
226		}
227	}
228	m, err := FindModel("xai", "grok-4.5")
229	if err != nil {
230		t.Fatal(err)
231	}
232	if !m.Reasoning || m.MaxOutput != 30000 || m.API != APIResponses {
233		t.Fatalf("xai/grok-4.5 metadata = %+v", m)
234	}
235}
236
237func TestMoonshotDeferredMarkerSurvivesDedupOnRetainedMessage(t *testing.T) {
238	client := NewMoonshot("token", "").(*openaiClient)
239	tools := []Tool{
240		{Name: "search_tools", Schema: json.RawMessage(`{"type":"object"}`)},
241		{Name: "lookup_weather", Schema: json.RawMessage(`{"type":"object"}`), Deferred: true},
242	}
243	wire, err := client.buildRequest(Request{
244		Model: "kimi-k3",
245		Tools: tools,
246		Messages: []Message{
247			{Role: RoleUser, Content: []Content{TextBlock{Text: "weather"}}},
248			{Role: RoleAssistant, Content: []Content{ToolCallBlock{ID: "call-1", Name: "search_tools", Arguments: json.RawMessage(`{}`)}}},
249			// Duplicate results: repair keeps the first and its marker, so
250			// the deferred tool still loads on the handshake message.
251			{Role: RoleTool, Content: []Content{ToolResultBlock{CallID: "call-1", Content: []Content{TextBlock{Text: "found"}}}}, AddedToolNames: []string{"lookup_weather"}},
252			{Role: RoleTool, Content: []Content{ToolResultBlock{CallID: "call-1", Content: []Content{TextBlock{Text: "stale duplicate"}}}}, AddedToolNames: []string{"lookup_weather"}},
253		},
254	})
255	if err != nil {
256		t.Fatal(err)
257	}
258	var loaded []oaiTool
259	for _, message := range wire.Messages {
260		if message.Role == "system" && len(message.Tools) > 0 {
261			loaded = message.Tools
262		}
263	}
264	if len(loaded) != 1 || loaded[0].Function.Name != "lookup_weather" {
265		t.Fatalf("loaded tools = %+v, want lookup_weather from the retained marker", loaded)
266	}
267}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close