11import { describe , expect , test } from 'bun:test'
2- import { buildResponsesRequest } from '../responsesAdapter.js'
2+ import { buildResponsesRequest , extractUsage } from '../responsesAdapter.js'
3+ import { formatOpenAIPromptCacheKey } from '../openaiShared.js'
4+ import { calculateCacheHitRate } from '../../../../utils/cacheWarning.js'
35
46describe ( 'buildResponsesRequest' , ( ) => {
7+ test ( 'includes max reasoning effort for ChatGPT Responses requests' , ( ) => {
8+ const request = buildResponsesRequest ( {
9+ model : 'gpt-5.6-sol' ,
10+ messages : [ { role : 'user' , content : 'hello' } ] ,
11+ tools : [ ] ,
12+ toolChoice : undefined ,
13+ reasoningEffort : 'max' ,
14+ } )
15+
16+ expect ( request . reasoning ) . toEqual ( { effort : 'max' } )
17+ } )
18+
519 test ( 'includes reasoning effort for ChatGPT Responses requests' , ( ) => {
620 const request = buildResponsesRequest ( {
721 model : 'gpt-5.5' ,
@@ -24,4 +38,146 @@ describe('buildResponsesRequest', () => {
2438
2539 expect ( 'max_output_tokens' in request ) . toBe ( false )
2640 } )
41+
42+ test ( 'includes stable prompt_cache_key for session-sticky cache routing' , ( ) => {
43+ const key = formatOpenAIPromptCacheKey ( 'session-abc-123' )
44+ const request = buildResponsesRequest ( {
45+ model : 'gpt-5.6-sol' ,
46+ messages : [ { role : 'user' , content : 'hello' } ] ,
47+ tools : [ ] ,
48+ toolChoice : undefined ,
49+ promptCacheKey : key ,
50+ } )
51+
52+ expect ( request . prompt_cache_key ) . toBe ( 'ccb:session-abc-123' )
53+ } )
54+
55+ test ( 'defaults prompt_cache_key to process-stable fallback when not overridden' , ( ) => {
56+ const request = buildResponsesRequest ( {
57+ model : 'gpt-5.5' ,
58+ messages : [ { role : 'user' , content : 'hello' } ] ,
59+ tools : [ ] ,
60+ toolChoice : undefined ,
61+ } )
62+ const again = buildResponsesRequest ( {
63+ model : 'gpt-5.5' ,
64+ messages : [ { role : 'user' , content : 'next' } ] ,
65+ tools : [ ] ,
66+ toolChoice : undefined ,
67+ } )
68+
69+ expect ( request . prompt_cache_key ) . toMatch ( / ^ c c b : [ 0 - 9 a - f - ] + $ / i)
70+ expect ( again . prompt_cache_key ) . toBe ( request . prompt_cache_key )
71+ } )
72+
73+ test ( 'prompt_cache_key is stable across turns (not derived from messages)' , ( ) => {
74+ const key = formatOpenAIPromptCacheKey ( 'same-session' )
75+ const turn1 = buildResponsesRequest ( {
76+ model : 'gpt-5.5' ,
77+ messages : [ { role : 'user' , content : 'first' } ] ,
78+ tools : [ ] ,
79+ toolChoice : undefined ,
80+ promptCacheKey : key ,
81+ } )
82+ const turn2 = buildResponsesRequest ( {
83+ model : 'gpt-5.5' ,
84+ messages : [
85+ { role : 'user' , content : 'first' } ,
86+ { role : 'assistant' , content : 'ok' } ,
87+ { role : 'user' , content : 'second' } ,
88+ ] ,
89+ tools : [ ] ,
90+ toolChoice : undefined ,
91+ promptCacheKey : key ,
92+ } )
93+
94+ expect ( turn1 . prompt_cache_key ) . toBe ( turn2 . prompt_cache_key )
95+ expect ( turn1 . prompt_cache_key ) . toBe ( 'ccb:same-session' )
96+ } )
97+ } )
98+
99+ describe ( 'extractUsage (OpenAI Responses → Anthropic usage)' , ( ) => {
100+ test ( 'subtracts cached_tokens so hit rate uses OpenAI total as denominator' , ( ) => {
101+ const usage = extractUsage ( {
102+ usage : {
103+ input_tokens : 30_000 ,
104+ output_tokens : 100 ,
105+ input_tokens_details : { cached_tokens : 20_000 } ,
106+ } ,
107+ } )
108+
109+ expect ( usage ) . toEqual ( {
110+ input_tokens : 10_000 ,
111+ output_tokens : 100 ,
112+ cache_creation_input_tokens : 0 ,
113+ cache_read_input_tokens : 20_000 ,
114+ } )
115+
116+ // Was 40% under the double-count bug; correct is 66.7%.
117+ const hitRate = calculateCacheHitRate ( usage )
118+ expect ( hitRate ) . toBeCloseTo ( ( 20_000 / 30_000 ) * 100 , 5 )
119+ } )
120+
121+ test ( 'full cache hit can report 100% (not capped at 50%)' , ( ) => {
122+ const usage = extractUsage ( {
123+ usage : {
124+ input_tokens : 30_000 ,
125+ output_tokens : 50 ,
126+ input_tokens_details : { cached_tokens : 30_000 } ,
127+ } ,
128+ } )
129+
130+ expect ( usage . input_tokens ) . toBe ( 0 )
131+ expect ( usage . cache_read_input_tokens ) . toBe ( 30_000 )
132+ expect ( calculateCacheHitRate ( usage ) ) . toBe ( 100 )
133+ } )
134+
135+ test ( 'maps cache_write_tokens to cache_creation without double-counting total' , ( ) => {
136+ const usage = extractUsage ( {
137+ usage : {
138+ input_tokens : 10_000 ,
139+ output_tokens : 10 ,
140+ input_tokens_details : {
141+ cached_tokens : 6_000 ,
142+ cache_write_tokens : 2_000 ,
143+ } ,
144+ } ,
145+ } )
146+
147+ expect ( usage ) . toEqual ( {
148+ input_tokens : 2_000 ,
149+ output_tokens : 10 ,
150+ cache_creation_input_tokens : 2_000 ,
151+ cache_read_input_tokens : 6_000 ,
152+ } )
153+ // segments sum to OpenAI total
154+ expect (
155+ usage . input_tokens +
156+ usage . cache_creation_input_tokens +
157+ usage . cache_read_input_tokens ,
158+ ) . toBe ( 10_000 )
159+ expect ( calculateCacheHitRate ( usage ) ) . toBeCloseTo ( 60 , 5 )
160+ } )
161+
162+ test ( 'clamps overlapping write/read that exceed total input' , ( ) => {
163+ const usage = extractUsage ( {
164+ usage : {
165+ input_tokens : 5_000 ,
166+ output_tokens : 0 ,
167+ input_tokens_details : {
168+ cached_tokens : 4_000 ,
169+ cache_write_tokens : 4_000 ,
170+ } ,
171+ } ,
172+ } )
173+
174+ expect (
175+ usage . input_tokens +
176+ usage . cache_creation_input_tokens +
177+ usage . cache_read_input_tokens ,
178+ ) . toBe ( 5_000 )
179+ expect ( usage . cache_read_input_tokens ) . toBe ( 4_000 )
180+ expect ( usage . cache_creation_input_tokens ) . toBe ( 1_000 )
181+ expect ( usage . input_tokens ) . toBe ( 0 )
182+ } )
27183} )
0 commit comments