Skip to content

Close billing and webhook gaps from the security audit - #153

Merged
skyfallwastaken merged 2 commits into
neofrom
fix/billing-audit
Sep 27, 2026
Merged

skyfallwastaken merged 2 commits into
neofrom
fix/billing-audit

Conversation

@skyfallwastaken

Copy link
Copy Markdown
Member

Fixes from a security audit of the gateway. Each fix has a regression test.

Billing

  • Fallback models priced. The proxy reserved only for body.model, so a :free model with paid models fallbacks ran on a $0 hold. Every model in models must now be listed, and the hold covers the priciest. Unpriced extras (:online, plugins, web_search_options, server-side tools) add the unknown-model hold.
  • Right output cap. /responses now reads only max_output_tokens. Chat Completions takes the larger of max_tokens / max_completion_tokens, and 0 means no cap. Previously max_tokens: 1 could price a request that generated up to the model maximum.
  • Abandoned requests are billed. Billed upstream calls no longer take the client's abort signal, because aborting before the response lost the generation id and released the hold unbilled.
    • OpenRouter: the proxy cancels the response itself when the client has gone, so OpenRouter stops the generation and reconciliation bills the partial tokens.
    • OCR, Exa and Jev finish and are charged.
  • Long streams keep their hold. A response still being read at half its reservation's lifetime moves to pending_reconciliation, so the expiry sweeper can't release a hold the request still owes.
  • Exa / Mistral OCR / TypeSafe. A 2xx without a readable cost now charges the hold (usage_source = fallback). These providers have no reconciliation lookup, so it used to be released uncharged.

Other

  • GitHub secret scanning. Signatures are verified as DER with node:crypto. WebCrypto's ECDSA verify rejected every real alert, so leaked keys were never revoked. The test now checks GitHub's published sample.
  • Replicate uploads. Uploads still in flight count toward the daily quota, so parallel uploads can't exceed it.
  • Framing. frame-ancestors 'none' on pages, so /keys can't be framed.

bun run typecheck and bun test (327 pass) are clean.

馃 Generated with Claude Code

@skyfallwastaken

Copy link
Copy Markdown
Member Author

@greptileapps review

@skyfallwastaken

Copy link
Copy Markdown
Member Author

@greptile review

@skyfallwastaken
skyfallwastaken merged commit 9f90397 into neo Sep 27, 2026
3 checks passed
@skyfallwastaken
skyfallwastaken deleted the fix/billing-audit branch September 27, 2026 13:32
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