@urun-sh/openai 0.6.17 → 0.6.18

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-Dd-h46sQ.cjs';
3
- export { D as DialObserver, a as DialReport, g as ProxyIdentity, h as ProxyVideoOutLane, S as SessionGoneError, U as UnknownModelError } from '../responses-turn-Dd-h46sQ.cjs';
2
+ import { f as ProxyHandlerOptions, P as ProxyClients } from '../responses-turn-B91w2YKn.cjs';
3
+ export { D as DialObserver, a as DialReport, g as ProxyIdentity, h as ProxyVideoOutLane, S as SessionGoneError, U as UnknownModelError } from '../responses-turn-B91w2YKn.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';
@@ -339,6 +339,17 @@ interface RealtimeBinding<S = unknown> {
339
339
  };
340
340
  /** `model` the binding names for this conversation (overrides session config default). */
341
341
  model?: string;
342
+ /**
343
+ * The GA transcription-session intent (`?intent=transcription`): this
344
+ * connection is a TRANSCRIPTION session — transcription events flow
345
+ * without the client opting in, and `response.create` answers the loud
346
+ * `unsupported` error (the connection stays open) because there are no
347
+ * model turns to create. Set by the shared binding composition (binding.ts)
348
+ * only when the resolver's URL carried the intent AND the model's catalog
349
+ * row vouches stt (the binding carries `turns`). Absent = the realtime
350
+ * (voice) session, today's behaviour.
351
+ */
352
+ intent?: 'transcription';
342
353
  /** True when `session` is a resumed authorized session (vs a fresh private one). */
343
354
  resumed?: boolean;
344
355
  /**
@@ -350,8 +361,14 @@ interface RealtimeBinding<S = unknown> {
350
361
  * `turn_start` IS the native barge-in — the adapter drives server-VAD
351
362
  * emulation, real transcription events, and model-visible committed audio
352
363
  * from it. Absent = the backend wired no turn lane: manual mode only.
364
+ *
365
+ * The CONSUMER passes its own lifecycle `signal` (the realtime connection's
366
+ * turn-lane controller): the lane must END its stream subscription when it
367
+ * aborts — the transport races its pulls against it — so the subscription
368
+ * never outlives the consumer. A consumer that passes none gets an
369
+ * un-raced lane (its own teardown is then its own problem).
353
370
  */
354
- turns?: () => AsyncIterable<{
371
+ turns?: (signal?: AbortSignal) => AsyncIterable<{
355
372
  kind: 'delta' | 'final' | 'turn_start' | 'turn_end' | 'turn_eager_end' | 'turn_resumed';
356
373
  text?: string;
357
374
  confidence?: number;
@@ -446,10 +463,39 @@ declare class OpenAIRealtimeConnection<S = unknown> {
446
463
  private closed;
447
464
  /** The backend's turn/transcript lane consumer (binding.turns), if wired. */
448
465
  private turnLaneAbort;
449
- /** Accumulated transcript of the OPEN user turn (delta/final events). */
466
+ /**
467
+ * The CONSUMER-SIDE turn-lane iterator — held so teardown can end the lane
468
+ * subscription through the async-iterator return() protocol when it is
469
+ * parked at a YIELD (no pending pull for the abort to unwind; a plain
470
+ * for-await has no handle to close). Null once the consumer loop exits.
471
+ */
472
+ private turnLaneIterator;
473
+ /** Accumulated transcript of the OPEN user turn (delta/final events). The
474
+ * `final` flag says the engine finalized the accumulated text (its last
475
+ * transcript event was a `final`) — a manual commit may complete on it. */
450
476
  private turnTranscript;
451
477
  /** Transcript of the LAST committed turn (the model-visible commit). */
452
478
  private lastCommittedTranscript;
479
+ /**
480
+ * A manual transcription-session commit whose item is still awaiting the
481
+ * lane's final text (the item id of the committed item): interim deltas
482
+ * meanwhile carry THIS id, and the lane's next `final`/`turn_end`
483
+ * discharges exactly one `…transcription.completed` for it. No timers.
484
+ */
485
+ private pendingTranscriptItemId;
486
+ /**
487
+ * The engine turn a manual transcription-session commit ALREADY discharged
488
+ * (the discharged item's id): the lane's `final` completed the committed
489
+ * item (or the commit completed it on the lane's already-final text) while
490
+ * the engine's `turn_end` for THAT utterance is still to come. That
491
+ * `turn_end` is the boundary signal only — the server-side commit it
492
+ * would otherwise open mints a SECOND item for the same utterance and
493
+ * clears audio the client already appended for the next one. Consumed by
494
+ * that `turn_end` itself (one-shot); invalidated by the next `turn_start`
495
+ * (a new engine utterance opened — its boundary was not the discharged
496
+ * one).
497
+ */
498
+ private manuallyDischargedTurnItem;
453
499
  constructor(opts: {
454
500
  ws: WebSocket;
455
501
  binding: RealtimeBinding<S>;
@@ -464,8 +510,33 @@ declare class OpenAIRealtimeConnection<S = unknown> {
464
510
  * is the native barge-in, turn_end is the native commit, deltas/finals are
465
511
  * the REAL input transcription. This is the server-VAD primitive, not an
466
512
  * emulation gap: the voice engine itself decides turn boundaries.
513
+ *
514
+ * TEARDOWN ENDS THE SUBSCRIPTION FROM EVERY PARKING STATE, which a plain
515
+ * `for await` cannot do (its `break` only runs after the next event — a
516
+ * quiet engine parks the pull forever, and a `return()` enqueued behind a
517
+ * pending `next()` waits for it to settle):
518
+ * - the `abort` threaded into `turns()` unwinds a PENDING pull (the
519
+ * transport races its `messages()` pulls against it);
520
+ * - the HELD consumer iterator's `return()` (teardown) closes a lane
521
+ * parked at a YIELD with no pending pull.
467
522
  */
468
523
  private startTurnLane;
524
+ /** True when this connection IS a transcription session (the GA verdict:
525
+ * the binding's `?intent=transcription`, or the client's own
526
+ * `session.type: "transcription"` — the latter only accepted behind a
527
+ * turns lane, so a transcription session always has a transcript source). */
528
+ private isTranscriptionSession;
529
+ /** True when input-transcription events flow: the client opted in on a
530
+ * voice session, or ALWAYS on a transcription session (no opt-in needed). */
531
+ private transcriptionEventsOn;
532
+ /**
533
+ * Discharge a pending manual transcription commit: the ONE
534
+ * `…transcription.completed` for the committed item, carrying the text the
535
+ * lane delivered for its audio, and a reset shared accumulator (the
536
+ * utterance is client-delivered; the next utterance starts clean). The
537
+ * lane's own boundary events drive this — no timers anywhere.
538
+ */
539
+ private completePendingTranscription;
469
540
  /** One backend turn/transcript event → REAL GA protocol events. */
470
541
  private onTurnEvent;
471
542
  /** The committed turn's transcript, consumed once (null = none). */
@@ -647,8 +718,21 @@ declare function realtimeModelOf(req: IncomingMessage): string | undefined;
647
718
  * (`sessionHandle` once → `openAudioOn(handle, signal)` +
648
719
  * `createResponseOn(handle, …)`), so no turn on this connection can ever
649
720
  * resolve a different pooled session than the bridge.
721
+ * - An stt verdict (the catalog `task` gate `supportsTranscription`, the
722
+ * SAME router-backed oracle the /v1/audio/transcriptions lane rides) with
723
+ * no `openTurnsOn` seam ⇒ 501 — a transcription-capable binding without
724
+ * its transcript source is never wired silently. A row that OVERRIDES
725
+ * the platform's default transcript stream (`engine_args.transcript_stream`,
726
+ * resolved through `transcriptStreamFor` when the seam is wired) is the
727
+ * same-shape 501 — the lane never listens on a stream the engine does
728
+ * not write.
729
+ * - `?intent=transcription` on anything but an stt row ⇒ 400 naming the
730
+ * model; any other intent value ⇒ 400 naming the accepted values. Every
731
+ * refusal lands BEFORE the pooled session is resolved (`sessionHandle`)
732
+ * and the bridge opens — a refused upgrade admits nothing. On an stt row
733
+ * the intent is recorded on the binding (a transcription session).
650
734
  */
651
- declare function realtimeBindingFor(clients: Pick<ProxyClients, 'sessionHandle' | 'createResponseOn'> & Partial<Pick<ProxyClients, 'supportsAudio' | 'openAudioOn'>>, model: string | undefined, signal?: AbortSignal): Promise<RealtimeBinding<ProxyClients>>;
735
+ declare function realtimeBindingFor(clients: Pick<ProxyClients, 'sessionHandle' | 'createResponseOn'> & Partial<Pick<ProxyClients, 'supportsAudio' | 'openAudioOn' | 'supportsTranscription' | 'openTurnsOn' | 'transcriptStreamFor'>>, model: string | undefined, signal?: AbortSignal, intent?: string): Promise<RealtimeBinding<ProxyClients>>;
652
736
  /**
653
737
  * The LOCAL lane resolver: fixed pooled clients, optional fixed apiKey (the
654
738
  * 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-B0zlsQGn.js';
3
- export { D as DialObserver, a as DialReport, g as ProxyIdentity, h as ProxyVideoOutLane, S as SessionGoneError, U as UnknownModelError } from '../responses-turn-B0zlsQGn.js';
2
+ import { f as ProxyHandlerOptions, P as ProxyClients } from '../responses-turn-FcEDaymF.js';
3
+ export { D as DialObserver, a as DialReport, g as ProxyIdentity, h as ProxyVideoOutLane, S as SessionGoneError, U as UnknownModelError } from '../responses-turn-FcEDaymF.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';
@@ -81,9 +81,11 @@ interface RealtimeBinding<S = unknown> {
81
81
 
82
82
  model?: string;
83
83
 
84
+ intent?: 'transcription';
85
+
84
86
  resumed?: boolean;
85
87
 
86
- turns?: () => AsyncIterable<{
88
+ turns?: (signal?: AbortSignal) => AsyncIterable<{
87
89
  kind: 'delta' | 'final' | 'turn_start' | 'turn_end' | 'turn_eager_end' | 'turn_resumed';
88
90
  text?: string;
89
91
  confidence?: number;
@@ -128,9 +130,15 @@ declare class OpenAIRealtimeConnection<S = unknown> {
128
130
 
129
131
  private turnLaneAbort;
130
132
 
133
+ private turnLaneIterator;
134
+
131
135
  private turnTranscript;
132
136
 
133
137
  private lastCommittedTranscript;
138
+
139
+ private pendingTranscriptItemId;
140
+
141
+ private manuallyDischargedTurnItem;
134
142
  constructor(opts: {
135
143
  ws: WebSocket;
136
144
  binding: RealtimeBinding<S>;
@@ -142,6 +150,12 @@ declare class OpenAIRealtimeConnection<S = unknown> {
142
150
 
143
151
  private startTurnLane;
144
152
 
153
+ private isTranscriptionSession;
154
+
155
+ private transcriptionEventsOn;
156
+
157
+ private completePendingTranscription;
158
+
145
159
  private onTurnEvent;
146
160
 
147
161
  private takeTurnTranscript;
@@ -188,7 +202,7 @@ declare class OpenAIRealtimeConnection<S = unknown> {
188
202
 
189
203
  declare function realtimeModelOf(req: IncomingMessage): string | undefined;
190
204
 
191
- declare function realtimeBindingFor(clients: Pick<ProxyClients, 'sessionHandle' | 'createResponseOn'> & Partial<Pick<ProxyClients, 'supportsAudio' | 'openAudioOn'>>, model: string | undefined, signal?: AbortSignal): Promise<RealtimeBinding<ProxyClients>>;
205
+ declare function realtimeBindingFor(clients: Pick<ProxyClients, 'sessionHandle' | 'createResponseOn'> & Partial<Pick<ProxyClients, 'supportsAudio' | 'openAudioOn' | 'supportsTranscription' | 'openTurnsOn' | 'transcriptStreamFor'>>, model: string | undefined, signal?: AbortSignal, intent?: string): Promise<RealtimeBinding<ProxyClients>>;
192
206
 
193
207
  declare function fixedClientsResolver(clients: ProxyClients, apiKey?: string): {
194
208
  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-U7EJSC2N.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};
1
+ import{B as l,C as a,D as x,E as s,G as O,H as f,T as I,a as n,b as p,c as m}from"../chunk-3U7YQ7LE.js";import{f as r,g as i}from"../chunk-NYSZ3RGB.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};
@@ -381,7 +381,8 @@ interface ModelPriceRow {
381
381
  }
382
382
 
383
383
  /**
384
- * THE OPENROUTER PROVIDER DOCUMENT — `GET /v1/models?format=openrouter`.
384
+ * THE OPENROUTER PROVIDER DOCUMENT — `GET /partners/openrouter/models`
385
+ * (`GET /v1/models?format=openrouter` answers 308 here for one release).
385
386
  *
386
387
  * OpenRouter's provider monitor polls this document (schema 2.4, per
387
388
  * openrouter.ai/docs/guides/community/for-providers §1) to list a provider's
@@ -610,28 +611,30 @@ declare class UnknownModelError extends Error {
610
611
  declare class SessionGoneError extends Error {
611
612
  }
612
613
  /**
613
- * OpenAI-shaped model list (same shape as models.ts listModels). When the
614
- * catalog oracle is configured, each entry ALSO carries the catalog
615
- * enrichment fields (additive JSON — OpenAI clients ignore unknown fields;
616
- * the OpenRouter + Vercel AI Gateway provider listings need them). Entries
617
- * whose slug matches no catalog row stay at the bare four fields.
614
+ * One `/v1/models` row — a CLOSED allow-list of externally contracted keys
615
+ * (owner ruling, ENG-472: publish what an external contract names, never the
616
+ * implementation). The four OpenAI `Model` fields, plus — only when known —
617
+ * the Hugging Face pair (`context_length`, `pricing`), the TTS `voices` and
618
+ * the `capabilities` object. `name`, `description`, `task`, `engine`,
619
+ * `gpu_spec` and `warm` are GONE from the type so they cannot be set; every
620
+ * row builder goes through `publicModelEntry` (openrouter-doc.ts), which
621
+ * copies exactly these keys.
618
622
  */
619
623
  interface RouterModelEntry {
624
+ /** OpenAI `Model`: the model identifier callers reference in the API. */
620
625
  id: string;
626
+ /** OpenAI `Model`: always "model". */
621
627
  object: 'model';
628
+ /** OpenAI `Model`: the Unix time the model id first appeared in the uRun catalog (0 until the catalog carries it). */
622
629
  created: number;
630
+ /** OpenAI `Model`: the organization that owns the model. */
623
631
  owned_by: string;
624
- /** Display name — the canonical `<model_id>:<variant>` catalog ref. */
625
- name?: string;
626
- /** User-facing description from the catalog (console Endpoints copy). */
627
- description?: string;
628
- /** Catalog modality (chat | code | agent | vl | audio | image | ...). */
629
- task?: string;
630
- /** Serving engine (vllm | sglang | llamacpp | ...). */
631
- engine?: string;
632
- /** The catalog lane this app deploys, e.g. 'rtx6000:1'. */
633
- gpu_spec?: string;
634
- /** Context window in tokens (chat rows carry it in engine_args). */
632
+ /**
633
+ * Hugging Face provider comparison table — its top-level `context_length`
634
+ * read (HF also gates `:fastest`/`:cheapest` on it). Only when the catalog
635
+ * states it and the placements agree (openrouter-doc.ts
636
+ * `placementsContextLength`).
637
+ */
635
638
  context_length?: number;
636
639
  /**
637
640
  * Per-token price, USD per MILLION tokens — the exact shape Hugging Face
@@ -641,8 +644,6 @@ interface RouterModelEntry {
641
644
  * a zero or an estimate here would be a silent lie on a public table.
642
645
  */
643
646
  pricing?: TokenPricing;
644
- /** Idle-floor replica count for a shared-lane model (U6 reconciler). */
645
- warm?: number;
646
647
  /**
647
648
  * The voices a `tts`-task model synthesizes under (ENG-331 — OpenAI's
648
649
  * speech API names a voice per request, so a caller needs the list).
@@ -664,7 +665,7 @@ interface RouterModelEntry {
664
665
  * whose placements disagree, a malformed value, or a slug with no catalog
665
666
  * row omit the object entirely — unknown, never guessed
666
667
  * (openrouter-doc.ts `placementsReasoning`, applied by both listing
667
- * lanes).
668
+ * lanes). The conformance kit's voice_profile check reads it.
668
669
  */
669
670
  capabilities?: {
670
671
  reasoning: boolean;
@@ -674,10 +675,11 @@ interface RouterModelList {
674
675
  object: 'list';
675
676
  data: RouterModelEntry[];
676
677
  }
677
- /** One `/v1/images/models` entry: the OpenAI model object plus the image
678
+ /** One `/v1/images/models` entry: the allow-listed model row plus the image
678
679
  * modes the catalog rows vouch for (the SAME per-placement consensus
679
680
  * {@link ModelRouter.supportsImages} applies — a row set that vouches
680
- * nothing is NOT listed). */
681
+ * nothing is NOT listed). `image_modes` is allow-listed for THIS listing
682
+ * only (publicModelEntry's image overload). */
681
683
  interface RouterImageModelEntry extends RouterModelEntry {
682
684
  image_modes: ImageCapabilityMode[];
683
685
  }
@@ -1102,7 +1104,8 @@ declare class ModelRouter<S> {
1102
1104
  */
1103
1105
  modelList(): Promise<RouterModelList>;
1104
1106
  /**
1105
- * The OpenRouter PROVIDER document (`GET /v1/models?format=openrouter`):
1107
+ * The OpenRouter PROVIDER document (`GET /partners/openrouter/models`;
1108
+ * `GET /v1/models?format=openrouter` 308-redirects there for one release):
1106
1109
  * schema 2.4 per openrouter.ai/docs/guides/community/for-providers §1 —
1107
1110
  * "an endpoint that returns all models that should be served by
1108
1111
  * OpenRouter". That is the SHARED block's sellable endpoints (the models
@@ -1218,6 +1221,25 @@ declare class ModelRouter<S> {
1218
1221
  * an infrastructure outage.
1219
1222
  */
1220
1223
  supportsTranscription(model: string | undefined): Promise<boolean>;
1224
+ /**
1225
+ * THE REALTIME TRANSCRIPTION LANE'S STREAM-NAME RESOLUTION (openai-realtime
1226
+ * binding.ts, ENG-461): the §5 stream the model's engine emits its own
1227
+ * transcript events on — `engine_args.transcript_stream`, defaulting to
1228
+ * the platform's {@link STT_TRANSCRIPT_STREAM} when no row carries the
1229
+ * field, over the SAME catalog join {@link supportsTranscription} uses
1230
+ * (exact `(model_id, variant)` for a shared-lane hit,
1231
+ * {@link catalogRowsForSlug} for a caller-org app). Deliberately NOT
1232
+ * task-gated: the binding consults it only behind a `transcribes` verdict,
1233
+ * and the resolution itself is a pure field consensus. The lane's consume
1234
+ * side reads the PLATFORM DEFAULT only — a row that OVERRIDES the default
1235
+ * is the binding's loud 501 naming the field, never a silent listen on a
1236
+ * stream the engine does not write; a catalog that cannot vow ONE name
1237
+ * (disagreeing placements, a non-string or empty value) is the loud
1238
+ * defect, never a guess. Undeployed uRun-namespace models throw
1239
+ * {@link UnknownModelError}, and a CONFIGURED catalog oracle that FAILS
1240
+ * propagates — never a fabricated default.
1241
+ */
1242
+ transcriptStreamFor(model: string | undefined): Promise<string>;
1221
1243
  /**
1222
1244
  * THE SPEECH LANE'S VOICE-SET RESOLUTION (proxy/speech.ts): the voice set
1223
1245
  * `/v1/models` publishes for `model` — resolved by the SAME resolver the
@@ -1268,6 +1290,43 @@ declare class ModelRouter<S> {
1268
1290
  closeAll(): Promise<void>;
1269
1291
  }
1270
1292
 
1293
+ /**
1294
+ * THE TURN/TRANSCRIPT LANE over the named §5 `stt` data stream — the
1295
+ * engine's OWN transcript source for the OpenAI Realtime transcription
1296
+ * intent (ENG-461).
1297
+ *
1298
+ * The delivery path is the platform's existing one (urun-python
1299
+ * `urun.serve.transcribe_bridge`): the streaming STT engine emits its own
1300
+ * transcript events on the session-scoped named stream {@link
1301
+ * STT_TRANSCRIPT_STREAM} — the SAME §5 named-DATA mechanism the text-lane
1302
+ * token deltas ride, in the text lane's own wire shape (`{"t": kind,
1303
+ * "delta": text, "start_ms": int}`). Nothing here invents a framing: the
1304
+ * lane is `session.stream('stt').messages()` (core `SessionStream.messages`,
1305
+ * contract §5 — the downstream twin of the `emit` produce seam), and this
1306
+ * module only DECODES each payload onto the `RealtimeBinding.turns` event
1307
+ * shape the realtime adapter already consumes.
1308
+ *
1309
+ * `confidence` in the wire payload is SEMANTIC END-OF-TURN confidence (the
1310
+ * engine's own VAD verdict that the user finished speaking), NOT
1311
+ * transcription confidence — carrying it onto the GA surface would
1312
+ * misrepresent it, so the decoder DROPS it by design (the adapter reads the
1313
+ * boundary from `turn_end`, which is the same verdict as an event).
1314
+ */
1315
+
1316
+ /** The turn kinds the transcribe bridge emits (TranscriptEvent kinds). */
1317
+ type SessionTurnKind = 'delta' | 'final' | 'turn_start' | 'turn_end' | 'turn_eager_end' | 'turn_resumed';
1318
+ /**
1319
+ * One decoded transcript-lane event — the `RealtimeBinding.turns` event
1320
+ * shape. `text` is the wire `delta` field (a transcript PIECE — the adapter
1321
+ * accumulates), `startMs` the additive `start_ms` clock. The wire
1322
+ * `confidence` (semantic end-of-turn confidence) is deliberately absent.
1323
+ */
1324
+ interface SessionTurnLaneEvent {
1325
+ kind: SessionTurnKind;
1326
+ text?: string;
1327
+ startMs?: number;
1328
+ }
1329
+
1271
1330
  /** One ready replica row from the presence read. */
1272
1331
  interface RuntimePresenceRow {
1273
1332
  runtime_id: string;
@@ -2106,9 +2165,10 @@ interface ProxyClients {
2106
2165
  /** `listModels(...)` result (an OpenAI model list object). */
2107
2166
  listModels(): Promise<unknown>;
2108
2167
  /**
2109
- * The OpenRouter provider document (`GET /v1/models?format=openrouter`).
2168
+ * The OpenRouter provider document (`GET /partners/openrouter/models`; the
2169
+ * old `GET /v1/models?format=openrouter` spelling 308-redirects there).
2110
2170
  * Optional at the seam: hand-built test clients may omit it, in which case
2111
- * the format=openrouter branch answers 501 — loud, never a silent empty.
2171
+ * that route answers 501 — loud, never a silent empty.
2112
2172
  */
2113
2173
  openRouterModels?(): Promise<unknown>;
2114
2174
  /**
@@ -2286,6 +2346,46 @@ interface ProxyClients {
2286
2346
  * queued admission going back only at the LAST waiter's abort.
2287
2347
  */
2288
2348
  openAudioOn?(handle: string, signal?: AbortSignal): Promise<PinnedAudioLane>;
2349
+ /**
2350
+ * THE TURN/TRANSCRIPT LANE OPEN (the realtime transcription intent,
2351
+ * ENG-461): the STT engine's OWN transcript events — the §5 `stt` named
2352
+ * data stream (urun.serve.transcribe_bridge's wire shape: `t` =
2353
+ * delta/final/turn_start/turn_end/turn_eager_end/turn_resumed, `delta`
2354
+ * text pieces, additive `start_ms`), consumed off the EXACT pooled session
2355
+ * the handle names (ModelRouter.sessionForHandle — a gone or replaced
2356
+ * session throws {@link SessionGoneError} LOUDLY, never a fresh session
2357
+ * opened behind the handle's back) and decoded onto the
2358
+ * `RealtimeBinding.turns` event shape (the wire's semantic-VAD
2359
+ * `confidence` is end-of-turn confidence, NOT transcription confidence —
2360
+ * the lane deliberately does not carry it). One lane per call; the
2361
+ * realtime binding calls it once per connection. The returned iterable is
2362
+ * lazy. `signal` is the CONSUMER's own lifecycle (the realtime
2363
+ * connection's turn-lane controller, aborted at its teardown): the
2364
+ * transport races every `messages()` pull against it and ends the stream
2365
+ * subscription on every exit path, so the lane never outlives its
2366
+ * consumer — a quiet engine cannot pin it open behind a never-settling
2367
+ * pull. Optional at the seam exactly like
2368
+ * {@link openAudioOn}: the binding's stt verdict answers its absence with
2369
+ * the loud 501 — never a transcription-capable binding with no transcript
2370
+ * source. An already-aborted signal rejects on the first pull — never
2371
+ * opens a lane for a vanished consumer.
2372
+ */
2373
+ openTurnsOn?(handle: string, signal?: AbortSignal): AsyncIterable<SessionTurnLaneEvent>;
2374
+ /**
2375
+ * THE REALTIME TRANSCRIPTION LANE'S STREAM-NAME RESOLUTION (openai-realtime
2376
+ * binding.ts): the §5 transcript stream the model's catalog rows vow
2377
+ * (ModelRouter.transcriptStreamFor — `engine_args.transcript_stream`,
2378
+ * defaulting to the platform's `stt`). The lane's consume side reads the
2379
+ * platform DEFAULT stream name only, so the binding answers a row that
2380
+ * OVERRIDES the default with the loud 501 naming the field — never a
2381
+ * silent listen on a stream the engine does not write, and never a
2382
+ * transcript-less transcription session. Optional at the seam exactly
2383
+ * like {@link supportsTranscription}: hand-built clients own their own
2384
+ * lane (their `openTurnsOn` decides what to read — no catalog row exists
2385
+ * to check), so its absence skips the catalog check; every router-backed
2386
+ * backhaul wires it.
2387
+ */
2388
+ transcriptStreamFor?(model: string | undefined): Promise<string>;
2289
2389
  /**
2290
2390
  * Open (or reuse) the NATIVE video FRAME lane on the pooled session `model`
2291
2391
  * routes to (transport/media.ts `enableSessionVideo`: discrete JPEG frames →
@@ -178,27 +178,19 @@ declare class SessionGoneError extends Error {
178
178
  }
179
179
 
180
180
  interface RouterModelEntry {
181
- id: string;
182
- object: 'model';
183
- created: number;
184
- owned_by: string;
185
181
 
186
- name?: string;
187
-
188
- description?: string;
182
+ id: string;
189
183
 
190
- task?: string;
184
+ object: 'model';
191
185
 
192
- engine?: string;
186
+ created: number;
193
187
 
194
- gpu_spec?: string;
188
+ owned_by: string;
195
189
 
196
190
  context_length?: number;
197
191
 
198
192
  pricing?: TokenPricing;
199
193
 
200
- warm?: number;
201
-
202
194
  voices?: string[];
203
195
 
204
196
  capabilities?: {
@@ -346,6 +338,8 @@ declare class ModelRouter<S> {
346
338
 
347
339
  supportsTranscription(model: string | undefined): Promise<boolean>;
348
340
 
341
+ transcriptStreamFor(model: string | undefined): Promise<string>;
342
+
349
343
  speechVoices(model: string | undefined): Promise<SpeechVoicesResolution>;
350
344
 
351
345
  imageModelList(): Promise<RouterImageModelList>;
@@ -355,6 +349,14 @@ declare class ModelRouter<S> {
355
349
  closeAll(): Promise<void>;
356
350
  }
357
351
 
352
+ type SessionTurnKind = 'delta' | 'final' | 'turn_start' | 'turn_end' | 'turn_eager_end' | 'turn_resumed';
353
+
354
+ interface SessionTurnLaneEvent {
355
+ kind: SessionTurnKind;
356
+ text?: string;
357
+ startMs?: number;
358
+ }
359
+
358
360
  interface RuntimePresenceRow {
359
361
  runtime_id: string;
360
362
 
@@ -615,6 +617,10 @@ interface ProxyClients {
615
617
 
616
618
  openAudioOn?(handle: string, signal?: AbortSignal): Promise<PinnedAudioLane>;
617
619
 
620
+ openTurnsOn?(handle: string, signal?: AbortSignal): AsyncIterable<SessionTurnLaneEvent>;
621
+
622
+ transcriptStreamFor?(model: string | undefined): Promise<string>;
623
+
618
624
  openVideo?(model: string | undefined): Promise<ProxyVideoLane>;
619
625
 
620
626
  openVideoOut?(model: string | undefined): Promise<ProxyVideoOutLane>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@urun-sh/openai",
3
- "version": "0.6.17",
3
+ "version": "0.6.18",
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.17"
73
+ "@urun-sh/core": "^0.6.18"
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-VRCCLQ5P.js";import{C as oe,D as se,E as ie,F as ae,H as le,I as A,J as ce,K as ue,L as de,M as he,N as pe,O as ge,P as fe,Q as me,R as ye,b as ee,d as te,e as ne,q as re,r as O}from"./chunk-U7EJSC2N.js";import{e as X,g as z}from"./chunk-TMQJY56F.js";import{a as Z}from"./chunk-3JWYIKHM.js";import{c as p}from"./chunk-YSFSRI3D.js";p();import{Counter as S,Gauge as we,Histogram as be,Registry as Ue,collectDefaultMetrics as Pe}from"@prometheus-io/client";import{createServer as $e}from"http";var Tt=9464,ve="/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","/v1/audio/models","/v1/audio/transcriptions",le,ue],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 T=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}},N=class{registry=new Ue;modelLabel=new T(He);orgLabel=new T(Me);requests;duration;ttft;streamErrors;undelivered;ledgerWrites;ledgerQueue;labelCardinality;labelOverflows;constructor(e={}){this.requests=new S({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 be({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 be({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 S({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 S({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 S({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 de)this.ledgerWrites.inc({outcome:n.outcome,reason:n.reason},0);this.ledgerQueue=new we({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"},he()),this.ledgerQueue.set({measure:"oldest_age_seconds"},pe()/1e3)}}),this.labelCardinality=new we({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 S({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 _e(t){return $e((e,n)=>{(async()=>{let o=(e.url??"/").split("?")[0];if(e.method!=="GET"||o!==ve){n.writeHead(404,{"content-type":"text/plain"}),n.end(`no route for ${e.method} ${o} \u2014 this port serves only GET ${ve}
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 Se}from"crypto";import{createServer as Be}from"http";var Lt=9465,ze="x-urun-key-id",je="x-urun-key-fingerprint",We="urun-ratelimit-bucket:",Je="urun-ratelimit-tier:",Ee="anonymous",qe=/^urun_sk_[A-Za-z0-9_-]{43}$/;function Ve(t){return Se("sha256").update(`${We}${t}`).digest("hex")}function Ye(t){return Se("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?Ee:Ve(n)),e.setHeader(je,n===null?Ee: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 R(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",Bt="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 zt(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=re(O(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?O(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:O(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 me({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("; ")}`)}},xe=512,ct=1024,L=class extends Error{constructor(){super(`conversation capacity reached (${xe} 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>=xe;){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,i)=>{let l=r().backhaul.clients.openAudioOn;if(l===void 0)throw new Error("the conversation backhaul wires no openAudioOn seam \u2014 the speech and realtime lanes cannot open their pinned native session audio on this deployment");return l(s,i)},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)},a.supportsTranscription=async s=>{let i=r().backhaul.clients.supportsTranscription;if(i===void 0)throw new Error("the conversation backhaul wires no supportsTranscription seam \u2014 the transcription lane cannot gate model capability on this deployment");return i(s)},a.listSpeechToTextModels=async()=>{let s=r().backhaul.clients.listSpeechToTextModels;if(s===void 0)throw new Error("the conversation backhaul wires no listSpeechToTextModels seam \u2014 the transcription lane cannot list the stt models on this deployment");return 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 X("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}},V=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"}},Y=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",J],["output_minutes",J],["cost_nano_usd",J],["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 J(t){return t===null?!0:typeof t=="number"?Number.isFinite(t):typeof t!="string"?!1:/[^\s]/.test(t)&&Number.isFinite(Number(t))}function q(t,e){return t===null?e===null:e===null?!1:Number(e)===t}function yt(t,e){return t===null?e===null:e===null?!1:Number(t.toFixed(9))===Number(e)}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 wt(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)&&q(t.prompt_tokens,n.prompt_tokens)&&q(t.completion_tokens,n.completion_tokens)&&q(t.output_units,n.output_units)&&yt(t.output_minutes,n.output_minutes)}function bt(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"&&wt(n,u.row)?new V(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 Y("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=bt(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 _t}from"http";p();var K="/inference-usage",ke=25e3;var vt=new Set(["Error","TypeError","SyntaxError","TimeoutError"]);function Oe(t){if(!(t instanceof Error))return`a ${typeof t}`;let e=vt.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(ke)})}catch(s){throw new A(`the uRun usage surface (POST ${K}) could not be reached within ${ke}ms: ${Oe(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: ${Oe(s)}`)}return ce(a,this.opts.onRejectedRow)}};var Et=5e3,St=3e3,Rt="/api/client-tokens";function Q(t,e,n){t.writeHead(e,{"content-type":"application/json"}),t.end(JSON.stringify(n))}function Ae(t,e,n,o){Q(t,e,{error:{message:n,type:o}})}async function xt(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(St)});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 En(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??(()=>xt(t.baseUrl)),s=t.usageClient??new G({apiUrl:t.apiUrl}),i=t.ledgerClient??new F({apiUrl:t.apiUrl}),l=new N(t.defaultMetrics===void 0?{}:{defaultMetrics:t.defaultMetrics}),u=async c=>e.resolve(R(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=ye({clients:async c=>r.clientsFor(await u(c)),usage:{authorize:c=>R(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(R(c.headers.authorization),d),observe:c=>l.observeLedger(c),...t.ledgerLog?{report:t.ledgerLog}:{}}}),v=null,E=_t((c,d)=>{let h=new URL(c.url??"/","http://localhost");(async()=>{if(c.method==="GET"&&h.pathname==="/healthz"){Q(d,200,{ok:!0});return}if(c.method==="GET"&&h.pathname==="/readyz"){let k=Date.now();if(!v||k-v.at>=Et){let Ne=await a();v={at:k,...Ne}}Q(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=[ee(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}}}),ne(E,{clients:async c=>r.clientsFor(await u(c)),statusOf:f}).close,se(E,{resolver:{resolve:async(c,d)=>{let h=await e.resolve(R(c.headers.authorization)),y=B.connectionFor(h),C;try{C=await ae(y.clients,ie(c),d)}catch(k){throw y.release().catch(()=>{}),k}return{...C,release:y.release}}}})];E.on("upgrade",(c,d)=>{let h=new URL(c.url??"/","http://localhost");h.pathname===Z||h.pathname===te||h.pathname===oe||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 Te=async()=>{for(let c of Ie)await c();await B.detachAll()};return{server:E,metricsServer:_e(l),metrics:l,edgeIdentityServer:Re(),registry:r,closeAll:()=>r.closeAll(),closeWebSockets:Te}}async function Sn(t,e,n){n&&await n();let o=new Promise(r=>t.close(()=>r()));t.closeAllConnections(),await o,await ge()||fe(),await e()}export{Tt as a,ve as b,I as c,Le as d,He as e,Me as f,Ke as g,Ge as h,T as i,N as j,_e as k,Lt as l,ze as m,je as n,We as o,Je as p,Ee as q,Ve as r,Ye as s,Qe as t,Re as u,m as v,_ as w,R as x,tt as y,nt as z,U as A,ot as B,P as C,Bt as D,zt as E,lt as F,M as G,$ as H,xe as I,ct as J,L as K,H as L,ht as M,pt as N,D as O,g as P,bt as Q,F as R,Et as S,St as T,En as U,Sn as V};