@@ -6,16 +6,13 @@ import type { TimeSegment } from '@/providers/types'
66import { calculateCost } from '@/providers/utils'
77
88/**
9- * Passthrough of `source` that runs exactly one terminal callback: `onDrain`
10- * when it completes normally (cost is settled then), or `onError` when a read
11- * errors mid-stream (records the hosted-key failure). A client `cancel` is not a
12- * key failure, so it runs neither. Lets hosted-key cost/failure metrics stay
13- * symmetric regardless of whether the provider's drain callback finalized timing.
9+ * Passthrough of `source` that runs at most one terminal callback: `onDrain` when
10+ * it completes normally, or `onError` when a read errors mid-stream. A client
11+ * `cancel` runs neither (an abort is not a key failure).
1412 */
15- function settleHostedCostOnStreamDrain (
13+ function tapStreamTermination (
1614 source : ReadableStream ,
17- onDrain : ( ) => void ,
18- onError : ( error : unknown ) => void
15+ callbacks : { onDrain ?: ( ) => void ; onError ?: ( error : unknown ) => void }
1916) : ReadableStream {
2017 const reader = source . getReader ( )
2118 let finished = false
@@ -26,7 +23,7 @@ function settleHostedCostOnStreamDrain(
2623 if ( done ) {
2724 if ( ! finished ) {
2825 finished = true
29- onDrain ( )
26+ callbacks . onDrain ?. ( )
3027 }
3128 controller . close ( )
3229 return
@@ -35,7 +32,7 @@ function settleHostedCostOnStreamDrain(
3532 } catch ( error ) {
3633 if ( ! finished ) {
3734 finished = true
38- onError ( error )
35+ callbacks . onError ?. ( error )
3936 }
4037 controller . error ( error )
4138 }
@@ -46,6 +43,29 @@ function settleHostedCostOnStreamDrain(
4643 } )
4744}
4845
46+ /**
47+ * Wrap a hosted-key streaming response so a mid-stream read error records a
48+ * hosted-key failure metric. Applied provider-agnostically at the chokepoint
49+ * (`executeProviderRequest`) so it covers every provider — including ones that
50+ * build streams bespoke (gemini) and don't go through {@link createStreamingExecution}.
51+ * Cost on success is settled per-provider; this only handles the failure leg.
52+ */
53+ export function recordHostedStreamFailure (
54+ source : ReadableStream ,
55+ hostedKey : { provider : string ; envVar : string } ,
56+ model : string
57+ ) : ReadableStream {
58+ return tapStreamTermination ( source , {
59+ onError : ( error ) =>
60+ hostedKeyMetrics . recordFailed ( {
61+ provider : hostedKey . provider ,
62+ tool : model ,
63+ key : hostedKey . envVar ,
64+ reason : classifyHostedKeyFailure ( error ) ,
65+ } ) ,
66+ } )
67+ }
68+
4969/**
5070 * Settle the authoritative streaming LLM cost onto `output.cost` from its final
5171 * tokens (the single cost seam shared with the non-streaming path), and — on the
@@ -253,18 +273,11 @@ export function createStreamingExecution(
253273 // returned stream and settle once the source completes (final tokens are set by
254274 // the provider's drain callback before the stream closes). Recomputes the
255275 // authoritative cost with the multiplier and emits the cost metric exactly once.
276+ // Failure on error is handled provider-agnostically in executeProviderRequest.
256277 const stream = hostedKey
257- ? settleHostedCostOnStreamDrain (
258- baseStream ,
259- ( ) => settleStreamingLlmCost ( output , model , hostedKey , cached ?? false ) ,
260- ( error ) =>
261- hostedKeyMetrics . recordFailed ( {
262- provider : hostedKey . provider ,
263- tool : model ,
264- key : hostedKey . envVar ,
265- reason : classifyHostedKeyFailure ( error ) ,
266- } )
267- )
278+ ? tapStreamTermination ( baseStream , {
279+ onDrain : ( ) => settleStreamingLlmCost ( output , model , hostedKey , cached ?? false ) ,
280+ } )
268281 : baseStream
269282
270283 return {
0 commit comments