@urun-sh/openai 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import { Server, IncomingMessage } from 'node:http';
2
- import { f as ProxyHandlerOptions, P as ProxyClients } from '../responses-turn-BfdBbey8.cjs';
3
- export { D as DialObserver, a as DialReport, g as ProxyIdentity, h as ProxyVideoOutLane, S as SessionGoneError, U as UnknownModelError } from '../responses-turn-BfdBbey8.cjs';
2
+ import { f as ProxyHandlerOptions, P as ProxyClients } from '../responses-turn-vMefRdgJ.cjs';
3
+ export { D as DialObserver, a as DialReport, g as ProxyIdentity, h as ProxyVideoOutLane, S as SessionGoneError, U as UnknownModelError } from '../responses-turn-vMefRdgJ.cjs';
4
4
  export { h as GEMINI_LIVE_PATH, V as VIDEO_OUT_SETUP_KEY } from '../translator-CO8W_hmJ.cjs';
5
5
  import { WebSocket } from 'ws';
6
6
  import '../models-DUdx_Y6X.cjs';
@@ -285,7 +285,7 @@ declare const OPENAI_REALTIME_PATH = "/v1/realtime";
285
285
  * convention) or a `{ resolve(req, signal) }` object. `signal` aborts when
286
286
  * the CLIENT SOCKET closes/errors before the upgrade is accepted — the
287
287
  * resolver SHOULD abandon its admission wait on it (the backhaul's
288
- * `openAudio` rides the same signal into core `whenLive`'s own abort
288
+ * `openAudioOn` rides the same signal into core `whenLive`'s own abort
289
289
  * support); a resolver that ignores it still cannot admit an upgrade to a
290
290
  * vanished client: the settled binding is released, never upgraded.
291
291
  */
@@ -302,7 +302,11 @@ interface RealtimeBinding<S = unknown> {
302
302
  * - `session.sendResponseCreate(params, input, requestId)` (the direct
303
303
  * SdkTransport-style seam), or
304
304
  * - `clients.createResponse(params)` — the canonical ProxyClients seam
305
- * the conversation factory hands out (`input` rides inside `params`).
305
+ * shape (`input` rides inside `params`). The shared binding composition
306
+ * (binding.ts, ENG-446) hands out a PINNED view of it: the call dials
307
+ * `createResponseOn(handle, …)` on the ONE pooled session the binding
308
+ * resolved with the audio bridge, so no turn on the connection can
309
+ * reach a second pooled session behind the bridge's back.
306
310
  * A binding with neither is rejected at upgrade time, LOUD.
307
311
  */
308
312
  session?: S & {
@@ -321,8 +325,8 @@ interface RealtimeBinding<S = unknown> {
321
325
  appendInputAudio(base64Pcm16: string): void;
322
326
  onOutputAudio(handler: (b64: string) => void): () => void;
323
327
  /**
324
- * The pooled uRun session this lane rides (backhaul `openAudio` reports
325
- * core `Session.id`), when the backhaul knows it — the field the
328
+ * The pooled uRun session this lane rides (backhaul `openAudioOn`
329
+ * reports core `Session.id`), when the backhaul knows it — the field the
326
330
  * `realtime_upgrade` log line puts on every lifecycle event so an
327
331
  * orphaned session is traceable from the proxy log alone. Null is the
328
332
  * honest absence (an id-less session reports no id); sessionIdOf
@@ -395,6 +399,22 @@ declare function attachOpenAIRealtime<S = unknown>(server: Server, options: Open
395
399
  * Multi-attach on the SAME authorized session (explicit resume) is the one
396
400
  * sanctioned shared form: one bridge owner at a time, validated by claim.
397
401
  *
402
+ * ONE SESSION IDENTITY PER CONNECTION (ENG-446): the shared binding
403
+ * composition resolves the connection's pooled session ONCE and pins BOTH the
404
+ * audio bridge (`openAudioOn(handle)`) and every turn (`createResponseOn
405
+ * (handle, …)`, threaded through the binding's `clients` view) to that one
406
+ * handle. This adapter therefore treats a turn seam failure that means THE
407
+ * SESSION (not the request) is dead — a {@link SessionGoneError} (the pool
408
+ * replaced or dropped the pinned entry), or a dial that dies before any
409
+ * client-visible content — as the LOUD `session_gone` CLOSE of the whole
410
+ * connection: an `error` event naming the cause, then the 1011 close. The
411
+ * bridge stays bound to the dead session and NEVER rebinds, and the turn is
412
+ * never retried on a replacement behind the bridge's back (the two-session
413
+ * race the speech lane's #500 pin fixed for its lane). The ONE exception is
414
+ * core's own BOUNDED ADMISSION VERDICT (`SessionFailedError` never-live /
415
+ * create-failed — the platform's answer about this assignment, not a death):
416
+ * it stays the per-turn `response.failed`, exactly as before.
417
+ *
398
418
  * NO-OP POLICY: every unsupported client event or backend gap renders as a
399
419
  * LOUD `error` server event (or a close) — never a silently ignored frame.
400
420
  */
@@ -466,7 +486,9 @@ declare class OpenAIRealtimeConnection<S = unknown> {
466
486
  * The upstream turn seam, normalized: a binding carries EITHER
467
487
  * `session.sendResponseCreate(params, input, requestId)` (direct
468
488
  * SdkTransport-style) OR `clients.createResponse(params)` (the canonical
469
- * ProxyClients seam the shared conversation factory hands out). A binding
489
+ * ProxyClients seam SHAPE — the shared binding composition hands out a
490
+ * PINNED view whose call dials `createResponseOn(handle, …)` on the ONE
491
+ * pooled session the audio bridge rides, ENG-446). A binding
470
492
  * with neither was already rejected at upgrade time; this is the loud
471
493
  * backstop. The ProxyClients seam is PROMISE-VALUED
472
494
  * (`rehomingCreateResponse` dials inside an async function — its
@@ -512,6 +534,45 @@ declare class OpenAIRealtimeConnection<S = unknown> {
512
534
  */
513
535
  private doneResponse;
514
536
  private upstreamErrorBody;
537
+ /**
538
+ * THE SESSION-GONE LOUD CLOSE (ENG-446): an `error` event naming the cause
539
+ * (`session_gone`), then the abnormal close and teardown. The connection's
540
+ * audio bridge is bound to the dead/replaced session and NEVER rebinds, and
541
+ * the turn is never retried on another session behind the bridge's back —
542
+ * the only honest moves are to say why and close.
543
+ */
544
+ private failSessionGone;
545
+ /**
546
+ * Classify ONE upstream turn failure (the dial throwing, or the stream
547
+ * throwing mid-read) — the two outcomes are the LOUD `session_gone` close
548
+ * and the per-turn fault the lane always had:
549
+ *
550
+ * - a {@link SessionGoneError} (the pool no longer holds the handle's
551
+ * session, or a replacement serves under it now) closes the connection
552
+ * LOUDLY, pre- or post-content alike — and post-CANCEL alike: the
553
+ * bridge is orphaned either way, and a cancelled turn must never mute
554
+ * a dead pinned session (round-1 review, CodeRabbit + Greptile);
555
+ * - a CANCELLED turn suppresses every OTHER late fault (round-1 review):
556
+ * `response.cancel` does not abort a pending dial, and the dial's
557
+ * rejection settles AFTER `cancelled` was set — the lane's pre-#511
558
+ * behaviour stands (`response.cancelled` from `inFlight.finally`, the
559
+ * connection stays open), so a generic late fault from the settling
560
+ * dial/read is swallowed here rather than closing the connection as
561
+ * `session_gone`;
562
+ * - a dial that dies BEFORE any client-visible content (nothing flushed)
563
+ * closes the connection LOUDLY too — under the pin there is no re-home,
564
+ * and keeping the connection open would leave the bridge attached to a
565
+ * session that cannot serve a turn, silently mute on the next
566
+ * response.create. The ONE exception is core's own BOUNDED ADMISSION
567
+ * VERDICT (`SessionFailedError` never-live / create-failed — the
568
+ * platform's answer about this assignment, not a death), which keeps
569
+ * the per-turn failure exactly as before;
570
+ * - everything else (content already flowed; the upstream answered with
571
+ * an `error` EVENT, handled elsewhere) is the per-turn fault — the
572
+ * connection stays open, and a later turn on the same pinned handle
573
+ * decides the connection's fate.
574
+ */
575
+ private upstreamTurnFault;
515
576
  private emitUpstreamFault;
516
577
  /**
517
578
  * Cancel the in-flight response. The upstream iterator is stopped through
@@ -540,11 +601,24 @@ declare class OpenAIRealtimeConnection<S = unknown> {
540
601
  * placements) through the SAME router-backed gate seam the Images lane uses
541
602
  * (`ProxyClients.supportsAudio` — `ModelRouter.supportsAudio`). A non-audio
542
603
  * model is refused LOUDLY with a 404 before anything is opened; a voice
543
- * binding acquires its PRIVATE audio bridge from `ProxyClients.openAudio`
544
- * (the pooled session's native `audio` lane at 24 kHz — the ONE canonical
545
- * session-audio wiring, `transport/media.ts` AUDIO_STREAM). There is no env-var
546
- * switch and no chat-default fall-through: an absent model resolves through
547
- * the router exactly like every other lane, and a chat default is refused
604
+ * binding rides ONE pooled-session identity for BOTH of its legs (ENG-446,
605
+ * the speech lane's #500 pin applied to the realtime lane): the connection
606
+ * resolves its pooled session ONCE (`ProxyClients.sessionHandle` — the same
607
+ * handle seam the speech lane pins its collector and turn with, here naming
608
+ * the INTERACTIVE pooled slot), opens its PRIVATE audio bridge on that exact
609
+ * handle (`openAudioOn(handle, signal)` — the pinned open), and drives every
610
+ * turn on the SAME handle (`createResponseOn(handle, …)`, threaded through
611
+ * the binding's `clients` view). The unpinned shape — the bridge via
612
+ * `openAudio(model)`, turns via the re-homing `createResponse` — let each leg
613
+ * resolve its OWN pooled session, so a dial that died before client-visible
614
+ * content re-homed the turn onto a REPLACEMENT session while the bridge
615
+ * stayed attached to the evicted one: the turn's audio was delivered to a
616
+ * session the client was not attached to, or a concurrent bridge on the
617
+ * replacement received another caller's audio. Behind the pin, a dead or
618
+ * replaced session is the connection's LOUD `session_gone` close
619
+ * (connection.ts), never a retry across sessions. There is no env-var switch
620
+ * and no chat-default fall-through: an absent model resolves through the
621
+ * router exactly like every other lane, and a chat default is refused
548
622
  * exactly like an explicitly named chat model.
549
623
  *
550
624
  * Used by BOTH compositions — the local `urun compat proxy` (fixed clients,
@@ -566,10 +640,15 @@ declare function realtimeModelOf(req: IncomingMessage): string | undefined;
566
640
  * - {@link UnknownModelError} ⇒ 404 with the router's own loud message.
567
641
  * - A non-audio verdict ⇒ 404 — the realtime lane never falls through to
568
642
  * the chat lane, in either direction.
569
- * - A voice verdict with no `openAudio` seam ⇒ 501 (a voice binding cannot
570
- * be served without its bridge; a voiceless downgrade is never silent).
643
+ * - A voice verdict with no `openAudioOn` seam ⇒ 501 (a voice binding cannot
644
+ * be served without its pinned bridge; a voiceless downgrade is never
645
+ * silent).
646
+ * - ONE pooled-session identity for the bridge AND every turn
647
+ * (`sessionHandle` once → `openAudioOn(handle, signal)` +
648
+ * `createResponseOn(handle, …)`), so no turn on this connection can ever
649
+ * resolve a different pooled session than the bridge.
571
650
  */
572
- declare function realtimeBindingFor(clients: Pick<ProxyClients, 'createResponse'> & Partial<Pick<ProxyClients, 'supportsAudio' | 'openAudio'>>, model: string | undefined, signal?: AbortSignal): Promise<RealtimeBinding<ProxyClients>>;
651
+ declare function realtimeBindingFor(clients: Pick<ProxyClients, 'sessionHandle' | 'createResponseOn'> & Partial<Pick<ProxyClients, 'supportsAudio' | 'openAudioOn'>>, model: string | undefined, signal?: AbortSignal): Promise<RealtimeBinding<ProxyClients>>;
573
652
  /**
574
653
  * The LOCAL lane resolver: fixed pooled clients, optional fixed apiKey (the
575
654
  * same shared-secret rule the local Gemini Live lane applies — a mismatched
@@ -1,6 +1,6 @@
1
1
  import { Server, IncomingMessage } from 'node:http';
2
- import { f as ProxyHandlerOptions, P as ProxyClients } from '../responses-turn-CdhIre_a.js';
3
- export { D as DialObserver, a as DialReport, g as ProxyIdentity, h as ProxyVideoOutLane, S as SessionGoneError, U as UnknownModelError } from '../responses-turn-CdhIre_a.js';
2
+ import { f as ProxyHandlerOptions, P as ProxyClients } from '../responses-turn-EwxfRJJg.js';
3
+ export { D as DialObserver, a as DialReport, g as ProxyIdentity, h as ProxyVideoOutLane, S as SessionGoneError, U as UnknownModelError } from '../responses-turn-EwxfRJJg.js';
4
4
  export { h as GEMINI_LIVE_PATH, V as VIDEO_OUT_SETUP_KEY } from '../translator-Ddd65sXR.js';
5
5
  import { WebSocket } from 'ws';
6
6
  import '../models-DUdx_Y6X.js';
@@ -171,6 +171,10 @@ declare class OpenAIRealtimeConnection<S = unknown> {
171
171
 
172
172
  private doneResponse;
173
173
  private upstreamErrorBody;
174
+
175
+ private failSessionGone;
176
+
177
+ private upstreamTurnFault;
174
178
  private emitUpstreamFault;
175
179
 
176
180
  private cancelResponse;
@@ -184,7 +188,7 @@ declare class OpenAIRealtimeConnection<S = unknown> {
184
188
 
185
189
  declare function realtimeModelOf(req: IncomingMessage): string | undefined;
186
190
 
187
- declare function realtimeBindingFor(clients: Pick<ProxyClients, 'createResponse'> & Partial<Pick<ProxyClients, 'supportsAudio' | 'openAudio'>>, model: string | undefined, signal?: AbortSignal): Promise<RealtimeBinding<ProxyClients>>;
191
+ declare function realtimeBindingFor(clients: Pick<ProxyClients, 'sessionHandle' | 'createResponseOn'> & Partial<Pick<ProxyClients, 'supportsAudio' | 'openAudioOn'>>, model: string | undefined, signal?: AbortSignal): Promise<RealtimeBinding<ProxyClients>>;
188
192
 
189
193
  declare function fixedClientsResolver(clients: ProxyClients, apiKey?: string): {
190
194
  resolve(req: IncomingMessage, signal?: AbortSignal): Promise<RealtimeBinding<ProxyClients>>;
@@ -1 +1 @@
1
- import{B as l,C as a,D as x,E as s,F as O,G as f,S as I,a as n,b as p,c as m}from"../chunk-TPH77ZOW.js";import{d as r,e as i}from"../chunk-WCMX5VOF.js";import{a as o,h as t}from"../chunk-3JWYIKHM.js";import"../chunk-6M5JK4YC.js";import{c as e}from"../chunk-YSFSRI3D.js";e();export{o as GEMINI_LIVE_PATH,a as OPENAI_REALTIME_PATH,l as OpenAIRealtimeConnection,n as ResumptionRegistry,i as SessionGoneError,r as UnknownModelError,t as VIDEO_OUT_SETUP_KEY,p as attachGeminiLive,x as attachOpenAIRealtime,I as createOpenAIProxy,f as fixedClientsResolver,O as realtimeBindingFor,s as realtimeModelOf,m as serveGeminiLiveSocket};
1
+ import{B as l,C as a,D as x,E as s,F as O,G as f,S as I,a as n,b as p,c as m}from"../chunk-URMOBMSP.js";import{d as r,e as i}from"../chunk-TMQJY56F.js";import{a as o,h as t}from"../chunk-3JWYIKHM.js";import"../chunk-6M5JK4YC.js";import{c as e}from"../chunk-YSFSRI3D.js";e();export{o as GEMINI_LIVE_PATH,a as OPENAI_REALTIME_PATH,l as OpenAIRealtimeConnection,n as ResumptionRegistry,i as SessionGoneError,r as UnknownModelError,t as VIDEO_OUT_SETUP_KEY,p as attachGeminiLive,x as attachOpenAIRealtime,I as createOpenAIProxy,f as fixedClientsResolver,O as realtimeBindingFor,s as realtimeModelOf,m as serveGeminiLiveSocket};
@@ -198,6 +198,10 @@ interface RouterModelEntry {
198
198
  warm?: number;
199
199
 
200
200
  voices?: string[];
201
+
202
+ capabilities?: {
203
+ reasoning: boolean;
204
+ };
201
205
  }
202
206
  interface RouterModelList {
203
207
  object: 'list';
@@ -338,10 +342,14 @@ declare class ModelRouter<S> {
338
342
 
339
343
  supportsSpeech(model: string | undefined): Promise<boolean>;
340
344
 
345
+ supportsTranscription(model: string | undefined): Promise<boolean>;
346
+
341
347
  speechVoices(model: string | undefined): Promise<SpeechVoicesResolution>;
342
348
 
343
349
  imageModelList(): Promise<RouterImageModelList>;
344
350
 
351
+ speechToTextModelList(): Promise<RouterModelList>;
352
+
345
353
  closeAll(): Promise<void>;
346
354
  }
347
355
 
@@ -555,6 +563,8 @@ interface ResponsesCreateParams {
555
563
  response_format?: unknown;
556
564
 
557
565
  voice?: string;
566
+
567
+ language?: string;
558
568
  }
559
569
 
560
570
  interface SessionEndInfo {
@@ -580,10 +590,14 @@ interface ProxyClients {
580
590
 
581
591
  supportsSpeech?(model: string | undefined): Promise<boolean>;
582
592
 
593
+ supportsTranscription?(model: string | undefined): Promise<boolean>;
594
+
583
595
  speechVoices?(model: string | undefined): Promise<SpeechVoicesResolution>;
584
596
 
585
597
  listImageModels?(): Promise<unknown>;
586
598
 
599
+ listSpeechToTextModels?(): Promise<unknown>;
600
+
587
601
  catalogIdentity(model: string | undefined): Promise<{
588
602
  model_id: string;
589
603
  variant: string;
@@ -593,7 +607,7 @@ interface ProxyClients {
593
607
 
594
608
  openAudio?(model: string | undefined, signal?: AbortSignal): Promise<ProxyAudioLane>;
595
609
 
596
- openAudioOn?(handle: string): Promise<PinnedAudioLane>;
610
+ openAudioOn?(handle: string, signal?: AbortSignal): Promise<PinnedAudioLane>;
597
611
 
598
612
  openVideo?(model: string | undefined): Promise<ProxyVideoLane>;
599
613
 
@@ -650,6 +650,23 @@ interface RouterModelEntry {
650
650
  * entry, and on tts entries whose voices the catalog does not vouch.
651
651
  */
652
652
  voices?: string[];
653
+ /**
654
+ * Declared lane capabilities, derived from the catalog row — never
655
+ * hand-set. `reasoning` is true/false exactly when the row's
656
+ * `engine_args.reasoning_parser` is configured/absent on EVERY GPU
657
+ * placement (the runtime's own per-row parser knob, urun-python
658
+ * engines_vllm_engine.py / engines_sglang_engine.py): true means the
659
+ * serve lane separates thinking text onto `reasoning_content` /
660
+ * reasoning items / `thinking` blocks and `content` carries only the
661
+ * answer; false means the model emits no separated reasoning. A variant
662
+ * whose placements disagree, a malformed value, or a slug with no catalog
663
+ * row omit the object entirely — unknown, never guessed
664
+ * (openrouter-doc.ts `placementsReasoning`, applied by both listing
665
+ * lanes).
666
+ */
667
+ capabilities?: {
668
+ reasoning: boolean;
669
+ };
653
670
  }
654
671
  interface RouterModelList {
655
672
  object: 'list';
@@ -1182,6 +1199,23 @@ declare class ModelRouter<S> {
1182
1199
  * never fabricates a `false` out of an infrastructure outage.
1183
1200
  */
1184
1201
  supportsSpeech(model: string | undefined): Promise<boolean>;
1202
+ /**
1203
+ * THE TRANSCRIPTION LANE'S GATE (proxy/transcriptions.ts,
1204
+ * `POST /v1/audio/transcriptions`): does `model`'s deployed app carry
1205
+ * catalog `task` **stt** — INVARIANTLY across every GPU-placement row
1206
+ * ({@link audioModalityForRows} answering exactly `'stt'`)? The SAME
1207
+ * shape as {@link supportsImages}/{@link supportsSpeech}, and deliberately
1208
+ * NOT {@link supportsAudio} (which is modality-AGNISTIC — `!== null`
1209
+ * accepts stt OR tts — because the realtime lane serves both): a `tts`
1210
+ * model must never be asked to transcribe and a chat model is the loud
1211
+ * 404 the images lane renders for non-image models. A SHARED-lane hit
1212
+ * joins on the lane's exact catalog id + variant (the SAME join
1213
+ * {@link imageModelList} uses for the shared block), exactly like
1214
+ * {@link supportsSpeech}. A CONFIGURED catalog oracle that FAILS
1215
+ * propagates its rejection — the gate never fabricates a `false` out of
1216
+ * an infrastructure outage.
1217
+ */
1218
+ supportsTranscription(model: string | undefined): Promise<boolean>;
1185
1219
  /**
1186
1220
  * THE SPEECH LANE'S VOICE-SET RESOLUTION (proxy/speech.ts): the voice set
1187
1221
  * `/v1/models` publishes for `model` — resolved by the SAME resolver the
@@ -1213,6 +1247,21 @@ declare class ModelRouter<S> {
1213
1247
  * bare-entries degradation here.
1214
1248
  */
1215
1249
  imageModelList(): Promise<RouterImageModelList>;
1250
+ /**
1251
+ * `GET /v1/audio/models`: the TRANSCRIPTION-capable (task `stt`) slice of
1252
+ * the model surface — the audio twin of {@link imageModelList}: the
1253
+ * caller-org deployed apps whose catalog rows vouch `stt`
1254
+ * ({@link audioModalityForRows} answering exactly `'stt'`, the SAME
1255
+ * consensus {@link supportsTranscription} applies), plus the shared block's
1256
+ * endpoints joined against each endpoint's OWN catalog row (exact model_id +
1257
+ * variant; a definite non-stt task is simply skipped, never a loud defect —
1258
+ * the task column IS the vouch, there is no expects_image-style flag to
1259
+ * require). The catalog read propagates (this listing IS the capability
1260
+ * oracle's answer), and the PRICE SURFACE IS NOT READ AT ALL — an stt model
1261
+ * is not token-priced, so {@link UNPRICED_SURFACE} states that as a fact
1262
+ * about the lane, exactly like the images listing.
1263
+ */
1264
+ speechToTextModelList(): Promise<RouterModelList>;
1216
1265
  /** Close every pooled session (Session.end() underneath) — proxy shutdown. */
1217
1266
  closeAll(): Promise<void>;
1218
1267
  }
@@ -1940,6 +1989,17 @@ interface ResponsesCreateParams {
1940
1989
  * default voice applies, today's behavior.
1941
1990
  */
1942
1991
  voice?: string;
1992
+ /**
1993
+ * The transcription lane's per-request LANGUAGE hint (ENG-332, additive
1994
+ * like `voice`): the ISO code a `stt`-task engine transcribes under
1995
+ * (LiveKit's `openai.STT` always sends one, so refusing the field would
1996
+ * break the stock consumer). Forwarded VERBATIM on the serve envelope so
1997
+ * the runtime validates it against the transcribe engine's declared
1998
+ * `served_languages` — the proxy never validates languages itself (it
1999
+ * cannot know the engine's set) and never silently drops the field.
2000
+ * Absent -> key absent, never an empty string.
2001
+ */
2002
+ language?: string;
1943
2003
  }
1944
2004
  /** How one pinned session ended (the native phase machinery's terminal step). */
1945
2005
  interface SessionEndInfo {
@@ -2058,6 +2118,17 @@ interface ProxyClients {
2058
2118
  * silent allow-all, and never a chat model asked to speak.
2059
2119
  */
2060
2120
  supportsSpeech?(model: string | undefined): Promise<boolean>;
2121
+ /**
2122
+ * The transcription lane's capability gate (proxy/transcriptions.ts), backed
2123
+ * by ModelRouter.supportsTranscription — the caller-org catalog's `task`
2124
+ * column, which must vouch `stt` (NOT `tts`: a synthesis model must never
2125
+ * be asked to transcribe; NOT chat) INVARIANTLY across placements. Optional
2126
+ * at the seam exactly like {@link supportsSpeech}: hand-built clients may
2127
+ * omit it, in which case the mounted transcription lane answers the loud
2128
+ * 501 — never a silent allow-all, and never a chat model asked to
2129
+ * transcribe.
2130
+ */
2131
+ supportsTranscription?(model: string | undefined): Promise<boolean>;
2061
2132
  /**
2062
2133
  * THE SPEECH LANE'S VOICE-SET RESOLUTION (proxy/speech.ts): the voices
2063
2134
  * `/v1/models` publishes for `model` (ModelRouter.speechVoices — the SAME
@@ -2080,6 +2151,14 @@ interface ProxyClients {
2080
2151
  * its absence is the loud 501, never a silently empty list.
2081
2152
  */
2082
2153
  listImageModels?(): Promise<unknown>;
2154
+ /**
2155
+ * `GET /v1/audio/models` — the transcription-capable (task `stt`) slice of
2156
+ * the model surface (ModelRouter.speechToTextModelList: deployed stt apps +
2157
+ * the shared block joined against the catalog), the audio twin of
2158
+ * {@link listImageModels}. Optional at the seam for the same reason; its
2159
+ * absence is the loud 501, never a silently empty list.
2160
+ */
2161
+ listSpeechToTextModels?(): Promise<unknown>;
2083
2162
  /**
2084
2163
  * THE PRE-DIAL DEFAULT FOR THE LEDGER'S BILLING IDENTITY
2085
2164
  * (ModelRouter.catalogIdentityFor): the CATALOG `(model_id, variant)` a
@@ -2146,28 +2225,46 @@ interface ProxyClients {
2146
2225
  * text-only embeddings exist — but a surface that RECEIVES audio while the
2147
2226
  * embedder wired no `openAudio` must fail LOUD, never drop chunks.
2148
2227
  *
2149
- * `signal` aborts when the CLIENT that asked for the lane went away while
2150
- * the lane's admission was still pending (the realtime lane's raw socket
2151
- * close) — the backhaul aborts its `whenLive` wait on it and hands the
2152
- * queued admission ticket back through core `Session.cancel()`, so a
2153
- * vanished client never holds the sense pod's one-session slot.
2154
- */
2228
+ * `signal` aborts when the CLIENT that asked for the lane went away while
2229
+ * the lane's admission was still pending (the realtime lane's raw socket
2230
+ * close) — the backhaul aborts its `whenLive` wait on it and hands the
2231
+ * queued admission ticket back through core `Session.cancel()`, so a
2232
+ * vanished client never holds the sense pod's one-session slot. The
2233
+ * client-gone wait is REFERENCE-COUNTED per pooled entry: several callers
2234
+ * can wait on ONE queued admission, and the ticket goes back only when
2235
+ * the LAST waiter aborts — one client's disconnect rejects only its own
2236
+ * open, never another caller's admission (round-1 review, Greptile).
2237
+ */
2155
2238
  openAudio?(model: string | undefined, signal?: AbortSignal): Promise<ProxyAudioLane>;
2156
2239
  /**
2157
- * THE PINNED AUDIO OPEN (speech lane): the NATIVE audio lane of the EXACT
2158
- * pooled session `handle` names (ModelRouter.sessionForHandle — a gone or
2159
- * replaced session throws {@link SessionGoneError} LOUDLY, never a fresh
2160
- * session opened behind the handle's back). The speech lane opens its
2240
+ * THE PINNED AUDIO OPEN (speech + realtime lanes): the NATIVE audio lane of
2241
+ * the EXACT pooled session `handle` names (ModelRouter.sessionForHandle — a
2242
+ * gone or replaced session throws {@link SessionGoneError} LOUDLY, never a
2243
+ * fresh session opened behind the handle's back). The speech lane opens its
2161
2244
  * audio collector here and dials its response turn on the SAME handle
2162
2245
  * ({@link createResponseOn}), so BOTH legs are one session identity by
2163
2246
  * construction: a re-home can never move the turn to a second session
2164
2247
  * while the collector stays subscribed to the first (which missed the
2165
2248
  * replacement's audio and answered a voiceless 502, or worse, served a
2166
- * concurrent collector another session's frames). Optional at the seam
2167
- * exactly like {@link openAudio}; the speech lane answers its absence
2168
- * with the loud 501 — never a voiceless downgrade.
2169
- */
2170
- openAudioOn?(handle: string): Promise<PinnedAudioLane>;
2249
+ * concurrent collector another session's frames). The realtime binding
2250
+ * (openai-realtime/binding.ts, ENG-446) rides the SAME pin: one handle per
2251
+ * connection for the bridge AND every turn, so a re-home can never detach
2252
+ * the turn from the bridge the audio rides. Optional at the seam exactly
2253
+ * like {@link openAudio}; both lanes answer its absence with the loud 501 —
2254
+ * never a voiceless downgrade.
2255
+ *
2256
+ * `signal` carries {@link openAudio}'s client-gone semantics onto the
2257
+ * pinned open (ENG-446): the realtime lane threads the upgrade's
2258
+ * client-socket abort down here — abort while the pinned session's
2259
+ * admission is still queued abandons the queued session (core
2260
+ * `Session.cancel()`), and the open rejects. A vanished client never holds
2261
+ * the sense pod's one-session slot through a queue wait it is no longer
2262
+ * party to. An already-cached lane is returned untouched — and the
2263
+ * client-gone wait rides the SAME per-entry refcount as {@link openAudio}:
2264
+ * one of several waiters aborting rejects only that caller's open, the
2265
+ * queued admission going back only at the LAST waiter's abort.
2266
+ */
2267
+ openAudioOn?(handle: string, signal?: AbortSignal): Promise<PinnedAudioLane>;
2171
2268
  /**
2172
2269
  * Open (or reuse) the NATIVE video FRAME lane on the pooled session `model`
2173
2270
  * routes to (transport/media.ts `enableSessionVideo`: discrete JPEG frames →
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@urun-sh/openai",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "OpenAI-compatible Realtime + Responses SDK over uRun session primitives (not websockets).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -70,7 +70,7 @@
70
70
  "build:bin": "bun build --compile --external @roamhq/wrtc src/proxy/bin.ts --outfile dist/bin/urun-openai && node scripts/assert-bin-transport.mjs"
71
71
  },
72
72
  "peerDependencies": {
73
- "@urun-sh/core": "^0.6.0"
73
+ "@urun-sh/core": "^0.6.2"
74
74
  },
75
75
  "dependencies": {
76
76
  "@prometheus-io/client": "^0.16.1",
@@ -1,7 +0,0 @@
1
- import{a as j,b as W}from"./chunk-TDRZZGUK.js";import{C as re,D as oe,E as se,F as ie,H as ae,I as A,J as le,K as ce,L as ue,M as de,N as he,O as pe,P as ge,Q as fe,R as me,b as Z,d as ee,e as te,q as ne,r as k}from"./chunk-TPH77ZOW.js";import{e as Q,g as z}from"./chunk-WCMX5VOF.js";import{a as X}from"./chunk-3JWYIKHM.js";import{c as p}from"./chunk-YSFSRI3D.js";p();import{Counter as R,Gauge as ye,Histogram as we,Registry as Ue,collectDefaultMetrics as Pe}from"@prometheus-io/client";import{createServer as $e}from"http";var It=9464,be="/metrics",I="(other)",Le="(none)",He=64,Me=128,De=["/v1/responses","/v1/chat/completions","/v1/systemone","/v1/messages","/v1/messages/count_tokens","/v1/models","/v1/images/models","/v1/images/generations","/v1/images/edits","/v1/audio/speech",ae,ce],Fe=new Set(De);function Ke(t){return Fe.has(t)?t:I}function Ge(t){return t===null?"none":t>=500?"5xx":t>=400?"4xx":t>=300?"3xx":t>=200?"2xx":"1xx"}var N=class{constructor(e){this.max=e}max;admitted=new Set;overflows=0;of(e,n=!0){return e===null||e===""?Le:this.admitted.has(e)?e:n?this.admitted.size>=this.max?(this.overflows+=1,I):(this.admitted.add(e),e):I}get size(){return this.admitted.size}get overflowCount(){return this.overflows}},T=class{registry=new Ue;modelLabel=new N(He);orgLabel=new N(Me);requests;duration;ttft;streamErrors;undelivered;ledgerWrites;ledgerQueue;labelCardinality;labelOverflows;constructor(e={}){this.requests=new R({name:"urun_inference_requests_total",help:"Completed /v1 requests, by lane, method, HTTP status class, streaming, model and org.",labelNames:["lane","method","status_class","stream","model","org"],registers:[this.registry]}),this.duration=new we({name:"urun_inference_request_duration_seconds",help:"Wall time from request receipt to response close. For a stream this is the WHOLE generation, not the time to the head.",labelNames:["lane","stream","model","org"],buckets:[.1,.25,.5,1,2.5,5,10,30,60,120,300,600],registers:[this.registry]}),this.ttft=new we({name:"urun_inference_ttft_seconds",help:"Time to first output-text delta on a streamed response. The metric Hugging Face gates provider listing on (<5s for conversational models).",labelNames:["lane","model","org"],buckets:[.1,.25,.5,1,2,3,5,8,15,30,60],registers:[this.registry]}),this.streamErrors=new R({name:"urun_inference_stream_errors_total",help:"Responses that emitted an error frame AFTER the head flushed. These are HTTP 200 with a complete body, so no status-based metric can see them.",labelNames:["lane","model"],registers:[this.registry]}),this.undelivered=new R({name:"urun_inference_undelivered_total",help:"Responses whose socket did not finish flushing \u2014 the client vanished mid-answer.",labelNames:["lane","model"],registers:[this.registry]}),this.ledgerWrites=new R({name:"urun_inference_ledger_writes_total",help:"Terminal fate of the per-request ledger row for each billable /v1 request. outcome=written is the row landing (reason=priced|unpriced); outcome=skipped and outcome=failed are BOTH revenue that went unrecorded. failed/duplicate is the ENG-410 fabricated-row signal.",labelNames:["outcome","reason"],registers:[this.registry]});for(let n of ue)this.ledgerWrites.inc({outcome:n.outcome,reason:n.reason},0);this.ledgerQueue=new ye({name:"urun_inference_ledger_queue",help:'The ledger write queue right now. measure="depth" is rows waiting for a worker; measure="oldest_age_seconds" is how long the head has waited \u2014 approaching the dwell bound means rows are about to be given up on, which is revenue lost.',labelNames:["measure"],registers:[this.registry],collect:()=>{this.ledgerQueue.set({measure:"depth"},de()),this.ledgerQueue.set({measure:"oldest_age_seconds"},he()/1e3)}}),this.labelCardinality=new ye({name:"urun_inference_label_cardinality",help:"Distinct values admitted for a bounded metric label. At its ceiling, further values report as (other).",labelNames:["label"],registers:[this.registry],collect:()=>{this.labelCardinality.set({label:"model"},this.modelLabel.size),this.labelCardinality.set({label:"org"},this.orgLabel.size)}}),this.labelOverflows=new R({name:"urun_inference_label_overflows_total",help:"Observations that would have taken a new label value but found the budget spent. Non-zero means a metric dimension is now partly blind.",labelNames:["label"],registers:[this.registry]}),e.defaultMetrics!==!1&&Pe({register:this.registry,prefix:"urun_inference_proxy_"})}observeLedger(e){this.ledgerWrites.inc({outcome:e.outcome,reason:e.reason})}observe(e){let n=Ke(e.path),o=e.status!==null&&!(e.status>=400&&e.status<500),r=this.modelLabel.overflowCount+this.orgLabel.overflowCount,a=this.modelLabel.of(e.model,o),s=this.orgLabel.of(e.org,o);this.modelLabel.overflowCount+this.orgLabel.overflowCount-r>0&&(this.modelLabel.overflowCount>r?this.labelOverflows.inc({label:"model"}):this.labelOverflows.inc({label:"org"}));let l=e.stream===!0?"true":"false";this.requests.inc({lane:n,method:e.method===""?I:e.method,status_class:Ge(e.status),stream:l,model:a,org:s}),this.duration.observe({lane:n,stream:l,model:a,org:s},e.duration_ms/1e3),e.ttft_ms!==null&&this.ttft.observe({lane:n,model:a,org:s},e.ttft_ms/1e3),e.stream_error&&this.streamErrors.inc({lane:n,model:a}),e.delivered||this.undelivered.inc({lane:n,model:a})}scrape(){return this.registry.metrics()}get contentType(){return this.registry.contentType}};function ve(t){return $e((e,n)=>{(async()=>{let o=(e.url??"/").split("?")[0];if(e.method!=="GET"||o!==be){n.writeHead(404,{"content-type":"text/plain"}),n.end(`no route for ${e.method} ${o} \u2014 this port serves only GET ${be}
2
- `);return}let r=await t.scrape();n.writeHead(200,{"content-type":t.contentType}),n.end(r)})().catch(o=>{if(!n.headersSent){n.writeHead(500,{"content-type":"text/plain"}),n.end(`${String(o instanceof Error?o.message:o)}
3
- `);return}n.end()})})}p();import{createHash as Ee}from"crypto";import{createServer as Be}from"http";var $t=9465,ze="x-urun-key-id",je="x-urun-key-fingerprint",We="urun-ratelimit-bucket:",Je="urun-ratelimit-tier:",_e="anonymous",qe=/^urun_sk_[A-Za-z0-9_-]{43}$/;function Ve(t){return Ee("sha256").update(`${We}${t}`).digest("hex")}function Ye(t){return Ee("sha256").update(`${Je}${t}`).digest("hex")}function Qe(t){let e=/^Bearer[ \t]+(\S.*)$/i.exec((t??"").trim());if(!e)return null;let n=e[1].trim();return qe.test(n)?n:null}function Re(){return Be((t,e)=>{let n=Qe(t.headers.authorization);e.setHeader(ze,n===null?_e:Ve(n)),e.setHeader(je,n===null?_e:Ye(n)),e.writeHead(200),e.end()})}p();import{createClientToken as Xe,ClientTokenError as Ze}from"@urun-sh/core";var m=class extends Error{constructor(n,o=401){super(n);this.status=o;this.name="ProxyAuthError"}status},_=class extends Error{constructor(e){super(e),this.name="ControlPlaneUnavailableError"}};function S(t){let e=(t??"").trim();if(e==="")throw new m("missing Authorization header \u2014 send your uRun org API key as `Authorization: Bearer <key>`");let n=/^Bearer[ \t]+(\S.*)$/i.exec(e);if(!n)throw new m("malformed Authorization header \u2014 expected `Authorization: Bearer <urun org api key>`");return n[1].trim()}var et=60,tt=6e4,nt=4096;function rt(t){return t===401||t===403}var U=class{constructor(e){this.opts=e;this.mint=e.mint??Xe,this.now=e.now??Date.now}opts;cache=new Map;mint;now;async resolve(e){if(e==="")throw new m("empty Authorization bearer token");let n=this.cache.get(e);if(n&&this.now()-n.at<tt)return{apiKey:e,orgId:n.orgId};let o;try{o=(await this.mint(e,{baseUrl:this.opts.gatewayUrl,expiresIn:et})).orgId}catch(r){throw r instanceof Ze&&rt(r.status)?(this.cache.delete(e),new m("the uRun org API key was rejected by the control plane \u2014 check the key and that it is not revoked")):new _(`could not verify the API key against the uRun control plane at ${this.opts.gatewayUrl}: ${r instanceof Error?r.message:String(r)}`)}if(!o)throw new _("the control plane verified the API key but returned no org_id \u2014 refusing to route a request with no tenancy (control-plane contract violation)");if(this.cache.size>=nt){let r=this.cache.keys().next();r.done||this.cache.delete(r.value)}return this.cache.set(e,{orgId:o,at:this.now()}),{apiKey:e,orgId:o}}clear(){this.cache.clear()}};p();var ot=8080,P="serve",Gt="0.0.0.0";function st(t,e,n){let o=(t[e]??"").trim();if(!o)throw new Error(`${e} is not set \u2014 ${n}`);return o}function Bt(t=process.env){let e=(t.PORT??"").trim(),n=e===""?ot:Number(e);if(!Number.isInteger(n)||n<0||n>65535)throw new Error(`PORT must be an integer in 0..65535, got ${JSON.stringify(e)}`);let o=ne(k(st(t,"URUN_BASE_URL","the hosted proxy needs the uRun session-gateway base it opens sessions and verifies API keys against (e.g. https://api.urun.sh)"),"URUN_BASE_URL")),r=(t.URUN_API_URL??"").trim(),a=r?k(r,"URUN_API_URL"):`${o.replace(/\/+$/,"")}/v1`,s=(t.URUN_CATALOG_URL??"").trim(),i=(t.URUN_CATALOG_ANON_KEY??"").trim();if(!!s!=!!i)throw new Error("URUN_CATALOG_URL and URUN_CATALOG_ANON_KEY must be set together (or both left unset) \u2014 exactly one of them is set, which would silently disable the model-catalog oracle");let l=s?{catalogUrl:k(s,"URUN_CATALOG_URL"),anonKey:i}:null,u=(t.URUN_SESSION_TOKEN_SECRET??"").trim(),f=(t.URUN_SHARED_ORG_API_KEY??"").trim(),x=(t.URUN_SHARED_ORG_ID??"").trim();if(!!f!=!!x)throw new Error("URUN_SHARED_ORG_API_KEY and URUN_SHARED_ORG_ID must be set together (or both left unset) \u2014 exactly one of them is set, which would silently leave the shared endpoints on the session lane");return{port:n,baseUrl:o,apiUrl:a,catalog:l,decisionSecret:u,sharedOrgKey:f,sharedOrgId:x}}p();import{createHash as it,randomUUID as at}from"crypto";var lt=512;function M(t){return`urun-inference-proxy-${it("sha256").update(t).digest("hex").slice(0,32)}`}var $=class{constructor(e){this.opts=e}opts;tenants=new Map;label(e,n){return Object.assign(Object.create(e),{tenant:n.orgId})}build(e){if(this.opts.build){let i=this.opts.build(e);return{router:i.router,clients:this.label(i.clients,e)}}let n=0,o=M(e.apiKey),r=()=>{n+=1},a=j({baseUrl:this.opts.baseUrl,orgId:e.orgId,defaultApp:null,fnName:P,sharedOnlyListing:!0,apiUrl:this.opts.apiUrl,auth:{lane:"api-key",apiKey:e.apiKey,gatewayUrl:this.opts.baseUrl},catalog:this.opts.catalog,subject:()=>n===0?o:`${o}#${n}`,onPoolWedged:r,release:"end"}),s=this.opts.decisionIntake?new fe({targetFor:i=>a.targetFor(i),apiUrl:this.opts.apiUrl,apiKey:e.apiKey,orgId:e.orgId,fnName:P,secret:this.opts.decisionIntake.secret,...this.opts.decisionIntake.shared?{shared:this.opts.decisionIntake.shared}:{}}):void 0;return{router:a,clients:this.label(W(a,void 0,r,s),e)}}clientsFor(e){let n=this.tenants.get(e.apiKey);if(n)return this.tenants.delete(e.apiKey),this.tenants.set(e.apiKey,n),n.clients;for(;this.tenants.size>=lt;){let r=this.tenants.entries().next();if(r.done)break;let[a,s]=r.value;this.tenants.delete(a),s.router.closeAll().catch(()=>{})}let o=this.build(e);return this.tenants.set(e.apiKey,o),o.clients}get size(){return this.tenants.size}async closeAll(){let e=[...this.tenants.values()].map(o=>o.router);this.tenants.clear();let n=[];if(await Promise.all(e.map(async o=>{try{await o.closeAll()}catch(r){n.push(String(r instanceof Error?r.message:r))}})),n.length>0)throw new Error(`failed to close ${n.length} tenant backhaul(s): ${n.join("; ")}`)}},Se=512,ct=1024,L=class extends Error{constructor(){super(`conversation capacity reached (${Se} conversations on this replica) \u2014 every slot holds an active or resumable conversation; retry later or scale out`),this.name="ConversationCapacityError"}};function ut(t){return{sessionStartOptions:e=>{if(e.startsWith(z))return;let n=t.get(e);return n===void 0?void 0:{sessionId:n}},onSessionOpen:(e,n)=>{e.startsWith(z)||t.set(e,n)}}}function dt(t,e,n,o){let r=j({baseUrl:t.baseUrl,orgId:e.orgId,defaultApp:null,fnName:P,sharedOnlyListing:!0,apiUrl:t.apiUrl,auth:{lane:"api-key",apiKey:e.apiKey,gatewayUrl:t.baseUrl},catalog:t.catalog,subject:n.subject,release:"detach",...ut(o)});return{clients:W(r),release:async()=>{await r.closeAll()}}}var H=class{constructor(e){this.opts=e}opts;byCaller=new Map;byHandle=new Map;build(e,n){let o={id:n,subject:`${M(e.apiKey)}:live:${n}`},r=new Map,a=this.opts.build?this.opts.build(e,o):dt(this.opts,e,o,r);return{id:n,backhaul:a,attachments:0,nativeRefs:r}}attachEntry(e,n){let o=this.byCaller.get(e.apiKey);o===void 0&&(o=new Map,this.byCaller.set(e.apiKey,o));let r=o.get(n);if(r!==void 0)return o.delete(n),o.set(n,r),r.attachments+=1,r;this.evictOverflow();let a=this.build(e,n);return a.attachments=1,o.set(n,a),a}get conversationCount(){let e=0;for(let n of this.byCaller.values())e+=n.size;return e}evictOverflow(){for(;this.conversationCount>=Se;){let e=this.byCaller.keys().next();if(e.done)throw new L;let n=e.value,o=this.byCaller.get(n),r=!1;for(let[a,s]of o)if(!(s.attachments>0)){o.delete(a),this.dropHandleIndexFor(n,a),s.backhaul.release().catch(()=>{}),r=!0;break}if(!r)throw o.size===0&&this.byCaller.delete(n),new L;o.size===0&&this.byCaller.delete(n)}}dropHandleIndexFor(e,n){for(let[o,r]of this.byHandle)r.apiKey===e&&r.conversationId===n&&this.byHandle.delete(o)}connectionFor(e){let n=null,o=!1,r=()=>(n===null&&(n=this.attachEntry(e,at())),n),a={createResponse:(s,i)=>r().backhaul.clients.createResponse(s,i),catalogIdentity:s=>r().backhaul.clients.catalogIdentity(s),servingAppSlug:s=>{let i=r().backhaul.clients.servingAppSlug;if(i===void 0)throw new Error("the conversation backhaul wires no servingAppSlug seam \u2014 the caller-org app a request resolves to cannot be known");return i(s)},listModels:()=>r().backhaul.clients.listModels(),sessionHandle:s=>r().backhaul.clients.sessionHandle(s),createResponseOn:(s,i)=>r().backhaul.clients.createResponseOn(s,i),onSessionEnd:(s,i)=>r().backhaul.clients.onSessionEnd(s,i)};return a.openAudio=async(s,i)=>{let l=r().backhaul.clients.openAudio;if(l===void 0)throw new Error("the conversation backhaul wires no openAudio seam \u2014 voice is unavailable on this deployment");return l(s,i)},a.openVideo=async s=>{let i=r().backhaul.clients.openVideo;if(i===void 0)throw new Error("the conversation backhaul wires no openVideo seam \u2014 video-in is unavailable on this deployment");return i(s)},a.openVideoOut=async s=>{let i=r().backhaul.clients.openVideoOut;if(i===void 0)throw new Error("the conversation backhaul wires no openVideoOut seam \u2014 video-out is unavailable on this deployment");return i(s)},a.supportsAudio=async s=>{let i=r().backhaul.clients.supportsAudio;if(i===void 0)throw new Error("the conversation backhaul wires no supportsAudio seam \u2014 the realtime lane cannot gate audio modality on this deployment");return i(s)},a.supportsSpeech=async s=>{let i=r().backhaul.clients.supportsSpeech;if(i===void 0)throw new Error("the conversation backhaul wires no supportsSpeech seam \u2014 the speech lane cannot gate model capability on this deployment");return i(s)},a.speechVoices=async s=>{let i=r().backhaul.clients.speechVoices;if(i===void 0)throw new Error("the conversation backhaul wires no speechVoices seam \u2014 the speech lane cannot resolve the voice set /v1/models publishes on this deployment");return i(s)},a.speechSessionHandle=async s=>{let i=r().backhaul.clients.speechSessionHandle;if(i===void 0)throw new Error("the conversation backhaul wires no speechSessionHandle seam \u2014 the speech lane cannot resolve its own pooled session on this deployment");return i(s)},a.openAudioOn=async s=>{let i=r().backhaul.clients.openAudioOn;if(i===void 0)throw new Error("the conversation backhaul wires no openAudioOn seam \u2014 the speech lane cannot open its pinned native session audio on this deployment");return i(s)},a.evictSpeechSession=async s=>{let i=r().backhaul.clients.evictSpeechSession;if(i===void 0)throw new Error("the conversation backhaul wires no evictSpeechSession seam \u2014 the speech lane cannot retire its pinned speech session after a failed turn on this deployment");return i(s)},{clients:a,bindResumeHandle:s=>{let i=n;if(i===null)throw new Error("bindResumeHandle before the conversation opened \u2014 no conversation to bind to");for(;this.byHandle.size>=ct;){let l=this.byHandle.keys().next();if(l.done)break;this.byHandle.delete(l.value)}this.byHandle.set(s,{apiKey:e.apiKey,conversationId:i.id})},clientsForResume:async s=>{let i=this.byHandle.get(s),l=i===void 0?void 0:this.byCaller.get(i.apiKey),u=i===void 0||l===void 0?void 0:l.get(i.conversationId);if(i===void 0||u===void 0||i.apiKey!==e.apiKey)throw new Q("the conversation behind this resume handle is gone (evicted at capacity, or the replica restarted) or the handle is not yours \u2014 reconnect without a handle to start a new conversation");return n=u,u.attachments+=1,u.backhaul.clients},release:async()=>{if(o)return;o=!0;let s=n;n=null,s!==null&&(s.attachments-=1,!(s.attachments>0)&&await s.backhaul.release().catch(()=>{}))}}}async detachAll(){let e=[];for(let o of this.byCaller.values())for(let r of o.values())e.push(r.backhaul.release());this.byCaller.clear(),this.byHandle.clear();let n=[];if(await Promise.all(e.map(async o=>{try{await o}catch(r){n.push(String(r instanceof Error?r.message:r))}})),n.length>0)throw new Error(`failed to detach ${n.length} conversation backhaul(s): ${n.join("; ")}`)}};p();var ht="/inference-ledger",pt=1e4,D=class extends Error{ledgerFailureReason;constructor(e,n="duplicate"){super(e),this.name="LedgerDuplicateError",this.ledgerFailureReason=n}},q=class extends Error{constructor(n,o){super(o);this.ledgerAlreadyWritten=n;this.name="LedgerAlreadyWrittenError"}ledgerAlreadyWritten},g=class extends Error{ledgerFailureReason="surface";constructor(e){super(e),this.name="LedgerSurfaceError"}},V=class extends m{ledgerFailureReason="auth";constructor(e){super(e),this.name="LedgerAuthError"}},gt=[["inference_id",t=>typeof t=="string"&&/[^\s]/.test(t)],["model_id",t=>typeof t=="string"&&/[^\s]/.test(t)],["variant",t=>t===null||typeof t=="string"],["task",t=>typeof t=="string"&&/[^\s]/.test(t)],["http_status",t=>typeof t=="number"&&Number.isInteger(t)],["started_at",t=>typeof t=="string"&&!Number.isNaN(Date.parse(t))],["completed_at",t=>typeof t=="string"&&!Number.isNaN(Date.parse(t))],["prompt_tokens",t=>t===null||typeof t=="number"&&Number.isInteger(t)],["completion_tokens",t=>t===null||typeof t=="number"&&Number.isInteger(t)],["output_units",xe],["cost_nano_usd",xe],["price_version",t=>t===null||typeof t=="string"]];function ft(t){for(let[e,n]of gt){if(!(e in t))return`${e} (absent)`;if(!n(t[e]))return`${e} (${JSON.stringify(t[e])})`}return null}function mt(t){let e;try{e=JSON.parse(t)}catch{return{status:"unavailable"}}if(e===null||typeof e!="object"||Array.isArray(e))return{status:"unavailable"};let n=e.existing_status;if(n==="not_in_this_org")return{status:"not_in_this_org"};if(n!=="found")return{status:"unavailable"};let o=e.existing;return o===null||typeof o!="object"||Array.isArray(o)?{status:"unavailable"}:ft(o)!==null?{status:"unavailable"}:{status:"found",row:o}}function xe(t){return t===null?!0:typeof t=="number"?Number.isFinite(t):typeof t!="string"?!1:/[^\s]/.test(t)&&Number.isFinite(Number(t))}function J(t,e){return t===null?e===null:e===null?!1:Number(e)===t}function Ce(t,e){if(typeof e!="string")return!1;let n=Date.parse(t),o=Date.parse(e);return Number.isFinite(n)&&Number.isFinite(o)&&n===o}function yt(t,e){let n=e;return n.inference_id===t.inference_id&&n.model_id===t.model_id&&(n.variant??null)===t.variant&&n.task===t.task&&n.http_status===t.http_status&&Ce(t.started_at,n.started_at)&&Ce(t.completed_at,n.completed_at)&&J(t.prompt_tokens,n.prompt_tokens)&&J(t.completion_tokens,n.completion_tokens)&&J(t.output_units,n.output_units)}function wt(t,e){if(t===null||typeof t!="object"||Array.isArray(t))throw new g(`${e} answered a "recorded" that is not an object: ${JSON.stringify(t)}`);let n=t,o=n.inference_id;if(typeof o!="string"||o==="")throw new g(`${e} answered a row with no inference_id: ${JSON.stringify(t)}`);if(!("cost_nano_usd"in n))throw new g(`${e} answered a row with no cost_nano_usd field: ${JSON.stringify(t)}`);let r=n.cost_nano_usd;if(r!=null&&!(typeof r=="number"&&Number.isInteger(r)&&r>=0))throw new g(`${e} answered cost_nano_usd ${JSON.stringify(r)}, which is neither a non-negative integer nor null`);let a=u=>{if(!(u in n))throw new g(`${e} answered a row with no ${u} field: ${JSON.stringify(t)}`);let f=n[u];if(f===null)return null;if(typeof f!="string")throw new g(`${e} answered a non-string ${u}: ${JSON.stringify(f)}`);if(!/[^\s]/.test(f))throw new g(`${e} answered a BLANK ${u} (${JSON.stringify(f)}) \u2014 it satisfies "is a string" while naming nothing, which is exactly what inference_requests_priced_attribution_check exists to forbid`);return f},s=a("price_version"),i=a("unpriced_reason"),l=r!=null;if(l&&(s===null||i!==null))throw new g(`${e} answered a PRICED row (cost_nano_usd ${String(r)}) that is not cleanly attributed \u2014 price_version ${JSON.stringify(s)}, unpriced_reason ${JSON.stringify(i)}`);if(!l&&(s!==null||i===null))throw new g(`${e} answered an UNPRICED row that does not say why \u2014 price_version ${JSON.stringify(s)}, unpriced_reason ${JSON.stringify(i)}. NULL means NOT YET PRICED, and the reason is how an operator learns that traffic is being served which nobody can bill.`);return{inference_id:o,cost_nano_usd:r===void 0?null:r,price_version:s,unpriced_reason:i}}var F=class{constructor(e){this.opts=e;this.fetchImpl=e.fetchImpl??fetch}opts;fetchImpl;async write(e,n){let o=`${this.opts.apiUrl.replace(/\/+$/,"")}${ht}`,r;try{r=await this.fetchImpl(o,{method:"POST",headers:{"content-type":"application/json",authorization:`Bearer ${e}`},body:JSON.stringify(n),signal:AbortSignal.timeout(pt)})}catch(l){throw new g(`the uRun ledger surface at ${o} could not be reached: ${l instanceof Error?l.message:String(l)}`)}if(r.status===409){let l=await r.text().catch(()=>""),u=mt(l);throw u.status==="found"&&yt(n,u.row)?new q(u.row,`the uRun control plane already holds THIS PROXY'S OWN row for inference_id ${n.inference_id} \u2014 every billable field matches the row being re-sent, so an earlier attempt landed and this retry is a no-op. Nothing was written and nothing was overwritten.`):u.status==="not_in_this_org"?new D(`inference_id ${n.inference_id} is ALREADY TAKEN by a ledger row this org cannot read. The id space is a global primary key, so this is not a retry and cannot be this proxy's own write \u2014 either two orgs collided on a uuid, or a row was written for this id by somebody else.`,"duplicate_foreign"):new D(`the uRun control plane already holds a ledger row for inference_id ${n.inference_id} that is NOT the one being written${u.status==="unavailable"?" (and it could not be read back to compare)":""} \u2014 nothing was written and nothing was overwritten. Either this proxy wrote twice, or a row was fabricated ahead of it (this write is authenticated by the CALLER'S OWN org key). ${l.slice(0,400)}`)}if(r.status===401||r.status===403)throw await r.text().catch(()=>{}),new V("the uRun org API key was rejected by the uRun control plane when writing the inference ledger \u2014 the request was served but produced no billable row");if(!r.ok){let l=await r.text().catch(u=>`<body unreadable: ${u instanceof Error?u.message:String(u)}>`);throw new g(`the uRun ledger surface at ${o} answered HTTP ${r.status}: ${l.slice(0,400)}`)}let a;try{a=await r.json()}catch(l){throw new g(`the uRun ledger surface at ${o} answered a body that is not JSON: ${l instanceof Error?l.message:String(l)}`)}let s=a?.recorded;if(s===void 0)throw new g(`the uRun ledger surface at ${o} answered a body with no "recorded" field`);let i=wt(s,`the uRun ledger surface at ${o}`);if(i.inference_id!==n.inference_id)throw new g(`the uRun ledger surface at ${o} answered for inference_id ${i.inference_id} when asked about ${n.inference_id}`);return i}};p();import{createServer as vt}from"http";p();var K="/inference-usage",Oe=25e3;var bt=new Set(["Error","TypeError","SyntaxError","TimeoutError"]);function ke(t){if(!(t instanceof Error))return`a ${typeof t}`;let e=bt.has(t.name)?t.name:"Error",n=t.cause?.code;return typeof n=="string"&&/^[A-Z][A-Z0-9_]{0,31}$/.test(n)?`${e} (${n})`:e}var G=class{constructor(e){this.opts=e;this.fetchImpl=e.fetchImpl??fetch}opts;fetchImpl;async lookup(e,n){let o=`${this.opts.apiUrl.replace(/\/+$/,"")}${K}`,r;try{r=await this.fetchImpl(o,{method:"POST",headers:{"content-type":"application/json",authorization:`Bearer ${e}`},body:JSON.stringify({inference_ids:n}),signal:AbortSignal.timeout(Oe)})}catch(s){throw new A(`the uRun usage surface (POST ${K}) could not be reached within ${Oe}ms: ${ke(s)}`)}if(r.status===401||r.status===403)throw await r.text().catch(()=>{}),new m("the uRun org API key was rejected by the uRun control plane \u2014 check the key and that it is not revoked");if(!r.ok){let s=await r.text().catch(()=>null),i=s===null?"body unreadable":`${s.length}-character body`;throw new A(`the uRun usage surface (POST ${K}) answered HTTP ${r.status} (${i})`)}let a;try{a=await r.json()}catch(s){throw new A(`the uRun usage surface (POST ${K}) answered HTTP ${r.status} with a body that is not JSON: ${ke(s)}`)}return le(a,this.opts.onRejectedRow)}};var _t=5e3,Et=3e3,Rt="/api/client-tokens";function Y(t,e,n){t.writeHead(e,{"content-type":"application/json"}),t.end(JSON.stringify(n))}function Ae(t,e,n,o){Y(t,e,{error:{message:n,type:o}})}async function St(t,e=fetch){let n=`${t.replace(/\/+$/,"")}${Rt}`;try{let o=await e(n,{method:"POST",headers:{"content-type":"application/json"},body:"{}",signal:AbortSignal.timeout(Et)});return o.status===404?{ready:!1,detail:`${n} answered 404 \u2014 this is not a uRun control plane`}:o.status>=500?{ready:!1,detail:`${n} answered HTTP ${o.status}`}:{ready:!0,detail:`${n} answered HTTP ${o.status}`}}catch(o){return{ready:!1,detail:`${n} unreachable: ${o instanceof Error?o.message:String(o)}`}}}function _n(t){let e=t.resolver??new U({gatewayUrl:t.baseUrl}),n=t.decisionSecret??"",o=t.sharedOrgKey&&t.sharedOrgId?{apiKey:t.sharedOrgKey}:void 0,r=t.registry??new $({baseUrl:t.baseUrl,apiUrl:t.apiUrl,catalog:t.catalog,...n!==""?{decisionIntake:{secret:n,...o?{shared:o}:{}}}:{}});n!==""&&process.stderr.write(`inference-proxy: stateless direct path ENABLED \u2014 caller-org decisions dial ready runtimes directly (no session mint)
4
- `);let a=t.probeControlPlane??(()=>St(t.baseUrl)),s=t.usageClient??new G({apiUrl:t.apiUrl}),i=t.ledgerClient??new F({apiUrl:t.apiUrl}),l=new T(t.defaultMetrics===void 0?{}:{defaultMetrics:t.defaultMetrics}),u=async c=>e.resolve(S(c.headers.authorization)),f=c=>c instanceof m?{status:c.status,openaiType:"authentication_error",anthropicType:"authentication_error"}:c instanceof _?{status:502,openaiType:"upstream_error",anthropicType:"api_error"}:null,x=me({clients:async c=>r.clientsFor(await u(c)),usage:{authorize:c=>S(c.headers.authorization),lookup:(c,d)=>s.lookup(c,d)},statusOf:f,...t.requestLog?{requestLog:t.requestLog}:{},observe:c=>l.observe(c),ledger:{write:(c,d)=>i.write(S(c.headers.authorization),d),observe:c=>l.observeLedger(c),...t.ledgerLog?{report:t.ledgerLog}:{}}}),v=null,E=vt((c,d)=>{let h=new URL(c.url??"/","http://localhost");(async()=>{if(c.method==="GET"&&h.pathname==="/healthz"){Y(d,200,{ok:!0});return}if(c.method==="GET"&&h.pathname==="/readyz"){let O=Date.now();if(!v||O-v.at>=_t){let Te=await a();v={at:O,...Te}}Y(d,v.ready?200:503,{ok:v.ready,control_plane:v.detail});return}let y=h.pathname==="/v1"||h.pathname.startsWith("/v1/"),C=h.pathname.startsWith("/partners/");if(!y&&!C){Ae(d,404,`no route for ${c.method} ${h.pathname} \u2014 this endpoint serves the OpenAI-compatible /v1 surface, the partner adapter surface (/partners/...), the WS upgrade paths (/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent and /v1/realtime), plus /healthz and /readyz`,"invalid_request_error");return}x(c,d)})().catch(y=>{if(!d.headersSent){Ae(d,500,String(y instanceof Error?y.message:y),"proxy_error");return}d.end()})}),B=t.conversations??new H({baseUrl:t.baseUrl,apiUrl:t.apiUrl,catalog:t.catalog}),Ie=[Z(E,{authenticate:async c=>{let d=await e.resolve(c),h=B.connectionFor(d);return{clients:h.clients,scope:M(d.apiKey),onBindResumeHandle:h.bindResumeHandle,clientsForResume:h.clientsForResume,release:h.release}}}),te(E,{clients:async c=>r.clientsFor(await u(c)),statusOf:f}).close,oe(E,{resolver:{resolve:async(c,d)=>{let h=await e.resolve(S(c.headers.authorization)),y=B.connectionFor(h),C;try{C=await ie(y.clients,se(c),d)}catch(O){throw y.release().catch(()=>{}),O}return{...C,release:y.release}}}})];E.on("upgrade",(c,d)=>{let h=new URL(c.url??"/","http://localhost");h.pathname===X||h.pathname===ee||h.pathname===re||d.destroyed||(d.write(`HTTP/1.1 404 Not Found\r
5
- Connection: close\r
6
- \r
7
- no WS route for ${h.pathname} \u2014 this endpoint serves only the Gemini Live, /v1/responses, and /v1/realtime upgrade paths`),d.destroy())});let Ne=async()=>{for(let c of Ie)await c();await B.detachAll()};return{server:E,metricsServer:ve(l),metrics:l,edgeIdentityServer:Re(),registry:r,closeAll:()=>r.closeAll(),closeWebSockets:Ne}}async function En(t,e,n){n&&await n();let o=new Promise(r=>t.close(()=>r()));t.closeAllConnections(),await o,await pe()||ge(),await e()}export{It as a,be as b,I as c,Le as d,He as e,Me as f,Ke as g,Ge as h,N as i,T as j,ve as k,$t as l,ze as m,je as n,We as o,Je as p,_e as q,Ve as r,Ye as s,Qe as t,Re as u,m as v,_ as w,S as x,tt as y,nt as z,U as A,ot as B,P as C,Gt as D,Bt as E,lt as F,M as G,$ as H,Se as I,ct as J,L as K,H as L,ht as M,pt as N,D as O,g as P,wt as Q,F as R,_t as S,Et as T,_n as U,En as V};
@@ -1 +0,0 @@
1
- import{c as a}from"./chunk-YSFSRI3D.js";a();function _(e,t,o,n){let r=e.response??{},i={request_id:o,consumer_id:n,stream:!0,kind:"chat",messages:t};return typeof r.instructions=="string"&&(i.instructions=r.instructions),Array.isArray(r.modalities)&&(i.modalities=r.modalities),typeof r.temperature=="number"&&(i.temperature=r.temperature),typeof r.max_output_tokens=="number"&&(i.max_output_tokens=r.max_output_tokens),r.tools!==void 0&&(i.tools=r.tools),i}function f(e,t,o){let n={request_id:t,consumer_id:o,stream:!!e.stream,kind:"responses",input:e.input};return e.model&&(n.model=e.model),e.tools!==void 0&&(n.tools=e.tools),e.tool_choice!==void 0&&(n.tool_choice=e.tool_choice),typeof e.temperature=="number"&&(n.temperature=e.temperature),typeof e.max_output_tokens=="number"&&(n.max_output_tokens=e.max_output_tokens),typeof e.top_p=="number"&&(n.top_p=e.top_p),Array.isArray(e.stop)&&e.stop.length>0&&(n.stop=e.stop),typeof e.reasoning_effort=="string"&&(n.reasoning_effort=e.reasoning_effort),typeof e.instructions=="string"&&(n.instructions=e.instructions),e.chat_template_kwargs!==void 0&&(n.chat_template_kwargs=e.chat_template_kwargs),e.response_format!==void 0&&(n.response_format=e.response_format),typeof e.voice=="string"&&(n.voice=e.voice),n}a();function y(e){if(e instanceof Error)return{type:"error",error:{type:"urun_error",code:e.name||null,message:e.message}};if(e&&typeof e=="object"&&e.t==="error"){let t=e,o=t.body??{},n=o.error,r=n?.message??o.message??`unreadable serve error body: ${JSON.stringify(t.body??null)}`;return{type:"error",error:{type:n?.type??"urun_error",code:t.code??n?.code??null,message:r}}}return{type:"error",error:{type:"urun_error",code:null,message:String(e)}}}a();a();var w="llm-resp";async function*m(e,t){let o=`${w}:${t}`,n=`resp_${t}`;yield{type:"response.created",response:{id:n,status:"in_progress"}};let r=new Map;try{for await(let i of e.stream(o).messages()){let s=i;if(s.t==="delta"){if(typeof s.delta=="string"&&(yield{type:"response.output_text.delta",item_id:n,delta:s.delta}),typeof s.reasoning=="string"&&(yield{type:"response.reasoning_text.delta",item_id:n,delta:s.reasoning}),Array.isArray(s.tool_calls))for(let u of s.tool_calls){let d=typeof u.index=="number"?u.index:0,c=r.get(d)??{};u.id&&(c.id=u.id),u.function?.name&&(c.name=u.function.name),r.set(d,c),yield{type:"response.function_call_arguments.delta",item_id:`fc_${t}_${d}`,tool_index:d,call_id:c.id,name:c.name,delta:u.function?.arguments??""}}}else if(s.t==="response"){yield{type:"response.completed",response:{id:n,status:"completed",...s.body}};return}else if(s.t==="error"){yield y(s);return}}}finally{try{e.doc("llm").set({requests:{[t]:null}})}catch{}}}a();var k=class e{constructor(t,o=e.DEFAULT_DOC_SYNC_GATE_TIMEOUT_MS){this.session=t;this.docSyncTimeoutMs=o;this.sessionTag=t.sessionId??globalThis.crypto.randomUUID()}session;docSyncTimeoutMs;sessionTag;static DEFAULT_DOC_SYNC_GATE_TIMEOUT_MS=15e3;get consumerId(){return this.session.consumerId}async ensureDocSynced(){let t=this.session.doc("llm");if(t.synced===!0||typeof t.onSynced!="function")return;let o;await new Promise((r,i)=>{let s=t.onSynced(()=>{clearTimeout(o),r()});o=setTimeout(()=>{s(),i(new Error(`[urun] llm doc plane never synced within ${this.docSyncTimeoutMs}ms \u2014 the request envelope write is withheld rather than raced into a possibly-lost pre-sync write (resumed-attach first-write-lost, prod-usw2 2026-09-23)`))},this.docSyncTimeoutMs)})}write(t){t.session_tag=this.sessionTag,this.session.doc("llm").set({requests:{[t.request_id]:{payload:t,consumer_id:t.consumer_id,stream:t.stream}}})}sendResponses(t,o){return this.writeGated(o,()=>{let n=f(t,o,this.consumerId);return this.write(n),m(this.session,o)})}sendResponseCreate(t,o,n){return this.writeGated(n,()=>{let r=_(t,o,n,this.consumerId);return this.write(r),m(this.session,n)})}async writeGated(t,o){try{await this.ensureDocSynced()}catch(n){throw new Error(`[urun] responses dial ${t} aborted before its envelope write: ${n instanceof Error?n.message:String(n)}`)}return o()}};export{_ as a,f as b,y as c,m as d,k as e};
@@ -1 +0,0 @@
1
- import{a as C}from"./chunk-K23AZPI4.js";import{a as L}from"./chunk-YXK42SKC.js";import{a as x,b as I,e as v,f as G,g as D,h as U,j as M}from"./chunk-WCMX5VOF.js";import{c as E,f as k,g as R,p as F}from"./chunk-6M5JK4YC.js";import{c as w}from"./chunk-YSFSRI3D.js";w();import{App as H,createClientToken as Y}from"@urun-sh/core";w();import{SessionFailedError as W}from"@urun-sh/core";var b=2;function q(e){let s=e?.type;return s==="response.created"||s==="response.in_progress"}function K(e){let s=e?.type;return s==="response.completed"||s==="error"}function $(e){return async function(i,p){return(async function*(){for(let r=1;r<=b;r++){let{app:d,entry:c,identity:l}=await e.sessionFor(i.model);p?.({sessionId:e.sessionIdOf?.(c)??null,identity:l});let h=!1,a=!1;try{let n=await e.createStream(c,i);for await(let o of n){if(K(o)&&(h=!0),q(o)){if(r>1)continue}else a=!0;yield o}}catch(n){if(n instanceof W&&(n.kind==="never-live"||n.kind==="create-failed")){try{e.onAdmissionVerdict?.(d,c,n)}catch{}throw n}if(r<b&&!a){await e.evict(d,c);continue}throw n}if(h)return;if(r<b&&!a){await e.evict(d,c);continue}return}})()}}w();var J=3,Q="llm",X=new Set(["paused","error"]);function j(e,s){let i=e.doc(Q),p=typeof i.onConnectionState=="function",u=typeof e.onPhase=="function";if(!p&&!u)throw new Error(`session for app "${s}" exposes neither a doc connection-state surface nor onPhase \u2014 session-gone detection requires @urun-sh/core Session (SessionDocument.onConnectionState / Session.onPhase); pooling a session with no native lifecycle signal would silently swallow requests dispatched after the platform closes it`);let r=null,d=!1,c=new Set,l=[],h=a=>{if(!r){r=a;for(let n of[...c])n(a);c.clear()}};return p&&l.push(i.onConnectionState(a=>{d&&(a.consecutiveFailures<J||h(new v(`the pooled session for app "${s}" is gone: its doc backhaul failed ${a.consecutiveFailures} consecutive times (last close ${a.lastCloseCode===null?"by provider liveness reap":`code ${a.lastCloseCode}`}${a.lastCloseReason?`, reason ${JSON.stringify(a.lastCloseReason)}`:""}) \u2014 the platform closed the session (idle timeout or a serve-side crash). The entry is evicted; retry to get a fresh session.`)))})),u&&l.push(e.onPhase(a=>{if(a.name==="live"){d=!0;return}d&&X.has(a.name)&&h(new v(`the pooled session for app "${s}" lost its compute binding (phase "${a.name}"). The entry is evicted; retry to get a fresh session. The named session itself is not over \u2014 it stays attachable by name.`))})),{gone:()=>r,onGone(a){return r?(a(r),()=>{}):(c.add(a),()=>c.delete(a))},dispose(){c.clear();for(let a of l.splice(0))a()}}}async function*A(e,s){let i=s.gone();if(i)throw i;let p=e[Symbol.asyncIterator](),u,r=new Promise((d,c)=>{u=s.onGone(l=>c(l))});r.catch(()=>{});try{for(;;){let d=await Promise.race([p.next(),r]);if(d.done)return;yield d.value}}finally{u?.(),p.return?.(void 0)}}function V(e,s,i,p){let u=e[i];if(typeof u!="function")throw new Error(`app "${s}" has no function "${i}" (set URUN_FUNCTION)`);let r=p?u(void 0,p):u();if(typeof r.end!="function")throw new Error(`app "${s}" function "${i}" returned a session without end()`);return r}function z(e,s,i){let{baseUrl:p,orgId:u,auth:r}=e,d=typeof e.subject=="function"?e.subject():e.subject,c=i.startsWith(D)&&d!==void 0?`${d}:speech`:d,l=s.fnName??e.fnName,h=e.sessionStartOptions?.(i),a=s.sharedApp?{sharedApp:!0,...h}:h,n=m=>({session:m,responses:new L(m),gone:j(m,i)});if(r.lane==="jwt"){let m=H(s.appSlug,{baseUrl:p,orgId:u,jwt:r.jwt});return n(V(m,s.appSlug,l,a))}let{apiKey:o,gatewayUrl:t}=r,f=H(s.appSlug,{baseUrl:p,orgId:u,getAccessToken:async()=>(await Y(o,{baseUrl:t,expiresIn:300,allowedFunctions:[`${s.appSlug}/${l}`],...c===void 0?{}:{subject:c},...s.sharedApp?{sharedApp:!0}:{}})).token});return n(V(f,s.appSlug,l,a))}function Z(e){let s=e.session.id;if(typeof s!="string"||s.length===0)throw new Error("pooled session exposes no stable session id \u2014 the session-identity seam requires @urun-sh/core Session (session.id)");return s}function ee(e){if(e.kind==="never-live")return!0;if(e.kind!=="create-failed")return!1;let s=e.status.error?.httpStatus,i=e.status.error?.code;return s===410||s===409&&(i==="cancelled"||i==="expired")?!0:i==="client_ended"}var ne=new Set(["error","paused"]);function ge(e){let{auth:s,catalog:i}=e,p=null,u=(d,c)=>{let l=z(e,d,c);return e.onSessionOpen&&typeof l.session.id=="string"&&l.session.id!==""&&e.onSessionOpen(c,l.session.id),l.gone.onGone(()=>{p?.evict(c,l)}),l},r=new M({sharedOnlyListing:e.sharedOnlyListing===!0,defaultApp:e.defaultApp,fnName:e.fnName,openSession:u,closeSession:async d=>{if(d.gone.dispose(),e.release==="detach"){if(typeof d.session.detach!="function")throw new Error('pooled session exposes no native detach() \u2014 the release:"detach" lane requires @urun-sh/core Session.detach');d.session.detach();return}await d.session.end()},listApps:s.lane==="api-key"?()=>G({apiUrl:e.apiUrl,apiKey:s.apiKey}):null,listCatalog:i?()=>C(i):null,listPrices:i?()=>I(i):null,shared:s.lane==="api-key"?()=>x({apiUrl:e.apiUrl,apiKey:s.apiKey}):null,sessionKey:Z});return p=r,e.defaultApp!==null&&e.statelessDirect!==!0&&r.seed(e.defaultApp,u({appSlug:e.defaultApp},U({appSlug:e.defaultApp}))),r}async function P(e,s){let i=e.whenLive;if(typeof i!="function")throw new Error("pooled session exposes no native whenLive() \u2014 the audio lane must wait for the session's own admission (cold wake: queued \u2192 demand \u2192 promote) before wiring media; a session without the core Session.whenLive surface cannot be served audio");await i.call(e,s===void 0?void 0:{signal:s})}async function N(e){if(typeof e.cancel=="function")try{await e.cancel();return}catch{}if(typeof e.detach!="function")throw new Error("pooled session exposes neither cancel() nor detach() \u2014 the client-gone abandon path requires the @urun-sh/core Session native lifecycle surface");e.detach()}function B(e){return e.reason instanceof Error?e.reason:new Error("the client socket closed")}function ve(e,s=R,i,p){let u=new WeakMap,r=new WeakMap,d=new WeakMap,c=new WeakMap,l=n=>{let o=n.session.id;return typeof o=="string"&&o.length>0?o:null},h=(n,o)=>{let t=u.get(n);return t||(t=(async()=>{await P(n.session,o);let f=await E(n.session,await s());return Object.assign(f,{sessionId:n.session.id})})(),u.set(n,t),t.catch(()=>u.delete(n))),t},a=n=>{let o=c.get(n);return o||(o=(async()=>{let t=await h(n);return{appendInputAudio:f=>t.appendInputAudio(f),onOutputAudio:f=>t.onOutputAudio(f),sessionId:l(n)}})(),c.set(n,o),o.catch(()=>c.delete(n))),o};return{createResponse:$({sessionFor:n=>e.sessionFor(n),sessionIdOf:n=>l(n),evict:(n,o)=>e.evict(n,o),onAdmissionVerdict:(n,o,t)=>{ee(t)&&(e.evict(n,o),i?.(n,t.kind==="never-live"?"never-live":"create-failed"))},createStream:async(n,o)=>(await P(n.session),A(await n.responses.responses.create(o),n.gone))}),listModels:()=>e.modelList(),openRouterModels:()=>e.openRouterModels(),supportsImages:(n,o)=>e.supportsImages(n,o),listImageModels:()=>e.imageModelList(),catalogIdentity:n=>e.catalogIdentityFor(n),servingAppSlug:async n=>await e.servingAppSlugFor(n),supportsAudio:n=>e.supportsAudio(n),supportsSpeech:n=>e.supportsSpeech(n),speechVoices:n=>e.speechVoices(n),openAudio:async(n,o)=>{let{entry:t}=await e.sessionFor(n);if(o?.aborted)throw await N(t.session),B(o);let f=h(t,o);return o===void 0?f:new Promise((m,S)=>{let y=()=>{N(t.session).catch(()=>{}),S(B(o))};o.addEventListener("abort",y,{once:!0});let O=()=>o.removeEventListener("abort",y);f.then(g=>{O(),m(g)},g=>{O(),S(g)})})},openAudioOn:async n=>a((await e.sessionForHandle(n)).entry),openVideo:async n=>{let{entry:o}=await e.sessionFor(n),t=r.get(o);return t||(t=k(o.session),r.set(o,t)),t},openVideoOut:async n=>{let{entry:o}=await e.sessionFor(n),t=d.get(o);return t||(t=F(o.session),d.set(o,t)),t},sessionHandle:async n=>(await e.handleFor(n)).handle,speechSessionHandle:async n=>(await e.speechHandleFor(n)).handle,evictSpeechSession:async n=>{let{app:o,entry:t}=await e.sessionForHandle(n);await e.evict(o,t)},createResponseOn:async(n,o)=>{let{entry:t}=await e.sessionForHandle(n);return await P(t.session),A(await t.responses.responses.create(o),t.gone)},onSessionEnd:async(n,o)=>{let{entry:t}=await e.sessionForHandle(n),f=t.session;if(typeof f.onPhase!="function")throw new Error("pooled session exposes no onPhase \u2014 session-end (goAway) wiring requires @urun-sh/core Session");let m=!1;return f.onPhase(S=>{if(m||!ne.has(S.name))return;m=!0;let y=f.endsAt??null;o({timeLeftMs:y?Math.max(0,y.getTime()-Date.now()):null,reason:S.reason??`session ${S.name}`})})},...p?{stateless:p}:{}}}export{ge as a,ve as b};