1package provider
2
3import (
4 "context"
5 "fmt"
6 "net/http"
7 "net/http/httptest"
8 "testing"
9)
10
11func TestOpenAIReportsReasoningTokens(t *testing.T) {
12 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
13 w.Header().Set("content-type", "text/event-stream")
14 fmt.Fprintln(w, `data: {"choices":[{"delta":{"content":"ok"},"finish_reason":"stop"}],"usage":{"prompt_tokens":12,"completion_tokens":9,"completion_tokens_details":{"reasoning_tokens":6}}}`)
15 fmt.Fprintln(w)
16 fmt.Fprintln(w, "data: [DONE]")
17 fmt.Fprintln(w)
18 }))
19 defer srv.Close()
20
21 events, err := NewOpenAI("test", srv.URL).Stream(context.Background(), Request{Model: "gpt-test"})
22 if err != nil {
23 t.Fatal(err)
24 }
25 usage := usageFromEvents(events)
26 if usage.OutputTokens != 9 || usage.ReasoningTokens != 6 || !usage.ReasoningTokensKnown {
27 t.Fatalf("usage = %+v", usage)
28 }
29}
30
31func TestDeepSeekNormalizesNativeCacheUsage(t *testing.T) {
32 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
33 w.Header().Set("content-type", "text/event-stream")
34 fmt.Fprintln(w, `data: {"choices":[{"delta":{"content":"ok"},"finish_reason":"stop"}],"usage":{"prompt_tokens":100,"prompt_cache_hit_tokens":70,"prompt_cache_miss_tokens":30,"completion_tokens":9}}`)
35 fmt.Fprintln(w)
36 fmt.Fprintln(w, "data: [DONE]")
37 fmt.Fprintln(w)
38 }))
39 defer srv.Close()
40
41 events, err := NewDeepSeek("test", srv.URL).Stream(context.Background(), Request{Model: "deepseek-flash"})
42 if err != nil {
43 t.Fatal(err)
44 }
45 usage := usageFromEvents(events)
46 if usage.InputTokens != 30 || usage.CacheReadTokens != 70 || usage.CacheWriteTokens != 0 || usage.CacheMeasuredPromptTokens != 100 || usage.CacheMeasuredReadTokens != 70 {
47 t.Fatalf("usage = %+v", usage)
48 }
49}
50
51func TestDeepSeekRejectsIncompleteCacheUsage(t *testing.T) {
52 hits := 70
53 usage := normalizeDeepSeekUsage(100, 9, &hits, nil)
54 if usage.InputTokens != 100 || usage.CacheReadTokens != 0 || usage.CacheMeasuredPromptTokens != 0 {
55 t.Fatalf("usage = %+v", usage)
56 }
57}
58
59func TestOpenAINormalizesMeasuredCacheUsage(t *testing.T) {
60 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
61 w.Header().Set("content-type", "text/event-stream")
62 fmt.Fprintln(w, `data: {"choices":[{"delta":{"content":"ok"},"finish_reason":"stop"}],"usage":{"prompt_tokens":100,"completion_tokens":9,"prompt_tokens_details":{"cached_tokens":70,"cache_write_tokens":10}}}`)
63 fmt.Fprintln(w)
64 fmt.Fprintln(w, "data: [DONE]")
65 fmt.Fprintln(w)
66 }))
67 defer srv.Close()
68
69 events, err := NewOpenAI("test", srv.URL).Stream(context.Background(), Request{Model: "gpt-test"})
70 if err != nil {
71 t.Fatal(err)
72 }
73 usage := usageFromEvents(events)
74 if usage.InputTokens != 20 || usage.CacheReadTokens != 70 || usage.CacheWriteTokens != 10 || usage.CacheMeasuredPromptTokens != 100 || usage.CacheMeasuredReadTokens != 70 {
75 t.Fatalf("usage = %+v", usage)
76 }
77}
78
79func TestOpenAIResponsesNormalizesMeasuredCacheUsage(t *testing.T) {
80 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
81 w.Header().Set("content-type", "text/event-stream")
82 fmt.Fprintln(w, `data: {"type":"response.completed","response":{"status":"completed","usage":{"input_tokens":100,"output_tokens":9,"input_tokens_details":{"cached_tokens":70,"cache_write_tokens":10}}}}`)
83 fmt.Fprintln(w)
84 }))
85 defer srv.Close()
86
87 events, err := NewOpenAICodex("test", "account", srv.URL).Stream(context.Background(), Request{Model: "gpt-5.5"})
88 if err != nil {
89 t.Fatal(err)
90 }
91 usage := usageFromEvents(events)
92 if usage.InputTokens != 20 || usage.CacheReadTokens != 70 || usage.CacheWriteTokens != 10 || usage.CacheMeasuredPromptTokens != 100 || usage.CacheMeasuredReadTokens != 70 {
93 t.Fatalf("usage = %+v", usage)
94 }
95}
96
97func TestOpenAIRejectsMalformedCacheUsageDetails(t *testing.T) {
98 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
99 w.Header().Set("content-type", "text/event-stream")
100 fmt.Fprintln(w, `data: {"choices":[{"delta":{"content":"ok"},"finish_reason":"stop"}],"usage":{"prompt_tokens":100,"completion_tokens":9,"prompt_tokens_details":{"cached_tokens":70,"cache_write_tokens":40}}}`)
101 fmt.Fprintln(w)
102 fmt.Fprintln(w, "data: [DONE]")
103 fmt.Fprintln(w)
104 }))
105 defer srv.Close()
106
107 events, err := NewOpenAI("test", srv.URL).Stream(context.Background(), Request{Model: "gpt-test"})
108 if err != nil {
109 t.Fatal(err)
110 }
111 usage := usageFromEvents(events)
112 if usage.InputTokens != 100 || usage.CacheReadTokens != 0 || usage.CacheWriteTokens != 0 || usage.CacheMeasuredPromptTokens != 0 || usage.CacheMeasuredReadTokens != 0 {
113 t.Fatalf("malformed cache usage = %+v", usage)
114 }
115}
116
117func TestOpenAIResponsesReportsReasoningTokens(t *testing.T) {
118 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
119 w.Header().Set("content-type", "text/event-stream")
120 fmt.Fprintln(w, `data: {"type":"response.completed","response":{"status":"completed","usage":{"input_tokens":12,"output_tokens":9,"output_tokens_details":{"reasoning_tokens":6}}}}`)
121 fmt.Fprintln(w)
122 }))
123 defer srv.Close()
124
125 events, err := NewOpenAICodex("test", "account", srv.URL).Stream(context.Background(), Request{Model: "gpt-5.5"})
126 if err != nil {
127 t.Fatal(err)
128 }
129 usage := usageFromEvents(events)
130 if usage.OutputTokens != 9 || usage.ReasoningTokens != 6 || !usage.ReasoningTokensKnown {
131 t.Fatalf("usage = %+v", usage)
132 }
133}
134
135func usageFromEvents(events <-chan Event) Usage {
136 var usage Usage
137 for event := range events {
138 if e, ok := event.(EventUsage); ok {
139 usage = e.Usage
140 }
141 }
142 return usage
143}