oc_chat
Loading...
Searching...
No Matches
usage.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# providers/openai_compat/usage.py
4
5"""
6Usage-block normalisation across the OpenAI-compatible dialect variants.
7
8Purpose
9-------
10:func:`normalise_usage` folds the ``usage`` block every lane returns into
11one :class:`UsageStats`. The lanes all claim the OpenAI chat-completions
12dialect, but they differ in which optional sub-blocks they populate:
13Groq and Cerebras report ``completion_tokens_details.reasoning_tokens``;
14OpenRouter adds a top-level ``cost`` (USD, ignored here — pricing is
15``oc_pricing``'s job) and may report ``prompt_tokens_details``; Bitdeer
16and NVIDIA NIM report neither and leave the optional blocks absent.
17
18Responsibilities
19----------------
20- :func:`normalise_usage` — total, defensive dict → :class:`UsageStats`.
21- :func:`_as_dict` — coerce a pydantic sub-block (or anything else) to a
22 plain dict so ``isinstance(x, dict)`` checks in callers hold.
23- :func:`_as_int` — coerce a reported count to a non-negative int.
24
25Diagnostics
26-----------
27Domain: OC_CHAT.CLIENT
28Levels:
29 L2 — lifecycle events
30 L3 — semantic details
31 L4 — deep tracing
32
33Contracts
34---------
35- Total: any input — ``None``, a non-dict, a partial block, a block with
36 string counts — yields a :class:`UsageStats` rather than raising. A
37 usage block is telemetry; a malformed one must never abort a turn.
38- Absent fields normalise to 0, which callers read as "no data" (the
39 contract's own zero-hit-zero-miss convention).
40- ``total_tokens`` is derived as prompt+completion ONLY when the lane did
41 not report it; a reported total is never overwritten.
42- ``cache_hit_tokens`` comes from ``prompt_tokens_details.cached_tokens``
43 (the OpenAI-standard prefix-cache field). The miss side is the reported
44 prompt total minus the hit, floored at 0 — never negative on a lane
45 that reports a cached count larger than its own prompt count.
46- Money is NOT parsed here. ``usage.cost`` (OpenRouter/Vercel) is
47 deliberately dropped: plan 202's fail-closed pricing path owns cost,
48 and a provider-asserted figure entering by this door would bypass it.
49
50Dependencies
51------------
52- oc_diagnostics (structured debug logging via _dbg)
53- option_c_agent_contracts (UsageStats)
54"""
55from __future__ import annotations
56
57from typing import Any
58
59from oc_diagnostics import _dbg
60
61from option_c_agent_contracts import UsageStats
62
63_dbg("OC_CHAT.CLIENT", "module", "loaded", 4)
64
65
66def _as_dict(value: Any) -> dict:
67 """Return *value* as a plain dict, or ``{}`` when it is not dict-like."""
68 if isinstance(value, dict):
69 return value
70 if hasattr(value, "model_dump"):
71 dumped = value.model_dump()
72 return dumped if isinstance(dumped, dict) else {}
73 return {}
74
75
76def _as_int(value: Any) -> int:
77 """Return *value* as a non-negative int, or 0 when it is not a count."""
78 func = "_as_int"
79 if value is None or isinstance(value, bool):
80 return 0
81 try:
82 return max(0, int(value))
83 except (TypeError, ValueError) as exc:
84 # VALIDATION_ERROR — a lane reported a non-numeric count; telemetry
85 # degrades to zero rather than aborting the turn.
86 _dbg("OC_CHAT.CLIENT", func,
87 f"VALIDATION_ERROR: {value!r}: {type(exc).__name__}: {exc}", 3)
88 return 0
89
90
91def normalise_usage(usage: Any) -> UsageStats:
92 """Fold a lane's ``usage`` block into :class:`UsageStats`.
93
94 Handles the dialect variants named in the module docstring; unknown
95 extra keys are ignored, so a lane that grows a field does not break
96 this parser.
97 """
98 func = "normalise_usage"
99 block = _as_dict(usage)
100 if not block:
101 return UsageStats()
102
103 prompt = _as_int(block.get("prompt_tokens"))
104 completion = _as_int(block.get("completion_tokens"))
105 reported_total = _as_int(block.get("total_tokens"))
106 total = reported_total or (prompt + completion)
107
108 completion_details = _as_dict(block.get("completion_tokens_details"))
109 prompt_details = _as_dict(block.get("prompt_tokens_details"))
110
111 reasoning = _as_int(completion_details.get("reasoning_tokens"))
112 cached = _as_int(prompt_details.get("cached_tokens"))
113 hit = min(cached, prompt) if prompt else cached
114 miss = max(0, prompt - hit)
115
116 _dbg("OC_CHAT.CLIENT", func,
117 f"prompt={prompt} completion={completion} total={total} "
118 f"reasoning={reasoning} cache_hit={hit}", 4)
119 return UsageStats(
120 prompt_tokens=prompt,
121 completion_tokens=completion,
122 total_tokens=total,
123 # Zero hit AND zero miss is the contract's "no cache data" signal, so a
124 # lane with no cached count must not report the whole prompt as a miss.
125 cache_hit_tokens=hit if cached else 0,
126 cache_miss_tokens=miss if cached else 0,
127 reasoning_tokens=reasoning,
128 )
129
130
131__all__ = ["normalise_usage"]
UsageStats normalise_usage(Any usage)
Definition usage.py:91
dict _as_dict(Any value)
Definition usage.py:66
int _as_int(Any value)
Definition usage.py:76