Skip to content

fix(adapters)!: require positive evidence for Anthropic detection - #23

Merged
mizcausevic-dev merged 3 commits into
mainfrom
fix/anthropic-detect-positive-evidence
Sep 13, 2026
Merged

mizcausevic-dev merged 3 commits into
mainfrom
fix/anthropic-detect-positive-evidence

Conversation

@mizcausevic-dev

Copy link
Copy Markdown
Owner

Summary

Branch 2 of the review's fix plan. Breaking, ships in the same 0.3.0 release as #22 (based on top of it, targets that branch rather than main).

src/adapters.ts's anthropic detect() ended in a catch-all (r.object !== "response"), making anthropic the default adapter for any bare { input_tokens, output_tokens } shape, since Anthropic's Messages API and OpenAI's Responses/Agents usage report those under identical key names. v0.1.1 fixed the marker-present direction; the marker-absent direction silently mislabeled things, including exactly the OpenAI Agents SDK's usage object shape (no top-level object field at all), as Anthropic, dropping cache and reasoning tokens with no error.

A note on scope, since this fix has a real side effect the review calls out explicitly (its own "Hazard B"): the existing test normalizes an Anthropic response passed a bare { input_tokens: 20, output_tokens: 8 } with no marker of any kind, exactly the shape that must now throw. That test is converted to assert the new throw rather than weakened back to a catch-all; a separate, already-passing test (still normalizes a genuine Anthropic response carrying type: message) covers the positive case. This is a deliberate, documented behavior change, not a silent regression, see the CHANGELOG entry and the --provider anthropic remedy named in it.

  • adapters.ts: anthropic detect() requires type: "message"/"message_start", or a cache field. openai detect() additionally treats input_tokens_details/output_tokens_details as a hard signal on its own, those key names don't exist in Anthropic's API.
  • adapters.ts, normalize.ts: detect() guards switched to Object.hasOwn; the provider lookup table is now a null-prototype object with an explicit error, closing a __proto__-as-provider-name path. No reachable pollution sink exists in this package today, this is hygiene for a library embedded in someone else's process, not a live vulnerability here.
  • Tests: converted the bare-Anthropic test to assert the new throw, added the exact marker-less OpenAI Agents/Responses reproduction from the review as a regression test, added a __proto__-guard test. Fixed fixtures/responses.jsonl's bare Anthropic record (added type: "message") so the normalizeMany batch test still succeeds on it.
  • README.md, CHANGELOG.md: documented the new detection rule and the behavior change plainly.

Test plan

  • npm ci
  • npm run lint
  • npm run typecheck
  • npm run coverage — 27/27 tests
  • npm run build
  • npm run demo
  • npm audit --audit-level=high

🤖 Generated with Claude Code

mizcausevic-dev and others added 2 commits September 13, 2026 17:53
…ache tokens

BREAKING CHANGE: inputTokens changes value for any Anthropic or Bedrock
response that reports cache tokens.

Anthropic and AWS both document input_tokens/inputTokens as excluding
cache tokens (total = cache_read + cache_creation + input_tokens, per
each vendor's own prompt-caching docs). This package's own README and
types.ts already claimed cache tokens are "already included in
inputTokens" for every provider, true for OpenAI, false for Anthropic
and Bedrock, whose input_tokens was passed straight through unchanged.
The OpenTelemetry GenAI semantic conventions this package targets also
say the total input count MAY be published regardless of caching.

Every 0.2.x version undercounted inputTokens on cache-heavy Anthropic
and Bedrock traffic. See CHANGELOG for the full note.

- adapters.ts: anthropic and bedrock extract() now add
  cache_read/cache_creation (Anthropic) and cacheReadInputTokens/
  cacheWriteInputTokens (Bedrock) into inputTokens. openai untouched,
  already correct. gemini untouched, candidatesTokenCount/
  thoughtsTokenCount inclusion is not stated in Google's docs and is
  gated on a live API check not performed in this branch.
- types.ts: cacheWriteTokens doc comment corrected, Bedrock also has
  cache-write, not Anthropic-only.
- test/normalize.test.ts: replaced the input_tokens:20/cache_read:15
  fixture (15+5=20 made the wrong reading numerically indistinguishable
  from the right one) with Anthropic's own doc-example shape, added a
  golden-invariant test per provider, doubled fixtures/responses.jsonl
  with cache/reasoning-bearing records so normalizeMany exercises the
  previously-untested path.
- README, CHANGELOG: documented the vendor asymmetry and the fix.
- package.json: 0.3.0 (not tagged, not published).

Verified locally: npm ci, lint, typecheck, coverage (25/25 tests),
build, demo, audit --audit-level=high all pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
BREAKING CHANGE: a bare { input_tokens, output_tokens } payload with no
provider marker on either side now throws instead of defaulting to
Anthropic.

Anthropic's Messages API and OpenAI's Responses/Agents usage report
input_tokens/output_tokens under identical key names. v0.1.1 fixed
detection for the case where OpenAI's object: "response" marker is
present; the fallback direction, no marker either way, still defaulted
to anthropic via a catch-all (r.object !== "response"). This silently
mislabeled any marker-less OpenAI Responses/Agents usage object
(exactly what the OpenAI Agents SDK's usage object looks like, no
top-level object field at all) as Anthropic, dropping its cache and
reasoning tokens with no error and pointing cost tracking at the wrong
price table. Same failure class independently found in a different
project's normalizer during this review.

- adapters.ts: anthropic detect() now requires type: "message"/
  "message_start", or a cache_read_input_tokens/cache_creation_input_tokens
  field. openai detect() additionally treats input_tokens_details/
  output_tokens_details as a hard signal on its own, Anthropic never
  emits those key names, so their presence identifies the shape even
  without the Responses API envelope.
- adapters.ts, normalize.ts: detect() guards use Object.hasOwn instead
  of the `in` operator; the provider lookup table is a null-prototype
  object with an explicit "not a supported adapter" error, closing a
  __proto__-as-provider-name path (no reachable pollution sink exists
  today, but this is a library embedded in someone else's process).
- test/normalize.test.ts: the old bare-Anthropic test asserted the
  removed catch-all; converted to assert the new throw (a genuine
  Anthropic response with type: "message" is already covered by a
  separate, still-passing test). Added a regression test for the exact
  marker-less OpenAI Agents/Responses reproduction from the review, and
  a test for the __proto__ guard. fixtures/responses.jsonl's bare
  Anthropic record now carries type: "message" so normalizeMany's
  batch test still succeeds on it.
- README, CHANGELOG: documented the new detection rule and the
  behavior change plainly, with the --provider remedy named.

Verified locally: npm ci, lint, typecheck, coverage (27/27 tests),
build, demo, audit --audit-level=high all pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@mizcausevic-dev
mizcausevic-dev changed the base branch from fix/token-inclusion-semantics to main September 13, 2026 22:04
@mizcausevic-dev
mizcausevic-dev merged commit b154192 into main Sep 13, 2026
4 checks passed
@mizcausevic-dev
mizcausevic-dev deleted the fix/anthropic-detect-positive-evidence branch September 13, 2026 22:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant