1package provider
2
3import (
4 "context"
5 "net/http"
6 "net/http/httptest"
7 "strings"
8 "testing"
9)
10
11func TestSSEParse(t *testing.T) {
12 r := strings.NewReader("event: foo\ndata: {\"a\":1}\n\ndata: hello\ndata: world\n\n")
13 ch := make(chan sseEvent, 4)
14 go readSSE(r, ch)
15
16 e := <-ch
17 if e.Event != "foo" || e.Data != `{"a":1}` {
18 t.Fatalf("event 1: %+v", e)
19 }
20 e = <-ch
21 if e.Event != "" || e.Data != "hello\nworld" {
22 t.Fatalf("event 2: %+v", e)
23 }
24 if _, ok := <-ch; ok {
25 t.Fatalf("channel not closed")
26 }
27}
28
29func TestModelCatalog(t *testing.T) {
30 if len(Catalog) == 0 {
31 t.Fatal("empty catalog")
32 }
33 if _, err := FindModel("anthropic", "claude-sonnet-4-5"); err != nil {
34 t.Fatal(err)
35 }
36 if _, err := FindModel("openai", "gpt-5"); err != nil {
37 t.Fatal(err)
38 }
39 if _, err := FindModel("", "nope"); err == nil {
40 t.Fatal("expected error")
41 }
42}
43
44func TestComputeCost(t *testing.T) {
45 m, _ := FindModel("anthropic", "claude-sonnet-4-5")
46 cost := ComputeCost(m, Usage{InputTokens: 1_000_000, OutputTokens: 1_000_000})
47 want := m.PriceInput + m.PriceOutput
48 if cost != want {
49 t.Fatalf("cost=%v want=%v", cost, want)
50 }
51}
52
53func TestAnthropicErrorStatus(t *testing.T) {
54 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
55 w.WriteHeader(http.StatusUnauthorized)
56 _, _ = w.Write([]byte(`{"error":{"type":"auth","message":"bad key"}}`))
57 }))
58 defer srv.Close()
59
60 c := NewAnthropic("x", srv.URL)
61 _, err := c.Stream(context.Background(), Request{Model: "claude-sonnet-4-5"})
62 if err == nil || !strings.Contains(err.Error(), "401") {
63 t.Fatalf("want 401 err, got %v", err)
64 }
65}
66
67func TestOpenAIErrorStatus(t *testing.T) {
68 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
69 w.WriteHeader(http.StatusBadRequest)
70 _, _ = w.Write([]byte(`{"error":{"message":"nope"}}`))
71 }))
72 defer srv.Close()
73
74 c := NewOpenAI("x", srv.URL)
75 _, err := c.Stream(context.Background(), Request{Model: "gpt-5"})
76 if err == nil || !strings.Contains(err.Error(), "400") {
77 t.Fatalf("want 400 err, got %v", err)
78 }
79}
80
81func TestAnthropicStreamHappyPath(t *testing.T) {
82 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
83 w.Header().Set("content-type", "text/event-stream")
84 w.WriteHeader(http.StatusOK)
85 fl, _ := w.(http.Flusher)
86 write := func(s string) {
87 _, _ = w.Write([]byte(s))
88 if fl != nil {
89 fl.Flush()
90 }
91 }
92 write("event: message_start\ndata: {\"message\":{\"usage\":{\"input_tokens\":10,\"output_tokens\":0}}}\n\n")
93 write("event: content_block_start\ndata: {\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n\n")
94 write("event: content_block_delta\ndata: {\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"hi\"}}\n\n")
95 write("event: content_block_stop\ndata: {\"index\":0}\n\n")
96 write("event: message_delta\ndata: {\"delta\":{\"stop_reason\":\"end_turn\"},\"usage\":{\"output_tokens\":5}}\n\n")
97 write("event: message_stop\ndata: {}\n\n")
98 }))
99 defer srv.Close()
100
101 c := NewAnthropic("x", srv.URL)
102 evs, err := c.Stream(context.Background(), Request{Model: "claude-sonnet-4-5"})
103 if err != nil {
104 t.Fatal(err)
105 }
106 var gotText string
107 var done EventDone
108 for ev := range evs {
109 switch e := ev.(type) {
110 case EventTextDelta:
111 gotText += e.Delta
112 case EventDone:
113 done = e
114 }
115 }
116 if gotText != "hi" {
117 t.Fatalf("text=%q", gotText)
118 }
119 if done.Stop != StopEnd {
120 t.Fatalf("stop=%v", done.Stop)
121 }
122}
123
124func TestOpenAIStreamHappyPath(t *testing.T) {
125 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
126 w.Header().Set("content-type", "text/event-stream")
127 w.WriteHeader(http.StatusOK)
128 fl, _ := w.(http.Flusher)
129 write := func(s string) {
130 _, _ = w.Write([]byte(s))
131 if fl != nil {
132 fl.Flush()
133 }
134 }
135 write("data: {\"choices\":[{\"index\":0,\"delta\":{\"content\":\"hel\"},\"finish_reason\":null}]}\n\n")
136 write("data: {\"choices\":[{\"index\":0,\"delta\":{\"content\":\"lo\"},\"finish_reason\":\"stop\"}]}\n\n")
137 write("data: {\"choices\":[],\"usage\":{\"prompt_tokens\":8,\"completion_tokens\":2}}\n\n")
138 write("data: [DONE]\n\n")
139 }))
140 defer srv.Close()
141
142 c := NewOpenAI("x", srv.URL)
143 evs, err := c.Stream(context.Background(), Request{Model: "gpt-5"})
144 if err != nil {
145 t.Fatal(err)
146 }
147 var gotText string
148 var done EventDone
149 for ev := range evs {
150 switch e := ev.(type) {
151 case EventTextDelta:
152 gotText += e.Delta
153 case EventDone:
154 done = e
155 }
156 }
157 if gotText != "hello" {
158 t.Fatalf("text=%q", gotText)
159 }
160 if done.Stop != StopEnd {
161 t.Fatalf("stop=%v", done.Stop)
162 }
163}