Skip to content

fix(provider): keep direct-service keys off receipt lookups - #1894

Merged
AbirAbbas merged 3 commits into
devfrom
fix/overflow-receipt-bearer
Oct 22, 2026
Merged

AbirAbbas merged 3 commits into
devfrom
fix/overflow-receipt-bearer

Conversation

@AbirAbbas

@AbirAbbas AbirAbbas commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

What was true

Client.settle recovers a late OpenRouter receipt for a torn, cut or unmetered streamed answer. Its direct-service guard returned early unless the attempt's base classified as OpenRouter, in which case fetchReceiptAt sent the direct service's own key to openrouter.ai/.../generation. #1887's TestLateReceiptFollowsTheOverflowRoute asserted that behaviour.

What is true now

A direct client returns before any receipt lookup, whatever the attempt base. Ordinary usage billing still runs first, and the router road's late-receipt recovery is unchanged.

Reach

No shipped configuration reached the old path. Every shipped overflow door (Z.ai, Moonshot and Qwen pay-as-you-go) belongs to the same vendor as the configured service, so the attempt base never classified as OpenRouter for a direct client. This closes the guard before any future door could open it.

Tests (from the contract, fake transports recording host and bearer)

  • TestV1V3DirectLateReceiptNeverUsesAnOverflowHost: a direct client on its configured base, its metered door, and an OpenRouter attempt base, for torn, stalled and unmetered endings with and without usage. It makes no lookup and banks nothing new; a reported-usage answer is billed exactly once, as before.
  • TestV2RouterTornAnswerRecoversWithItsOwnBearer: a torn router answer is still recovered from openrouter.ai with the router's own bearer: two requests, the same recovered price and tokens, and one recovered usage event.
  • Before the fix, the three missing-usage OpenRouter cases failed with GET /api/v1/generation carrying Bearer vendor-key. After it, they pass.

Gates: gofmt, go vet ./internal/provider/, go test -count=1 for provider, telemetry and config, make test-laws, and make pr-ready.

Follow-ups found by the audit, not changed here

Both can change behaviour a person sees, so they need their own decision:

  • Video polling (internal/provider/media.go ~252/284/387): a provider-supplied absolute polling_url is fetched with the client's bearer whatever its host. Downloads already check sameHostAsBase; polling does not.
  • Redirects: the default HTTP clients keep Go's redirect policy, which forwards Authorization to subdomains of the original host. Credential-free probes already refuse redirects.

Also in this PR

  • receiptResult's five-second wait for the receipt worker is now a thirty-second hang ceiling. Under shared-box load it was exceeded by two receipt tests that pass in a tenth of a second alone; a passing test still returns the moment the worker reports. Test-only.

🤖 Generated with Claude Code

AbirAbbas and others added 3 commits October 22, 2026 11:40
A direct service's late receipt recovery used to follow an attempt base
that classified as OpenRouter, sending that service's own key to the
router's generation route. Direct clients now stop before any receipt
lookup, whatever the attempt base; ordinary usage billing still runs first,
and the router road recovers its receipts exactly as before.

The overflow receipt test asserted the old behaviour, so it is replaced by
a direct-service test across the configured base, its metered door and an
OpenRouter attempt, and a torn router answer that records each request's
host and bearer.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
receiptResult gave the worker five seconds of wall clock to report. Under a
shared box's load (average twenty on eight cores) that budget was exceeded
twice by tests that pass in a tenth of a second alone:
TestLateReceiptFollowsTheOverflowRoute and
TestK10TrimmedGenerationPairsRecoveryAndBlankUsesAttempt. The ceiling is a
hang detector, so it moves to thirty seconds; a passing test still returns
the moment the worker reports.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@AbirAbbas
AbirAbbas merged commit bc37d44 into dev Oct 22, 2026
5 checks passed
@AbirAbbas
AbirAbbas deleted the fix/overflow-receipt-bearer branch October 22, 2026 16:02
@AbirAbbas

Copy link
Copy Markdown
Collaborator Author

The two follow-ups above are filed with stub replications: #1895 (video polling sends the key to any polling host) and #1896 (redirects to subdomains and other ports keep the key).

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