Repository navigation
[AISAGE-757] fix(ai-gateway): keep upstream alive on disconnect without cost lookup - #8552
chrarnoldus wants to merge 1 commit into
Conversation
…when only the stream reports usage
| // record can still cost the request, or when the user's own key pays for it. | ||
| const abortUpstreamOnClientDisconnect = | ||
| hasGenerationLookup(effectiveProviderContext.provider.id) || | ||
| effectiveProviderContext.userByok !== null; |
There was a problem hiding this comment.
WARNING: userByok does not cover every provider the user pays for
The stated rule is to abort on disconnect when the user's own key pays, but userByok is null for two user-paid providers:
custom(packages/web-shared/src/lib/ai-gateway/providers/get-provider.ts:175) is served with the organization's stored custom-LLM credentials.openai-chatgpt(packages/web-shared/src/lib/ai-gateway/openai-chatgpt/routing.ts:328) is served on the user's delegated ChatGPT connection.
Both hit hasGenerationLookup(...) === false and userByok === null, so after this change their upstream is no longer aborted on client disconnect and keeps generating (up to the 10-minute upstream budget) into a response nobody receives. Kilo does not pay, but the user's own ChatGPT allowance or custom-LLM key does, and the request-log/usage path records no benefit for them. Consider basing the decision on who pays rather than on userByok alone (for example also aborting for openai-chatgpt and custom).
| ); | ||
| // The request signal also aborts the upstream fetch, unless the upstream must | ||
| // keep streaming for usage accounting (see abortUpstreamOnClientDisconnect). | ||
| // Any abort/cancellation logged for this request after this line is a |
There was a problem hiding this comment.
SUGGESTION: This comment is no longer accurate for the non-aborting path
When abortUpstreamOnClientDisconnect is false the request signal is not forwarded, so upstream-request.ts no longer sets clientDisconnected and a genuine UPSTREAM_DURATION_BUDGET_MS timeout after the client left is logged and reported as an upstream failure rather than being suppressed. The claim that any later abort "is a consequence of the client going away" now holds only for the providers that still receive the signal; it would be worth qualifying it so incident triage does not dismiss a real budget timeout.
Code Review SummaryStatus: 2 Issues Found | Recommendation: Address before merge Executive SummaryThe change correctly keeps billing usage for providers without a generation lookup, but the Assumptions: Overview
Issue Details (click to expand)WARNING
SUGGESTION
Files Reviewed (3 files)
Fix these issues in Kilo Cloud Reviewed by deepseek-flash-latest · Input: 169.3K · Output: 20.3K · Cached: 1.6M Review guidance: REVIEW.md from base branch |
|
Closing: request cancellation ( |
Summary
The gateway passes
request.signalto the upstream fetch, so when the client disconnects, the upstream stream and the usage clone are aborted together. Usage normally arrives in the final chunk, so the request is then recorded with 0 tokens. OpenRouter and Vercel recover the cost through the generation lookup. Providers without a lookup (Martian, which serves the stealth models) were billed $0, including when the client stopped reading after it already had the full response.The upstream is now aborted on client disconnect only when the cost stays recoverable:
openrouter,vercel), oruserByok), so Kilo does not pay.For every other provider, the upstream keeps streaming after the client leaves, bounded by the existing upstream duration budget, and
countAndStoreUsage(insideafter()) records the real usage.Verification
stealth/claude-sonnet-4.6). Before: aborting mid-stream on/chat/completions, or atcontent_block_stopon/messages, recorded 0 tokens and $0. After: both recorded full input and output usage and the matching cost.Visual Changes
N/A
Reviewer Notes
request.signalonly fires on client disconnect when the function setssupportsCancellation, whichkilocode-ai-gatewaydoes not. Production therefore already keeps reading in most cases; the abort reproduces undernext dev. This change keeps billing correct if cancellation is enabled later. With cancellation enabled, Vercel terminates work outsideafter()/waitUntil, and usage accounting already runs inafter().message_startinput tokens on the Messages API.