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}