Skip to content

Commit 1a71c2b

Browse files
Fix 0083 zero-bound to exceed per section 5.5.15 (#236)
A declared token budget bound of 0 was skipped for both metrics and emitted no exceeded signal. Per observability section 5.5.15 the exceeded test is a strict actual > max, so a 0 bound is exceeded by any positive usage: the exceeded span attribute and the exceeded counter must fire. Only the utilization histogram sample is skipped, since actual / 0 has no defined ratio. Narrow the evaluation helper to include a 0 bound (gate on is-not-None, not truthy) and move the divide-by-zero guard to the utilization record site alone.
1 parent b25f67c commit 1a71c2b

2 files changed

Lines changed: 71 additions & 7 deletions

File tree

src/openarmature/observability/otel/observer.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -220,18 +220,20 @@ def _token_budget_evaluations(token_budget: Any, usage: Any) -> list[dict[str, A
220220
# DECLARED bound that also has a usable actual to compare against, so a bound
221221
# whose count the provider did not report (None) is omitted -- mirroring the
222222
# token.usage gate rather than coercing a missing count to 0. A declared
223-
# bound of 0 is degenerate (ratio undefined) and is skipped for both
224-
# instruments; its declared value still surfaces as a prompt span attribute.
225-
# Empty when there is no budget, no usage, or no evaluable bound; the caller
226-
# then leaves the exceeded signal absent (not false).
223+
# bound of 0 IS evaluated: the exceeded test is a strict ``actual > max``, so
224+
# a 0 bound is exceeded by any positive usage per §5.5.15; only the
225+
# utilization ratio is undefined for a 0 denominator, and the metric recorder
226+
# skips that one sample. Empty when there is no budget, no usage, or no bound
227+
# with a reported count; the caller then leaves the exceeded signal absent
228+
# (not false).
227229
if token_budget is None or usage is None:
228230
return []
229231
evaluations: list[dict[str, Any]] = []
230232
input_max = getattr(token_budget, "input_max_tokens", None)
231-
if input_max and usage.prompt_tokens is not None:
233+
if input_max is not None and usage.prompt_tokens is not None:
232234
evaluations.append({"kind": "input", "actual": usage.prompt_tokens, "max": input_max})
233235
total_max = getattr(token_budget, "total_max_tokens", None)
234-
if total_max:
236+
if total_max is not None:
235237
if usage.total_tokens is not None:
236238
total_actual = usage.total_tokens
237239
elif usage.prompt_tokens is not None and usage.completion_tokens is not None:
@@ -1518,7 +1520,11 @@ def _record_llm_metrics(self, event: LlmRetryAttemptEvent) -> None:
15181520
):
15191521
for ev in _token_budget_evaluations(event.token_budget, usage):
15201522
dims = {**base_dims, "openarmature.gen_ai.token_budget.kind": ev["kind"]}
1521-
self._token_budget_utilization_histogram.record(ev["actual"] / ev["max"], dims)
1523+
# The utilization ratio is undefined for a 0 denominator; skip
1524+
# that one sample. A 0 bound is still counted as exceeded below
1525+
# (strict actual > max), per §5.5.15.
1526+
if ev["max"] > 0:
1527+
self._token_budget_utilization_histogram.record(ev["actual"] / ev["max"], dims)
15221528
if ev["actual"] > ev["max"]:
15231529
self._token_budget_exceeded_counter.add(1, dims)
15241530

tests/unit/test_observability_otel.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1419,6 +1419,64 @@ async def test_token_budget_missing_input_count_omits_evaluation() -> None:
14191419
assert [p for p in points if "token_budget" in p[0]] == []
14201420

14211421

1422+
async def test_token_budget_zero_bound_exceeds_but_skips_utilization() -> None:
1423+
# §5.5.15 (proposal 0083): a declared bound of 0 is exceeded by any positive
1424+
# usage (the exceeded test is a strict actual > max), so the exceeded span
1425+
# attr + counter fire. The utilization ratio is undefined for a 0 denominator,
1426+
# so that one histogram sample is skipped -- not a fabricated sentinel.
1427+
from openarmature.llm.response import Usage
1428+
from openarmature.prompts import TokenBudget
1429+
from tests._helpers.typed_event import make_retry_attempt_event
1430+
1431+
event = make_retry_attempt_event(
1432+
model="test-model",
1433+
provider="openai",
1434+
usage=Usage(prompt_tokens=5, completion_tokens=1, total_tokens=6),
1435+
token_budget=TokenBudget(input_max_tokens=0),
1436+
)
1437+
points, llm_spans = await _drive_metrics_events([event])
1438+
1439+
attrs: dict[str, Any] = dict(llm_spans[0].attributes or {})
1440+
assert attrs.get("openarmature.prompt.token_budget.input_max_tokens") == 0
1441+
assert attrs.get("openarmature.llm.token_budget.exceeded") is True
1442+
1443+
exceeded = [p for p in points if p[0] == _TB_EXCEEDED]
1444+
util = [p for p in points if p[0] == _TB_UTILIZATION]
1445+
assert len(exceeded) == 1
1446+
assert exceeded[0][1] == 1
1447+
assert exceeded[0][3]["openarmature.gen_ai.token_budget.kind"] == "input"
1448+
# No utilization sample -- the 0-denominator ratio is undefined and skipped.
1449+
assert util == []
1450+
1451+
1452+
async def test_token_budget_zero_total_bound_exceeds_but_skips_utilization() -> None:
1453+
# §5.5.15 (proposal 0083): the total branch mirrors the input branch -- a
1454+
# total_max of 0 is exceeded by any positive total usage (exceeded attr +
1455+
# counter fire, kind total), and the undefined 0-denominator utilization
1456+
# sample is skipped.
1457+
from openarmature.llm.response import Usage
1458+
from openarmature.prompts import TokenBudget
1459+
from tests._helpers.typed_event import make_retry_attempt_event
1460+
1461+
event = make_retry_attempt_event(
1462+
model="test-model",
1463+
provider="openai",
1464+
usage=Usage(prompt_tokens=5, completion_tokens=1, total_tokens=6),
1465+
token_budget=TokenBudget(total_max_tokens=0),
1466+
)
1467+
points, llm_spans = await _drive_metrics_events([event])
1468+
1469+
attrs: dict[str, Any] = dict(llm_spans[0].attributes or {})
1470+
assert attrs.get("openarmature.prompt.token_budget.total_max_tokens") == 0
1471+
assert attrs.get("openarmature.llm.token_budget.exceeded") is True
1472+
1473+
exceeded = [p for p in points if p[0] == _TB_EXCEEDED]
1474+
util = [p for p in points if p[0] == _TB_UTILIZATION]
1475+
assert len(exceeded) == 1
1476+
assert exceeded[0][3]["openarmature.gen_ai.token_budget.kind"] == "total"
1477+
assert util == []
1478+
1479+
14221480
async def test_llm_span_zero_duration_when_latency_missing() -> None:
14231481
# When the typed event omits latency_ms (None), the handler falls
14241482
# back to a zero-duration span at end_time rather than guessing

0 commit comments

Comments
 (0)