@@ -259,3 +259,202 @@ def test_normalise_works_on_pydantic_generated_schema() -> None:
259259 # Property titles stripped.
260260 for prop in out ["properties" ].values ():
261261 assert "title" not in prop
262+
263+
264+ # ---------------------------------------------------------------------------
265+ # Codex review #5 — input_tokens excludes cached_tokens (cache-hit accounting)
266+ # ---------------------------------------------------------------------------
267+
268+
269+ @pytest .mark .asyncio
270+ async def test_usage_subtracts_cached_tokens_from_input () -> None :
271+ """``input_tokens`` is the uncached portion; cached prefix reported
272+ separately, matching Responses/faux accounting (avoids double-count
273+ in cubebox cost aggregation)."""
274+ usage_chunk = SimpleNamespace (
275+ id = "x" ,
276+ choices = [],
277+ usage = SimpleNamespace (
278+ prompt_tokens = 1000 ,
279+ completion_tokens = 50 ,
280+ prompt_tokens_details = SimpleNamespace (cached_tokens = 800 ),
281+ ),
282+ )
283+ final = _make_chunk (finish_reason = "stop" )
284+
285+ with patch ("openai.AsyncOpenAI" ) as mock_openai :
286+ client = MagicMock ()
287+ mock_openai .return_value = client
288+ client .chat = MagicMock ()
289+ client .chat .completions = MagicMock ()
290+
291+ async def create (** _ ):
292+ return _async_iter ([usage_chunk , final ])
293+
294+ client .chat .completions .create = create
295+
296+ provider = OpenAIProvider (api_key = "x" )
297+ provider ._client = client
298+
299+ ms = await provider .stream (
300+ _model (),
301+ [UserMessage (content = [TextContent (text = "hi" )])],
302+ )
303+ async for _ in ms :
304+ pass
305+ msg = await ms .result ()
306+
307+ assert msg .usage .input_tokens == 200 # 1000 - 800
308+ assert msg .usage .cache_read_tokens == 800
309+ assert msg .usage .output_tokens == 50
310+
311+
312+ @pytest .mark .asyncio
313+ async def test_usage_no_cached_field_input_tokens_is_full_prompt () -> None :
314+ """No prompt_tokens_details → cached_tokens=0, input_tokens=prompt_tokens."""
315+ usage_chunk = SimpleNamespace (
316+ id = "x" ,
317+ choices = [],
318+ usage = SimpleNamespace (
319+ prompt_tokens = 200 ,
320+ completion_tokens = 10 ,
321+ prompt_tokens_details = None ,
322+ ),
323+ )
324+ final = _make_chunk (finish_reason = "stop" )
325+
326+ with patch ("openai.AsyncOpenAI" ) as mock_openai :
327+ client = MagicMock ()
328+ mock_openai .return_value = client
329+ client .chat = MagicMock ()
330+ client .chat .completions = MagicMock ()
331+
332+ async def create (** _ ):
333+ return _async_iter ([usage_chunk , final ])
334+
335+ client .chat .completions .create = create
336+
337+ provider = OpenAIProvider (api_key = "x" )
338+ provider ._client = client
339+
340+ ms = await provider .stream (
341+ _model (),
342+ [UserMessage (content = [TextContent (text = "hi" )])],
343+ )
344+ async for _ in ms :
345+ pass
346+ msg = await ms .result ()
347+
348+ assert msg .usage .input_tokens == 200
349+ assert msg .usage .cache_read_tokens == 0
350+
351+
352+ # ---------------------------------------------------------------------------
353+ # Codex review #6 — on_payload may opt out of stream_options.include_usage
354+ # ---------------------------------------------------------------------------
355+
356+
357+ @pytest .mark .asyncio
358+ async def test_on_payload_can_disable_include_usage () -> None :
359+ """Caller-supplied ``stream_options.include_usage=False`` must survive
360+ the default-inject step."""
361+ captured : dict [str , Any ] = {}
362+
363+ async def on_payload (payload , model ):
364+ payload ["stream_options" ] = {"include_usage" : False }
365+ return payload
366+
367+ with patch ("openai.AsyncOpenAI" ) as mock_openai :
368+ client = MagicMock ()
369+ mock_openai .return_value = client
370+ client .chat = MagicMock ()
371+ client .chat .completions = MagicMock ()
372+
373+ async def capture (** kwargs ):
374+ captured .update (kwargs )
375+ return _async_iter ([_make_chunk (finish_reason = "stop" )])
376+
377+ client .chat .completions .create = capture
378+
379+ provider = OpenAIProvider (api_key = "x" )
380+ provider ._client = client
381+
382+ ms = await provider .stream (
383+ _model (),
384+ [UserMessage (content = [TextContent (text = "hi" )])],
385+ options = StreamOptions (on_payload = on_payload ),
386+ )
387+ async for _ in ms :
388+ pass
389+ await ms .result ()
390+
391+ assert captured ["stream_options" ] == {"include_usage" : False }
392+
393+
394+ @pytest .mark .asyncio
395+ async def test_default_adds_include_usage_when_not_set () -> None :
396+ """Without on_payload customization, default adds include_usage=True."""
397+ captured : dict [str , Any ] = {}
398+ with patch ("openai.AsyncOpenAI" ) as mock_openai :
399+ client = MagicMock ()
400+ mock_openai .return_value = client
401+ client .chat = MagicMock ()
402+ client .chat .completions = MagicMock ()
403+
404+ async def capture (** kwargs ):
405+ captured .update (kwargs )
406+ return _async_iter ([_make_chunk (finish_reason = "stop" )])
407+
408+ client .chat .completions .create = capture
409+
410+ provider = OpenAIProvider (api_key = "x" )
411+ provider ._client = client
412+
413+ ms = await provider .stream (
414+ _model (),
415+ [UserMessage (content = [TextContent (text = "hi" )])],
416+ )
417+ async for _ in ms :
418+ pass
419+ await ms .result ()
420+
421+ assert captured ["stream_options" ] == {"include_usage" : True }
422+
423+
424+ # ---------------------------------------------------------------------------
425+ # Codex review #7 — preserve sibling metadata when $ref-resolving
426+ # ---------------------------------------------------------------------------
427+
428+
429+ def test_normalise_ref_resolution_preserves_sibling_description () -> None :
430+ """Pydantic emits ``$ref`` alongside ``description`` / ``default`` on
431+ Optional[Enum] fields; both must survive inlining."""
432+ schema = {
433+ "$defs" : {"Color" : {"title" : "Color" , "enum" : ["r" , "g" ], "type" : "string" }},
434+ "properties" : {
435+ "c" : {
436+ "$ref" : "#/$defs/Color" ,
437+ "description" : "primary colour" ,
438+ "default" : "r" ,
439+ }
440+ },
441+ }
442+ out = OpenAIProvider ._normalise_tool_schema (schema )
443+ resolved = out ["properties" ]["c" ]
444+ assert resolved ["enum" ] == ["r" , "g" ]
445+ assert resolved ["description" ] == "primary colour"
446+ assert resolved ["default" ] == "r"
447+
448+
449+ def test_normalise_ref_resolution_resolved_def_wins_on_key_collision () -> None :
450+ """If both $def and sibling supply the same key, the $def value wins
451+ (sibling is treated as additional metadata, not an override)."""
452+ schema = {
453+ "$defs" : {"X" : {"type" : "string" , "description" : "from def" }},
454+ "properties" : {
455+ "x" : {"$ref" : "#/$defs/X" , "description" : "from sibling" },
456+ },
457+ }
458+ out = OpenAIProvider ._normalise_tool_schema (schema )
459+ # setdefault means the def's value sticks.
460+ assert out ["properties" ]["x" ]["description" ] == "from def"
0 commit comments