v0.4.3 2a19df1

openai_openrouter_test.go RawHistoryFolder

1package provider
2
3import (
4	"context"
5	"encoding/json"
6	"io"
7	"net/http"
8	"net/http/httptest"
9	"strings"
10	"testing"
11)
12
13func TestOpenRouterBuildRequestIncludesSessionID(t *testing.T) {
14	c := NewOpenRouter("token", "").(*openaiClient)
15	wire, err := c.buildRequest(Request{
16		Model:     "@preset/flash",
17		SessionID: "sess-abc",
18		Messages:  []Message{{Role: RoleUser, Content: []Content{TextBlock{Text: "hi"}}}},
19	})
20	if err != nil {
21		t.Fatal(err)
22	}
23	if wire.SessionID != "sess-abc" {
24		t.Fatalf("SessionID = %q; want sess-abc", wire.SessionID)
25	}
26}
27
28func TestOpenAIBuildRequestOmitsSessionID(t *testing.T) {
29	c := NewOpenAI("token", "").(*openaiClient)
30	wire, err := c.buildRequest(Request{
31		Model:        "gpt-5",
32		SessionID:    "sess-abc",
33		MaxToolCalls: 4,
34		Messages:     []Message{{Role: RoleUser, Content: []Content{TextBlock{Text: "hi"}}}},
35	})
36	if err != nil {
37		t.Fatal(err)
38	}
39	if wire.SessionID != "" {
40		t.Fatalf("SessionID = %q; want empty on non-openrouter clients", wire.SessionID)
41	}
42	if wire.MaxToolCalls != 0 {
43		t.Fatalf("MaxToolCalls = %d; want empty on non-openrouter clients", wire.MaxToolCalls)
44	}
45}
46
47func TestOpenRouterOmitsServerToolCallsFromHistory(t *testing.T) {
48	c := NewOpenRouter("token", "").(*openaiClient)
49	wire, err := c.buildRequest(Request{
50		Model: "@preset/flash",
51		Messages: []Message{
52			{Role: RoleUser, Content: []Content{TextBlock{Text: "time?"}}},
53			{Role: RoleAssistant, Content: []Content{
54				ToolCallBlock{ID: "srv-1", Name: "openrouter:datetime", Arguments: json.RawMessage(`{}`), Server: true},
55				TextBlock{Text: "it is noon"},
56			}},
57		},
58	})
59	if err != nil {
60		t.Fatal(err)
61	}
62	if len(wire.Messages) != 2 {
63		t.Fatalf("messages = %d; want 2", len(wire.Messages))
64	}
65	am := wire.Messages[1]
66	if len(am.ToolCalls) != 0 {
67		t.Fatalf("replayed server tool_calls = %+v; want none", am.ToolCalls)
68	}
69	if am.Content != "it is noon" {
70		t.Fatalf("content = %#v", am.Content)
71	}
72}
73
74func TestOpenRouterServerToolStreamDoesNotStopForClientExecution(t *testing.T) {
75	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
76		body, _ := io.ReadAll(r.Body)
77		if !strings.Contains(string(body), `"session_id":"sess-1"`) {
78			t.Errorf("request body missing session_id: %s", body)
79		}
80		w.Header().Set("content-type", "text/event-stream")
81		w.WriteHeader(http.StatusOK)
82		fl, _ := w.(http.Flusher)
83		write := func(s string) {
84			_, _ = w.Write([]byte(s))
85			if fl != nil {
86				fl.Flush()
87			}
88		}
89		write("data: {\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"id\":\"call_dt\",\"type\":\"openrouter:datetime\",\"name\":\"openrouter:datetime\",\"arguments\":\"{}\"}]},\"finish_reason\":null}]}\n\n")
90		write("data: {\"choices\":[{\"index\":0,\"delta\":{\"content\":\"2026-08-30\"},\"finish_reason\":null}]}\n\n")
91		write("data: {\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"tool_calls\"}]}\n\n")
92		write("data: [DONE]\n\n")
93	}))
94	defer srv.Close()
95
96	c := NewOpenRouter("token", srv.URL)
97	evs, err := c.Stream(context.Background(), Request{
98		Model:     "@preset/flash",
99		SessionID: "sess-1",
100		Messages:  []Message{{Role: RoleUser, Content: []Content{TextBlock{Text: "time?"}}}},
101	})
102	if err != nil {
103		t.Fatal(err)
104	}
105	var done EventDone
106	for ev := range evs {
107		if e, ok := ev.(EventDone); ok {
108			done = e
109		}
110	}
111	if done.Stop != StopEnd {
112		t.Fatalf("stop = %v; want end (server tools must not trigger client execution)", done.Stop)
113	}
114	var sawServer bool
115	for _, c := range done.Message.Content {
116		if tc, ok := c.(ToolCallBlock); ok {
117			if !tc.Server || tc.Name != "openrouter:datetime" {
118				t.Fatalf("tool call = %+v; want server openrouter:datetime", tc)
119			}
120			sawServer = true
121		}
122	}
123	if !sawServer {
124		t.Fatalf("assembled message missing server tool call: %+v", done.Message.Content)
125	}
126}
127
128func TestOpenRouterAdvertisesServerTools(t *testing.T) {
129	c := NewOpenRouter("token", "").(*openaiClient)
130	wire, err := c.buildRequest(Request{
131		Model: "@preset/flash",
132		Tools: []Tool{
133			{Name: "openrouter:datetime"},
134			{Name: "openrouter:web_search", Schema: json.RawMessage(`{"max_results":1}`)},
135			{Name: "read", Description: "read a file", Schema: json.RawMessage(`{"type":"object"}`)},
136		},
137		Messages: []Message{{Role: RoleUser, Content: []Content{TextBlock{Text: "hi"}}}},
138	})
139	if err != nil {
140		t.Fatal(err)
141	}
142	if len(wire.Tools) != 3 {
143		t.Fatalf("tools = %d; want 3", len(wire.Tools))
144	}
145	if wire.Tools[0].Type != "openrouter:datetime" || wire.Tools[0].Function != nil {
146		t.Fatalf("datetime tool = %+v", wire.Tools[0])
147	}
148	if wire.Tools[1].Type != "openrouter:web_search" || string(wire.Tools[1].Parameters) != `{"max_results":1}` {
149		t.Fatalf("web_search tool = %+v", wire.Tools[1])
150	}
151	if wire.Tools[2].Type != "function" || wire.Tools[2].Function == nil || wire.Tools[2].Function.Name != "read" {
152		t.Fatalf("function tool = %+v", wire.Tools[2])
153	}
154}

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