v0.3.70 5bef29b

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 == 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}

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