Skip to content

Charge requests whose client hangs up before the answer ends - #20

Closed
Marketen wants to merge 1 commit into
mainfrom
fix/charge-abandoned-requests
Closed

Marketen wants to merge 1 commit into
mainfrom
fix/charge-abandoned-requests

Conversation

@Marketen

@Marketen Marketen commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

Why

A request is charged when the provider reports its usage, at the end of the answer. When the client hung up earlier, the gateway cut the provider off, no usage arrived, and Metering released the hold without a charge. The provider still billed us for what it had generated. Anyone could get answers for free by disconnecting just before the end, on any account. Guest chat (#18) would make that anonymous.

Providers have no API to look up what one cancelled request cost, so the only way to charge the exact amount is to let the answer finish.

What changes

  • Once credit is reserved, the provider call no longer follows the client's connection (Proxy). The relay keeps reading to the end, and the usage the provider reports is charged, for every account.
  • Pressing Stop no longer lowers the bill. The request's max_tokens (or the model maximum) bounds it, and the hold already covers it.
  • A stalled provider still ends: the provider client's existing idle timeout (PROVIDER_SILENCE_TIMEOUT_SECONDS, 15 min) applies.
  • A client that hung up gets no fallback answer.
  • The handler's 5-second post-finish drain is gone; it's covered by the above.

Provider faults are unchanged: what the provider reports is charged (see dappnode-nexus#152), otherwise nothing.

Tests

  • TestStreamingUsageRetention, a real HTTP run through the production OpenAI adapter: the client hangs up after the first chunk, the provider finishes, and usage is charged. It previously asserted the opposite.
  • New proxy_hangup_test.go: hang-up mid-stream, hang-up during a non-streaming answer, no fallback after a hang-up. Each fails with its part of the fix removed.
  • go test -race ./... and go vet ./... pass.

🤖 Generated with Claude Code

A request is charged when the provider reports its usage, which it does at
the end of the answer. When the client hung up earlier, the gateway cut the
provider off, no usage arrived, and Metering released the hold without a
charge, while the provider still billed us for what it had generated. Anyone
could get answers for free by disconnecting just before the end.

Once credit is reserved, the provider call no longer follows the client's
connection: the answer runs to the end, and its reported usage is charged,
for every account. Pressing Stop therefore no longer lowers the bill; the
request's max_tokens bounds it, and the provider client's existing idle
timeout still ends a stalled stream. A client that hung up gets no fallback
answer. The old 5-second post-finish drain is no longer needed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Marketen

Marketen commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

Merged into #22.

@Marketen Marketen closed this Nov 4, 2026
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