-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathusage_cache_test.go
More file actions
186 lines (174 loc) · 6.7 KB
/
Copy pathusage_cache_test.go
File metadata and controls
186 lines (174 loc) · 6.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
package llm
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"testing"
)
// Cache-token exclusive normalization (odek applyUsage parity).
//
// Anthropic reports cache tokens exclusively (input_tokens excludes them).
// OpenAI (prompt_tokens_details.cached_tokens) and DeepSeek
// (prompt_cache_hit_tokens + prompt_cache_miss_tokens = prompt_tokens)
// report them inclusively, as subsets of prompt_tokens.
//
// Usage.PromptTokens must be exclusive ("uncached" input) on every
// provider, with cache volumes carried in CacheReadTokens /
// CacheCreationTokens, so budget enforcement can sum them without
// double-counting. CacheReported is true when any cache field was present.
func TestUsageCache_OpenAICachedTokens(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, `{
"choices": [{"message": {"content": "ok"}, "finish_reason": "stop"}],
"usage": {
"prompt_tokens": 300,
"completion_tokens": 30,
"prompt_tokens_details": {"cached_tokens": 200}
}
}`)
}))
defer srv.Close()
cc := newTestClient(t, ProviderConfig{ID: "openai", Format: FormatOpenAI, BaseURL: srv.URL, APIKey: "k"}, srv)
res, err := cc.Call(context.Background(), &ChatRequest{Messages: []Message{{Role: RoleUser, Content: "hi"}}})
if err != nil {
t.Fatal(err)
}
if res.Usage.PromptTokens != 100 {
t.Errorf("PromptTokens = %d, want 100 (300 prompt − 200 cached; exclusive)", res.Usage.PromptTokens)
}
if res.Usage.CacheReadTokens != 200 {
t.Errorf("CacheReadTokens = %d, want 200 (OpenAI cached_tokens)", res.Usage.CacheReadTokens)
}
if res.Usage.CachedTokens != 200 {
t.Errorf("CachedTokens = %d, want 200 (display field unchanged)", res.Usage.CachedTokens)
}
if !res.Usage.CacheReported {
t.Error("CacheReported = false, want true (prompt_tokens_details present)")
}
if res.Usage.CompletionTokens != 30 {
t.Errorf("CompletionTokens = %d, want 30", res.Usage.CompletionTokens)
}
}
func TestUsageCache_AnthropicExclusive(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, `{
"content": [{"type":"text","text":"ok"}],
"stop_reason":"end_turn",
"usage":{
"input_tokens": 500,
"output_tokens": 50,
"cache_creation_input_tokens": 400,
"cache_read_input_tokens": 100
}
}`)
}))
defer srv.Close()
cc := newTestClient(t, ProviderConfig{ID: "anthropic", Format: FormatAnthropic, BaseURL: srv.URL, APIKey: "k", Quirks: Quirks{AnthropicVersion: "2023-06-01"}}, srv)
res, err := cc.Call(context.Background(), &ChatRequest{Messages: []Message{{Role: RoleUser, Content: "hi"}}})
if err != nil {
t.Fatal(err)
}
// Anthropic input_tokens is already uncached-only: no subtraction.
if res.Usage.PromptTokens != 500 {
t.Errorf("PromptTokens = %d, want 500 (Anthropic is exclusive already)", res.Usage.PromptTokens)
}
if res.Usage.CacheCreationTokens != 400 || res.Usage.CacheReadTokens != 100 {
t.Errorf("cache fields = %d/%d, want 400/100", res.Usage.CacheCreationTokens, res.Usage.CacheReadTokens)
}
if !res.Usage.CacheReported {
t.Error("CacheReported = false, want true (Anthropic cache fields present)")
}
if res.Usage.CompletionTokens != 50 {
t.Errorf("CompletionTokens = %d, want 50", res.Usage.CompletionTokens)
}
}
func TestUsageCache_DeepSeekHitMiss(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, `{
"choices": [{"message": {"content": "ok"}, "finish_reason": "stop"}],
"usage": {
"prompt_tokens": 1000,
"completion_tokens": 40,
"prompt_cache_hit_tokens": 750,
"prompt_cache_miss_tokens": 250
}
}`)
}))
defer srv.Close()
cc := newTestClient(t, ProviderConfig{ID: "deepseek", Format: FormatOpenAI, BaseURL: srv.URL, APIKey: "k"}, srv)
res, err := cc.Call(context.Background(), &ChatRequest{Messages: []Message{{Role: RoleUser, Content: "hi"}}})
if err != nil {
t.Fatal(err)
}
if res.Usage.PromptTokens != 0 {
t.Errorf("PromptTokens = %d, want 0 (prompt 1000 = hit 750 + miss 250; every token is cache-accounted)", res.Usage.PromptTokens)
}
if res.Usage.CacheReadTokens != 750 {
t.Errorf("CacheReadTokens = %d, want 750", res.Usage.CacheReadTokens)
}
if res.Usage.CacheCreationTokens != 250 {
t.Errorf("CacheCreationTokens = %d, want 250", res.Usage.CacheCreationTokens)
}
if !res.Usage.CacheReported {
t.Error("CacheReported = false, want true (DeepSeek hit/miss present)")
}
}
func TestUsageCache_HostileCachedTokensNeverNegative(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, `{
"choices": [{"message": {"content": "ok"}, "finish_reason": "stop"}],
"usage": {
"prompt_tokens": 50,
"completion_tokens": 5,
"prompt_tokens_details": {"cached_tokens": 500}
}
}`)
}))
defer srv.Close()
cc := newTestClient(t, ProviderConfig{ID: "openai", Format: FormatOpenAI, BaseURL: srv.URL, APIKey: "k"}, srv)
res, err := cc.Call(context.Background(), &ChatRequest{Messages: []Message{{Role: RoleUser, Content: "hi"}}})
if err != nil {
t.Fatal(err)
}
if res.Usage.PromptTokens < 0 {
t.Errorf("PromptTokens = %d, must never go negative", res.Usage.PromptTokens)
}
if res.Usage.PromptTokens != 50 {
t.Errorf("PromptTokens = %d, want 50 (hostile cached_tokens skipped; no subtraction)", res.Usage.PromptTokens)
}
if !res.Usage.CacheReported {
t.Error("CacheReported = false, want true (details object present)")
}
}
func TestUsageCache_OpenAIFormatAnthropicCacheFields(t *testing.T) {
// Some OpenAI-compatible gateways forward Anthropic-named cache fields
// on the chat-completions usage object. They are exclusive — do not
// subtract from prompt_tokens.
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, `{
"choices": [{"message": {"content": "ok"}, "finish_reason": "stop"}],
"usage": {
"prompt_tokens": 500,
"completion_tokens": 50,
"cache_creation_input_tokens": 400,
"cache_read_input_tokens": 100
}
}`)
}))
defer srv.Close()
cc := newTestClient(t, ProviderConfig{ID: "openai", Format: FormatOpenAI, BaseURL: srv.URL, APIKey: "k"}, srv)
res, err := cc.Call(context.Background(), &ChatRequest{Messages: []Message{{Role: RoleUser, Content: "hi"}}})
if err != nil {
t.Fatal(err)
}
if res.Usage.PromptTokens != 500 {
t.Errorf("PromptTokens = %d, want 500 (Anthropic-named fields are exclusive)", res.Usage.PromptTokens)
}
if res.Usage.CacheCreationTokens != 400 || res.Usage.CacheReadTokens != 100 {
t.Errorf("cache fields = %d/%d, want 400/100", res.Usage.CacheCreationTokens, res.Usage.CacheReadTokens)
}
if !res.Usage.CacheReported {
t.Error("CacheReported = false, want true")
}
}