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 TestOpenAINormalizesMeasuredCacheUsage(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,"completion_tokens":9,"prompt_tokens_details":{"cached_tokens":70,"cache_write_tokens":10}}}`)
35 fmt.Fprintln(w)
36 fmt.Fprintln(w, "data: [DONE]")
37 fmt.Fprintln(w)
38 }))
39 defer srv.Close()
40
41 events, err := NewOpenAI("test", srv.URL).Stream(context.Background(), Request{Model: "gpt-test"})
42 if err != nil {
43 t.Fatal(err)
44 }
45 usage := usageFromEvents(events)
46 if usage.InputTokens != 20 || usage.CacheReadTokens != 70 || usage.CacheWriteTokens != 10 || usage.CacheMeasuredPromptTokens != 100 || usage.CacheMeasuredReadTokens != 70 {
47 t.Fatalf("usage = %+v", usage)
48 }
49}
50
51func TestOpenAIResponsesNormalizesMeasuredCacheUsage(t *testing.T) {
52 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
53 w.Header().Set("content-type", "text/event-stream")
54 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}}}}`)
55 fmt.Fprintln(w)
56 }))
57 defer srv.Close()
58
59 events, err := NewOpenAICodex("test", "account", srv.URL).Stream(context.Background(), Request{Model: "gpt-5.5"})
60 if err != nil {
61 t.Fatal(err)
62 }
63 usage := usageFromEvents(events)
64 if usage.InputTokens != 20 || usage.CacheReadTokens != 70 || usage.CacheWriteTokens != 10 || usage.CacheMeasuredPromptTokens != 100 || usage.CacheMeasuredReadTokens != 70 {
65 t.Fatalf("usage = %+v", usage)
66 }
67}
68
69func TestOpenAIRejectsMalformedCacheUsageDetails(t *testing.T) {
70 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
71 w.Header().Set("content-type", "text/event-stream")
72 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}}}`)
73 fmt.Fprintln(w)
74 fmt.Fprintln(w, "data: [DONE]")
75 fmt.Fprintln(w)
76 }))
77 defer srv.Close()
78
79 events, err := NewOpenAI("test", srv.URL).Stream(context.Background(), Request{Model: "gpt-test"})
80 if err != nil {
81 t.Fatal(err)
82 }
83 usage := usageFromEvents(events)
84 if usage.InputTokens != 100 || usage.CacheReadTokens != 0 || usage.CacheWriteTokens != 0 || usage.CacheMeasuredPromptTokens != 0 || usage.CacheMeasuredReadTokens != 0 {
85 t.Fatalf("malformed cache usage = %+v", usage)
86 }
87}
88
89func TestOpenAIResponsesReportsReasoningTokens(t *testing.T) {
90 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
91 w.Header().Set("content-type", "text/event-stream")
92 fmt.Fprintln(w, `data: {"type":"response.completed","response":{"status":"completed","usage":{"input_tokens":12,"output_tokens":9,"output_tokens_details":{"reasoning_tokens":6}}}}`)
93 fmt.Fprintln(w)
94 }))
95 defer srv.Close()
96
97 events, err := NewOpenAICodex("test", "account", srv.URL).Stream(context.Background(), Request{Model: "gpt-5.5"})
98 if err != nil {
99 t.Fatal(err)
100 }
101 usage := usageFromEvents(events)
102 if usage.OutputTokens != 9 || usage.ReasoningTokens != 6 || !usage.ReasoningTokensKnown {
103 t.Fatalf("usage = %+v", usage)
104 }
105}
106
107func usageFromEvents(events <-chan Event) Usage {
108 var usage Usage
109 for event := range events {
110 if e, ok := event.(EventUsage); ok {
111 usage = e.Usage
112 }
113 }
114 return usage
115}