@urun-sh/openai 0.5.6 → 0.6.1

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.
Files changed (52) hide show
  1. package/dist/{ResponsesClient-BE3-hx3m.d.ts → ResponsesClient-CSSrOYD8.d.ts} +5 -1
  2. package/dist/{ResponsesClient-DjZWjlFH.d.cts → ResponsesClient-_OZERUjH.d.cts} +9 -1
  3. package/dist/{chunk-7M6LY6DR.js → chunk-3JWYIKHM.js} +1 -1
  4. package/dist/{chunk-BSHT6RZZ.js → chunk-6M5JK4YC.js} +1 -1
  5. package/dist/chunk-76BZLCGT.js +1 -0
  6. package/dist/chunk-7V2JKPIG.js +1 -0
  7. package/dist/{chunk-5HKWNK3O.js → chunk-K23AZPI4.js} +1 -1
  8. package/dist/chunk-T2LNHDPS.js +7 -0
  9. package/dist/chunk-TMQJY56F.js +4 -0
  10. package/dist/chunk-WEBOBE7Z.js +58 -0
  11. package/dist/chunk-X3GLH2HQ.js +1 -0
  12. package/dist/gemini-live.cjs +2 -2
  13. package/dist/gemini-live.d.cts +75 -7
  14. package/dist/gemini-live.d.ts +30 -7
  15. package/dist/gemini-live.js +1 -1
  16. package/dist/hosted/bin.cjs +37 -35
  17. package/dist/hosted/bin.js +1 -1
  18. package/dist/hosted/index.cjs +33 -31
  19. package/dist/hosted/index.d.cts +70 -4
  20. package/dist/hosted/index.d.ts +21 -4
  21. package/dist/hosted/index.js +1 -1
  22. package/dist/index.cjs +1 -1
  23. package/dist/index.d.cts +4 -4
  24. package/dist/index.d.ts +4 -4
  25. package/dist/index.js +1 -1
  26. package/dist/pi-extension/index.cjs +7 -7
  27. package/dist/pi-extension/index.d.cts +2 -2
  28. package/dist/pi-extension/index.d.ts +2 -2
  29. package/dist/pi-extension/index.js +1 -1
  30. package/dist/pi-extension/standalone.cjs +53 -53
  31. package/dist/proxy/cli.cjs +48 -46
  32. package/dist/proxy/cli.js +15 -14
  33. package/dist/proxy/index.cjs +25 -24
  34. package/dist/proxy/index.d.cts +142 -25
  35. package/dist/proxy/index.d.ts +22 -11
  36. package/dist/proxy/index.js +1 -1
  37. package/dist/{responses-turn-Dr37N3kH.d.ts → responses-turn-EwxfRJJg.d.ts} +187 -7
  38. package/dist/{responses-turn-tNUHq5b0.d.cts → responses-turn-vMefRdgJ.d.cts} +707 -23
  39. package/dist/{translator-Bh0Bp_Ie.d.cts → translator-CO8W_hmJ.d.cts} +1 -1
  40. package/dist/{translator-BCoRFaTs.d.ts → translator-Ddd65sXR.d.ts} +1 -1
  41. package/dist/{types-lsVTbNcH.d.cts → types-CHPtJx6d.d.cts} +13 -0
  42. package/dist/{types-lsVTbNcH.d.ts → types-CHPtJx6d.d.ts} +4 -0
  43. package/dist/{video-out-CCksIcj8.d.cts → video-out-BsuLqlID.d.cts} +1 -1
  44. package/dist/{video-out-IGQ4YQ-c.d.ts → video-out-Cawtm7SF.d.ts} +1 -1
  45. package/package.json +4 -2
  46. package/dist/chunk-2T2YYBVX.js +0 -1
  47. package/dist/chunk-3WAJD62J.js +0 -4
  48. package/dist/chunk-FP4RSAIE.js +0 -1
  49. package/dist/chunk-HR5H6S7L.js +0 -1
  50. package/dist/chunk-NY23USZF.js +0 -57
  51. package/dist/chunk-VLRMJRLS.js +0 -6
  52. /package/dist/{chunk-5CCJUNH7.js → chunk-YSFSRI3D.js} +0 -0
@@ -1,6 +1,6 @@
1
1
  import { C as CatalogRow } from './models-DUdx_Y6X.cjs';
2
2
  import { IncomingMessage } from 'node:http';
3
- import { a as AudioBridge, h as VideoFrameLane, j as VideoOutLane } from './video-out-CCksIcj8.cjs';
3
+ import { a as AudioBridge, h as VideoFrameLane, j as VideoOutLane } from './video-out-BsuLqlID.cjs';
4
4
 
5
5
  /**
6
6
  * WHAT a proxy serves — the `identity` block on `GET /stats`. Reuse of a
@@ -183,7 +183,9 @@ interface InferenceRecord {
183
183
  * per-request resolver over the catalog edge function's `shared` block:
184
184
  *
185
185
  * { shared_org_id, endpoints: [{ model_id, variant, gpu_spec, app_slug,
186
- * function, warm }] }
186
+ * function, warm, ready, ready_runtimes,
187
+ * ready_since }],
188
+ * regions: [...], compliance: { zdr, hipaa } }
187
189
  *
188
190
  * Every function here is PURE and synchronous over the parsed block: the
189
191
  * catalog fetch (U5's edge-function surface) is a separate seam and is NOT
@@ -217,11 +219,50 @@ interface SharedEndpointRow {
217
219
  * row. Exactly one per model_id — zero or two is a loud catalog defect.
218
220
  */
219
221
  compat_default?: boolean | null;
222
+ /**
223
+ * The catalog row's output-free opt-in (owner decision 2026-09-21, the
224
+ * `reflex-latest:bf16` decision lane): a ZERO per-token OUTPUT rate is a
225
+ * deliberate published price for this endpoint — the model is a
226
+ * non-generative decision/action surface whose tokens are billed on
227
+ * input alone, so the OpenRouter document emits `cost_usd: '0'`
228
+ * completion pricing instead of refusing it. Absent (pre-flag edge
229
+ * function) and null both mean NOT opted in — the zero-output refusal
230
+ * stands.
231
+ */
232
+ output_free?: boolean | null;
233
+ /**
234
+ * Runtime-plane readiness (up-debounce, down-immediate upstream). ABSENT
235
+ * on blocks from an edge function that predates the readiness seam — the
236
+ * OpenRouter document builder treats that as NOT ready, loudly (see
237
+ * openrouter-doc.ts); `null` is likewise unknown, not ready.
238
+ */
239
+ ready?: boolean | null;
240
+ /** How many ready runtimes stand behind `ready` (null when unknown). */
241
+ ready_runtimes?: number | null;
242
+ /** ISO 8601 instant the endpoint became (and stayed) ready; null when not. */
243
+ ready_since?: string | null;
220
244
  }
221
245
  /** The catalog edge function's `shared` block. */
222
246
  interface SharedCatalogBlock {
223
247
  shared_org_id: string;
224
248
  endpoints: SharedEndpointRow[];
249
+ /**
250
+ * Deployment-topology region codes (AWS-style, e.g. 'us-west-2') — the
251
+ * physical surface the shared endpoints serve from. Absent/empty means the
252
+ * topology is undeclared.
253
+ */
254
+ regions?: string[] | null;
255
+ /**
256
+ * Operator-declared data-handling posture (zero data retention, HIPAA).
257
+ * Present ONLY when an operator has declared BOTH flags — and nothing
258
+ * else: the wire contract is exactly {zdr, hipaa}, and the parser treats
259
+ * a further key as a catalog defect. ABSENT means UNDECLARED — never a
260
+ * default, because these are claims about where customer data goes.
261
+ */
262
+ compliance?: {
263
+ zdr: boolean;
264
+ hipaa: boolean;
265
+ } | null;
225
266
  }
226
267
 
227
268
  /**
@@ -371,32 +412,125 @@ interface ModelPriceRow {
371
412
  * source is model-prices.ts — the SAME per-token join the OpenAI listing
372
413
  * uses ({@link pricingFor}); the per-million rate is shifted to per-token
373
414
  * on its decimal string, never by float division. An entry with no
374
- * per-token row — today ALL of them, because `model_prices` prices GPU
415
+ * per-token row — today MOST of them, because `model_prices` prices GPU
375
416
  * minutes (`per_minute` et al.), which is not evidence about tokens —
376
417
  * carries NO pricing array at all: the schema's rule is "a modality with
377
418
  * no pricing array is simply unpriced". Never a zero, never a GPU-minute
378
419
  * rate relabelled as a token rate (ENG-317 owns the human pricing
379
- * decision that would make such rows exist).
420
+ * decision that would make such rows exist) — with ONE owner-ratified
421
+ * exception: `reflex-latest:bf16` is a non-generative decision lane whose
422
+ * catalog row declares `output_free` (owner decision 2026-09-21), so its
423
+ * ZERO completion rate is the published price (`cost_usd: '0'`), billed
424
+ * on input alone. The zero reaches this builder only through
425
+ * model-prices.ts's allowlist (routing resolves prices against the
426
+ * shared block's `output_free` set); the builder itself just renders
427
+ * whatever a resolved price says.
380
428
  *
381
429
  * The full closed-value-domain schema ships as OpenAPI 3.1 at
382
430
  * openrouter.ai/docs/assets/provider-monitor-schema-v2.openapi.json.
431
+ *
432
+ * THE OPERATIONAL FIELDS (schema 2.4, the "launch control" surface):
433
+ * - `is_ready` is the shared endpoint's SELLABLE readiness:
434
+ * `runtime ready AND priced` — `endpoint.ready === true` AND a per-token
435
+ * price row for this exact (model_id, variant). Priced means the price
436
+ * book RESOLVES a pair for the SKU: an output-free row's `{input: >0,
437
+ * output: 0}` is a resolved pair (model-prices.ts's allowlist decides,
438
+ * from the shared block's `output_free` flag), so an input-priced,
439
+ * output-free, runtime-ready endpoint IS ready. An unpriced SKU is
440
+ * never sellable: OpenRouter's `is_ready: false` keeps an endpoint
441
+ * hidden, and we must never expose a model we cannot bill, so a ready
442
+ * runtime without a price is emitted NOT ready. Runtime readiness is
443
+ * THE ONE DELIBERATELY CONSERVATIVE DEFAULT
444
+ * in this module: a block whose endpoints carry NO `ready` field at all
445
+ * predates the readiness seam (the catalog edge function is not yet
446
+ * redeployed), and UNKNOWN READINESS IS NOT READINESS — every such
447
+ * endpoint is declared `is_ready: false` AND one loud warning per
448
+ * document build names the missing seam, so the default is visible,
449
+ * never silent. A `null` ready is likewise unknown, not ready. Endpoints
450
+ * of the SAME identity follow the existing dedup rule (first occurrence
451
+ * in the deterministic shared-block order wins).
452
+ * - `is_free` is `false` for every declared model: we bill. An unpriced row
453
+ * is UNPRICED, not free — `is_free: true` would mint a `:free` SKU whose
454
+ * every price OpenRouter ignores. The same holds for an output-free row:
455
+ * its input side bills, so it is not a `:free` SKU either — the zero
456
+ * lives in the completion `cost_usd`, nowhere else.
457
+ * - `deprecation_date` is OMITTED: the schema makes it optional and no
458
+ * shared endpoint has a deprecation schedule to declare.
459
+ * - `datacenters` maps the block's deployment-topology region codes
460
+ * (AWS-style) to the schema's `{ country_code, region }` elements through
461
+ * an EXPLICIT region→country table (a country cannot be derived from a
462
+ * prefix — sa-east-1 is BR). Unmappable codes are omitted and WARNED,
463
+ * never guessed; absent/empty topology omits the field.
464
+ * - `compliance` is emitted ONLY when the block's `compliance` is present:
465
+ * zdr/hipaa are claims about where customer data goes, DECLARED by an
466
+ * operator — never inferred, never defaulted, and never half-emitted.
467
+ * - `tokenizer` names the model family the catalog row's `hf_repo`
468
+ * carries (e.g. `Inferact/Qwen3.8-27B-NVFP4` → `Qwen`) through an
469
+ * explicit, BOUNDED table over the repo's model-NAME tokens — whole
470
+ * tokens only, version-aware for the families whose generations differ
471
+ * (llama-2 is not llama-3); a repo that names no known family OMITS the
472
+ * field.
383
473
  */
384
474
 
385
475
  type OpenRouterProviderDoc = {
386
476
  data: OpenRouterProviderModel[];
387
477
  };
478
+ /** One schema-2.4 `datacenters` element: ISO 3166-1 alpha-2 country + region. */
479
+ interface OpenRouterDatacenter {
480
+ country_code: string;
481
+ region?: string;
482
+ }
483
+ /** The operator-declared data-handling posture (schema-2.4 `compliance`). */
484
+ interface OpenRouterCompliance {
485
+ zdr: boolean;
486
+ hipaa: boolean;
487
+ }
388
488
  interface OpenRouterProviderModel {
389
489
  schema_version: '2.4';
390
- /** The EXACT id OpenRouter sends back as `model` — the app slug. */
490
+ /**
491
+ * The EXACT id OpenRouter sends back as `model`: the verbatim
492
+ * `<model_id>:<variant>` external identity (see MODEL-IDENTITY.md) — never
493
+ * the deployment slug.
494
+ */
391
495
  id: string;
392
496
  name: string;
393
497
  created: number;
394
498
  /** Valid enum: int4|int8|fp4|mxfp4|nvfp4|fp6|fp8|mxfp8|fp16|bf16|fp32|null. */
395
499
  quantization: string | null;
500
+ /** Tokenizer family name (e.g. 'Qwen'); omitted when the repo names none. */
501
+ tokenizer?: string;
396
502
  description: string;
397
503
  hugging_face_id: string;
504
+ /** SELLABLE readiness — `endpoint.ready === true` AND a per-token price. */
505
+ is_ready: boolean;
506
+ /** `false` always: we bill; an unpriced row is unpriced, not free. */
507
+ is_free: boolean;
398
508
  input_modalities: Array<Record<string, unknown>>;
399
509
  output_modalities: Array<Record<string, unknown>>;
510
+ /** Physical serving topology; omitted when the block declares none. */
511
+ datacenters?: OpenRouterDatacenter[];
512
+ /** Operator-declared posture; omitted entirely when undeclared. */
513
+ compliance?: OpenRouterCompliance;
514
+ }
515
+ /**
516
+ * The resolution of ONE tts model's voice set — ONE answer BOTH consumers
517
+ * read, so the lane can never drift from the listing:
518
+ * - `published` is the set the model's /v1/models entry carries ([] =
519
+ * publish none).
520
+ * - `defect` is non-null ONLY when the catalog PRESENTED
521
+ * `engine_args.voices` that could not be vouched (empty, malformed, or
522
+ * not an identical non-empty string array on every GPU placement). The
523
+ * publish side keeps its long-standing posture through a defect (the
524
+ * card list for the qwen3 class, nothing otherwise — the listing is a
525
+ * best-effort enrichment), but the speech lane must never silently
526
+ * validate against the card's nine over a catalog that SPOKE — it fails
527
+ * loud (500) on `defect` instead.
528
+ */
529
+ interface SpeechVoicesResolution {
530
+ /** The voices the /v1/models entry publishes for this model ([] = none). */
531
+ readonly published: readonly string[];
532
+ /** Why the catalog's own voices value could not be vouched, when it presented one. */
533
+ readonly defect: string | null;
400
534
  }
401
535
 
402
536
  /**
@@ -419,11 +553,12 @@ interface OpenRouterProviderModel {
419
553
  *
420
554
  * RESOLUTION ORDER (one canonical path, documented end to end):
421
555
  * 1. model absent / "urun" / an alias of the startup app → DEFAULT app.
422
- * 2. exact slug match on a deployed serve app → that app.
556
+ * 2. exact slug match on a deployed app that declares serves="openai" →
557
+ * that app.
423
558
  * 3. catalog-id form matching exactly one deployed app → that app
424
559
  * (two or more candidates → loud ambiguity error naming them).
425
- * 4. the name maps to an org app that is NOT an active app exposing the
426
- * proxy's serve function → loud 404
560
+ * 4. the name maps to an org app that has NO function declaring the
561
+ * serve protocol (`serves="openai"`) → loud 404
427
562
  * naming `urun serve <model>` and the available models.
428
563
  * 5. the name matches a catalog row but no deployed app → loud 404
429
564
  * naming `urun serve <id>` (deployed-only v1).
@@ -449,9 +584,13 @@ interface OpenRouterProviderModel {
449
584
  /** One org app row from `GET {orgApi}/apps` (urun-cli `ApiClient.list_apps`). */
450
585
  interface DeployedApp {
451
586
  app_slug: string;
587
+ /** The serve function the BACKHAUL dials on the app (URUN_FUNCTION); no
588
+ * longer a routing membership test — routing reads `serves`. */
452
589
  function_name?: string | null;
590
+ /** Declared serve protocol (`"openai"`), or null/absent = undeclared = the
591
+ * proxy must not route this app. THE routing membership test. */
592
+ serves?: string | null;
453
593
  deployment_status?: string | null;
454
- [k: string]: unknown;
455
594
  }
456
595
  /**
457
596
  * A model that does not resolve to a deployed app the proxy may serve.
@@ -502,6 +641,32 @@ interface RouterModelEntry {
502
641
  pricing?: TokenPricing;
503
642
  /** Idle-floor replica count for a shared-lane model (U6 reconciler). */
504
643
  warm?: number;
644
+ /**
645
+ * The voices a `tts`-task model synthesizes under (ENG-331 — OpenAI's
646
+ * speech API names a voice per request, so a caller needs the list).
647
+ * Catalog-sourced from `engine_args.voices` when the row carries it;
648
+ * otherwise the qwen3 CustomVoice timbres for that one model class (see
649
+ * openrouter-doc.ts `QWEN3_CUSTOMVOICE_VOICES`). Absent on every non-tts
650
+ * entry, and on tts entries whose voices the catalog does not vouch.
651
+ */
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
+ };
505
670
  }
506
671
  interface RouterModelList {
507
672
  object: 'list';
@@ -523,7 +688,8 @@ interface RouterImageModelList {
523
688
  * lane (the catalog `shared` block, the cross-org session dial) — the exact
524
689
  * serve function on the shared org's app and the flag that switches the
525
690
  * control plane's shared-admission carve-out on. A plain caller-org app
526
- * carries only `appSlug`; the router's own `fnName` applies there.
691
+ * carries only `appSlug`; the router's `fnName` (the function the backhaul
692
+ * dials) applies there.
527
693
  */
528
694
  interface RouteTarget {
529
695
  appSlug: string;
@@ -540,6 +706,16 @@ interface RouteTarget {
540
706
  modelId?: string;
541
707
  /** Shared lanes only: the catalog variant the request pinned. */
542
708
  variant?: string;
709
+ /**
710
+ * Shared lanes only: the pod org (`shared.shared_org_id` of the block that
711
+ * resolved the lane). THE STATELESS DECISION TOKEN'S PROVIDER CLAIM — the
712
+ * decision intake's provider-aware org gate accepts tenant OR provider
713
+ * matching the pod org, and on a shared pod the caller-org tenant differs
714
+ * from the shared-org pod. Sourced from the SAME block that resolved the
715
+ * lane, never a second env spelling, so key and pod-org identity cannot
716
+ * drift.
717
+ */
718
+ sharedOrgId?: string;
543
719
  }
544
720
  interface ModelRouterOptions<S> {
545
721
  /**
@@ -550,10 +726,25 @@ interface ModelRouterOptions<S> {
550
726
  * "NO-DEFAULT MODE").
551
727
  */
552
728
  defaultApp: string | null;
553
- /** The serve function name every routed app must expose (URUN_FUNCTION). */
729
+ /**
730
+ * The serve function the BACKHAUL dials on the resolved app (URUN_FUNCTION)
731
+ * — shared lanes may pin their own per-row function instead (RouteTarget
732
+ * fnName). NOT a routing membership test any more: routing reads the app's
733
+ * declared `serves` protocol (see {@link ModelRouter.servable}).
734
+ */
554
735
  fnName: string;
555
- /** Open a backhaul session for a resolved dial target (called at most once per pool key). */
556
- openSession: (target: RouteTarget) => S | Promise<S>;
736
+ /**
737
+ * Open a backhaul session for a resolved dial target (called at most once
738
+ * per pool key). The SECOND argument is the POOL KEY the entry will
739
+ * occupy — the same string `sessionFor`/`speechSessionFor` store it under
740
+ * — so an implementer can wire per-slot machinery (the backhaul's
741
+ * gone-watch eviction and the speech lane's subject scoping) against the
742
+ * slot the entry ACTUALLY holds: a lane-scoped open (`speech:<slug>`)
743
+ * that wired its eviction on the unscoped key would leave a dead entry
744
+ * stranded in the pool forever. Implementers that ignore it behave
745
+ * exactly as before.
746
+ */
747
+ openSession: (target: RouteTarget, poolKey: string) => S | Promise<S>;
557
748
  /** Terminal release for one pool entry (Session.end() underneath). */
558
749
  closeSession: (entry: S) => Promise<void>;
559
750
  /**
@@ -609,6 +800,19 @@ interface ModelRouterOptions<S> {
609
800
  * discipline as the apps/catalog oracles.
610
801
  */
611
802
  shared?: (() => Promise<SharedCatalogBlock | null>) | null;
803
+ /**
804
+ * OWNER RULING (2026-09-21, shared-endpoints inference-proxy): the model
805
+ * listing is the platform's SHARED surface and nothing else. When true,
806
+ * {@link ModelRouter.modelList} advertises ONLY the catalog `shared`
807
+ * block's endpoints and never the caller-org's deployed apps — the same
808
+ * id space {@link ModelRouter.openRouterModels} publishes, so every
809
+ * listing format agrees. The hosted lane (hosted/tenants.ts) sets this;
810
+ * the local `urun compat` lane leaves it absent and behaves
811
+ * byte-for-byte as before (a caller's own apps stay listed there).
812
+ * An absent shared block (or none configured) is an EMPTY listing —
813
+ * "no shared models" is an answer, never a fallback to the org's apps.
814
+ */
815
+ sharedOnlyListing?: boolean;
612
816
  }
613
817
  /**
614
818
  * The image-capability modes the Images lane gates on (images.ts's
@@ -681,11 +885,24 @@ declare class ModelRouter<S> {
681
885
  * complete — the same posture `parseSharedBlock` takes on a malformed
682
886
  * routing block.
683
887
  *
888
+ * `outputFree` is the shared block's EXACT `(model_id, variant)` opt-in
889
+ * set ({@link outputFreeKey} keys, built from `SharedEndpointRow.output_free`):
890
+ * the only rows whose zero OUTPUT rate the resolver may publish — see
891
+ * model-prices.ts for the one-flag-one-lane rule. Absent/empty set: every
892
+ * zero keeps the refusal.
893
+ *
684
894
  * The resolve step sits OUTSIDE the catch deliberately: it is our own
685
895
  * code, so a programming error in it must surface as a crash, not become
686
896
  * an empty price list.
687
897
  */
688
898
  private pricesForListing;
899
+ /**
900
+ * The EXACT `(model_id, variant)` pairs the shared block declares
901
+ * output-free ({@link outputFreeKey} keys) — the only allowlist
902
+ * resolveTokenPrices accepts a zero OUTPUT rate against. Only a
903
+ * `true` flag contributes: absent and null are the un-flagged default.
904
+ */
905
+ private outputFreeKeys;
689
906
  /**
690
907
  * Where a degradation says so. Defaults to `console.warn` rather than to
691
908
  * nothing: a caller may ROUTE the signal (the CLI sends it to stderr, as
@@ -694,7 +911,14 @@ declare class ModelRouter<S> {
694
911
  * time bomb.
695
912
  */
696
913
  private warn;
697
- /** Apps this proxy may serve: active AND exposing the serve function. */
914
+ /**
915
+ * Apps this proxy may serve: active AND declaring the OpenAI-compatible
916
+ * serve protocol (`@app.function(serves="openai")`). The OLD rule — a
917
+ * function literally named `serve` — is GONE: the name carried no
918
+ * capability meaning (an app with a function named `serve_runtime` could
919
+ * never be routed), and the declaration is what the deploy actually knows.
920
+ * `fnName` survives only as the function the BACKHAUL dials.
921
+ */
698
922
  private servable;
699
923
  private availableIds;
700
924
  /**
@@ -754,6 +978,15 @@ declare class ModelRouter<S> {
754
978
  * Rides {@link resolveTarget}'s cached listings and OPENS NO SESSION.
755
979
  */
756
980
  servingAppSlugFor(model: string | undefined): Promise<string>;
981
+ /**
982
+ * THE STATELESS DIRECT PATH's resolution seam: the dial target for a
983
+ * model — the pure resolution (app slug, fn, shared flag) with NO session
984
+ * opened. The stateless decision dial (stateless-dispatch.ts) uses it to
985
+ * discover ready replicas for the resolved (org, app, fn) instead of
986
+ * minting a session; every public no-dial accessor above rides this same
987
+ * private resolution.
988
+ */
989
+ targetFor(model: string | undefined): Promise<RouteTarget>;
757
990
  /**
758
991
  * Resolve a request's `model` to its POOL KEY — the app slug for a
759
992
  * caller-org app, `shared:<slug>` for a shared-lane dial (the two never
@@ -782,6 +1015,28 @@ declare class ModelRouter<S> {
782
1015
  entry: S;
783
1016
  identity: TurnCatalogIdentity;
784
1017
  }>;
1018
+ /**
1019
+ * The SPEECH lane's pooled session for a model — the SAME resolution and
1020
+ * acquisition as {@link sessionFor}, stored under a LANE-SCOPED pool key
1021
+ * (`speech:` + the ordinary key) so the session a speech collector
1022
+ * subscribes to is one NO interactive lane (text turns via
1023
+ * `rehomingCreateResponse`/`sessionFor`, Realtime and Gemini Live via
1024
+ * `openAudio`/`handleFor`) can ever resolve: the per-session AudioBridge
1025
+ * broadcasts every untagged output frame to every subscriber, so a shared
1026
+ * session is exactly the mixed-audio defect (greptile urun-ts#500). The
1027
+ * entry's OWN lifecycle (gone-watch eviction, wedge re-salt, billing
1028
+ * identity) is identical — only the slot differs, which is the whole point.
1029
+ */
1030
+ speechSessionFor(model: string | undefined): Promise<{
1031
+ app: string;
1032
+ entry: S;
1033
+ identity: TurnCatalogIdentity;
1034
+ }>;
1035
+ /**
1036
+ * The ONE acquisition path both lanes ride — opened lazily under exactly
1037
+ * `key`, reused afterwards. A failed open never poisons the slot.
1038
+ */
1039
+ private acquire;
785
1040
  private keyOf;
786
1041
  /**
787
1042
  * SESSION-IDENTITY SEAM (a): the opaque stable handle for the pooled
@@ -794,6 +1049,17 @@ declare class ModelRouter<S> {
794
1049
  app: string;
795
1050
  handle: string;
796
1051
  }>;
1052
+ /**
1053
+ * SESSION-IDENTITY SEAM (a), SPEECH lane: the handle for the SPEECH-scoped
1054
+ * pooled session ({@link speechSessionFor} — `speech:<key>` as the handle's
1055
+ * pool key, so {@link sessionForHandle} resolves exactly the speech slot).
1056
+ * The handle codec needs no change: it already carries whatever pool key
1057
+ * minted it, and an interactive {@link handleFor} can never produce one.
1058
+ */
1059
+ speechHandleFor(model: string | undefined): Promise<{
1060
+ app: string;
1061
+ handle: string;
1062
+ }>;
797
1063
  /**
798
1064
  * SESSION-IDENTITY SEAM (b): the exact pooled session a handle names.
799
1065
  * NEVER opens a fresh session — a handle whose session is gone (closed,
@@ -849,11 +1115,20 @@ declare class ModelRouter<S> {
849
1115
  * {@link pricesForListing}'s posture (an unreadable surface prices
850
1116
  * nothing and WARNS — discovery outlives the price oracle); a model with
851
1117
  * no per-token row carries NO pricing array (the schema rule: "a
852
- * modality with no pricing array is simply unpriced").
1118
+ * modality with no pricing array is simply unpriced"), and the block's
1119
+ * `output_free` endpoints are the ONLY rows whose zero OUTPUT rate
1120
+ * prices as `cost_usd: '0'` — the allowlist resolves prices IN the
1121
+ * pricing step below, so the document cannot emit a zero the catalog
1122
+ * did not opt into.
853
1123
  *
854
1124
  * A CONFIGURED catalog read that FAILS propagates — the provider
855
1125
  * document cannot vouch a sellable model from nothing, and a half-empty
856
1126
  * one must never be served as if complete.
1127
+ *
1128
+ * The builder's own loud announcements (the absent readiness seam that
1129
+ * conservatively declares every endpoint not ready, unmappable region
1130
+ * codes) go through this router's {@link warn} sink, so a lane that
1131
+ * routes degradations somewhere sees them too — never a default print.
857
1132
  */
858
1133
  openRouterModels(): Promise<OpenRouterProviderDoc>;
859
1134
  /**
@@ -907,6 +1182,57 @@ declare class ModelRouter<S> {
907
1182
  * for ⇒ false — the realtime resolver renders that as the loud 404.
908
1183
  */
909
1184
  supportsAudio(model: string | undefined): Promise<boolean>;
1185
+ /**
1186
+ * THE SPEECH LANE'S GATE (proxy/speech.ts, `POST /v1/audio/speech`): does
1187
+ * `model`'s deployed app carry catalog `task` **tts** — INVARIANTLY across
1188
+ * every GPU-placement row ({@link audioModalityForRows} answering exactly
1189
+ * `'tts'`)? The SAME shape as {@link supportsImages}/{@link supportsAudio}:
1190
+ * authorized resolution ({@link resolveTarget} — undeployed uRun models
1191
+ * throw {@link UnknownModelError}), then the SAME catalog join
1192
+ * ({@link catalogRowsForSlug}); a SHARED-lane hit joins on the lane's exact
1193
+ * catalog id + variant, exactly like {@link supportsAudio}.
1194
+ *
1195
+ * An `stt` verdict is deliberately NOT speech-capable: a transcription
1196
+ * model must never be asked to synthesize, and a chat model on this route
1197
+ * is the loud 404 the images lane renders for non-image models. A
1198
+ * CONFIGURED catalog oracle that FAILS propagates its rejection — the gate
1199
+ * never fabricates a `false` out of an infrastructure outage.
1200
+ */
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>;
1219
+ /**
1220
+ * THE SPEECH LANE'S VOICE-SET RESOLUTION (proxy/speech.ts): the voice set
1221
+ * `/v1/models` publishes for `model` — resolved by the SAME resolver the
1222
+ * listing's enrichment applies (openrouter-doc.ts `speechVoicesFor`, via
1223
+ * {@link speechVoicesForMatches}) over the SAME catalog join
1224
+ * {@link supportsSpeech} uses (exact `(model_id, variant)` for a
1225
+ * shared-lane hit, {@link catalogRowsForSlug} for a caller-org app), so
1226
+ * the lane enforces exactly ONE list: the one the listing publishes — the
1227
+ * catalog-vouched `engine_args.voices`, else the qwen3 CustomVoice card's
1228
+ * nine for that one model class. A catalog that PRESENTS a voices value
1229
+ * it cannot vouch answers WITH the defect set on the resolution — the
1230
+ * lane fails loud (500) rather than silently validating against the card
1231
+ * fallback. Undeployed uRun-namespace models throw
1232
+ * {@link UnknownModelError} (the lane's 404), and a CONFIGURED catalog
1233
+ * oracle that FAILS propagates — never a fabricated empty set.
1234
+ */
1235
+ speechVoices(model: string | undefined): Promise<SpeechVoicesResolution>;
910
1236
  /**
911
1237
  * `GET /v1/images/models`: the IMAGE-CAPABLE slice of the model surface —
912
1238
  * the caller-org deployed apps whose catalog rows vouch an image mode
@@ -921,10 +1247,136 @@ declare class ModelRouter<S> {
921
1247
  * bare-entries degradation here.
922
1248
  */
923
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>;
924
1265
  /** Close every pooled session (Session.end() underneath) — proxy shutdown. */
925
1266
  closeAll(): Promise<void>;
926
1267
  }
927
1268
 
1269
+ /** One ready replica row from the presence read. */
1270
+ interface RuntimePresenceRow {
1271
+ runtime_id: string;
1272
+ /** Base URL of the runtime's :8099 endpoint, e.g. `http://10.0.1.5:8099`. */
1273
+ address: string;
1274
+ ready: boolean;
1275
+ /** ISO 8601 instant of the runtime's last presence heartbeat. */
1276
+ last_seen: string;
1277
+ status?: string | null;
1278
+ }
1279
+ /** One resolved dial target — the router's pure resolution (no session).
1280
+ * The shared arm's fields ride through so the dial can mint the
1281
+ * provider-aware token and the handlers can repin the CATALOG billing pair:
1282
+ * `sharedOrgId` is the shared block's `shared_org_id` (the pod org). */
1283
+ interface DialTarget {
1284
+ appSlug: string;
1285
+ fnName?: string;
1286
+ sharedApp?: boolean;
1287
+ modelId?: string;
1288
+ variant?: string;
1289
+ sharedOrgId?: string;
1290
+ }
1291
+ interface StatelessDecisionDialOptions {
1292
+ /** Resolve a request's model to its dial target WITHOUT opening a session. */
1293
+ targetFor: (model: string | undefined) => Promise<DialTarget>;
1294
+ apiUrl: string;
1295
+ apiKey: string;
1296
+ /** The caller org's id — the minted token's `tenant` claim. */
1297
+ orgId: string;
1298
+ /** The serve function name the resolved app must expose (the backhaul's
1299
+ * URUN_FUNCTION default, 'serve'). */
1300
+ fnName: string;
1301
+ /** The shared secret render injects (URUN_SESSION_TOKEN_SECRET). */
1302
+ secret: string;
1303
+ /** THE SHARED LANE'S credential (URUN_SHARED_ORG_API_KEY): the shared
1304
+ * org's API key, so discovery and decisions can reach the SHARED org's
1305
+ * pods too. The provider org id comes from the resolved target's
1306
+ * `sharedOrgId` (the block's `shared_org_id`), never a second env spelling
1307
+ * of it — key and pod-org identity cannot drift apart. Absent ⇒ shared
1308
+ * models keep the session lane (the v1 boundary narrows to "no shared
1309
+ * credential mounted" instead of "shared never"). */
1310
+ shared?: {
1311
+ apiKey: string;
1312
+ };
1313
+ fetchImpl?: typeof fetch;
1314
+ nowMs?: () => number;
1315
+ /** Per-attempt POST budget override (test seam; production rides
1316
+ * DECISION_TIMEOUT_MS). */
1317
+ timeoutMs?: number;
1318
+ }
1319
+ /**
1320
+ * The stateless decision dial. `decide` resolves the target, discovers ready
1321
+ * replicas, picks the least-outstanding one, mints the scoped token and POSTs
1322
+ * the flat body to the runtime's `/v1/decision` intake.
1323
+ */
1324
+ declare class StatelessDecisionDial {
1325
+ private readonly opts;
1326
+ private readonly replicas;
1327
+ private readonly nowMs;
1328
+ /** Per-(presence key, app, fn) fresh listing, TTL-bounded to
1329
+ * PRESENCE_FRESH_S. A per-DECISION control-plane read would be the same
1330
+ * design error the session dial was — a network round-trip for state the
1331
+ * runtime plane re-proves every ~5s heartbeat. The cache serves the
1332
+ * caller from memory, refreshes in the BACKGROUND past half the TTL, and
1333
+ * evicts a replica (or the whole entry) the moment a dial to its cached
1334
+ * address fails. */
1335
+ private readonly presenceCache;
1336
+ constructor(opts: StatelessDecisionDialOptions);
1337
+ /** Enabled = the pod secret is configured. Absent ⇒ the session lane serves
1338
+ * (byte-for-byte behavior). */
1339
+ get enabled(): boolean;
1340
+ /**
1341
+ * Resolve a request's lane target WITHOUT dialing: `null` for a SHARED-lane
1342
+ * model when the dial holds NO shared credential — the caller keeps the
1343
+ * session lane. WITH the shared key, a shared target is dialable and rides
1344
+ * through with its full identity (modelId/variant for the billing repin,
1345
+ * `sharedOrgId` for the provider-aware token). The HTTP handlers call this
1346
+ * FIRST and route non-null targets into decide; decide re-resolves its own
1347
+ * target so the one it reports is the one it actually dialed.
1348
+ */
1349
+ targetFor(model: string | undefined): Promise<DialTarget | null>;
1350
+ /**
1351
+ * One decision. Returns the intake's `(status, body)` plus the SYNTHESIZED
1352
+ * completed body the lane stamps its billing receipt from — the runtime's
1353
+ * chat body carries `usage` (OpenAI shape) and `urun_timing`; both are the
1354
+ * exact fields the ledger's receipt stamper reads, mapped once here so the
1355
+ * stateless lane prices identically to the session lane. The RESOLVED
1356
+ * target rides along: the handlers repin the billing identity from it
1357
+ * (the dial's own answer, handed out — never asked for again) before
1358
+ * stamping.
1359
+ */
1360
+ decide(body: Record<string, unknown>, model: string | undefined): Promise<{
1361
+ status: number;
1362
+ body: unknown;
1363
+ completed: unknown;
1364
+ target: DialTarget & {
1365
+ fnName: string;
1366
+ };
1367
+ }>;
1368
+ /** Serve the caller from the cache; a MISS (or a TTL-expired entry) reads
1369
+ * synchronously — the cold path is the one decision that pays the hop —
1370
+ * and an entry past half the TTL fires a single-flight BACKGROUND refresh
1371
+ * that never blocks this caller. */
1372
+ private ensureRows;
1373
+ private readPresence;
1374
+ /** LEAST-OUTSTANDING: pick the ready replica with the fewest in-flight
1375
+ * decisions (ties → the first), skipping replicas in failure cooldown. */
1376
+ pick(rows: RuntimePresenceRow[]): string | null;
1377
+ private post;
1378
+ }
1379
+
928
1380
  /**
929
1381
  * THE CANONICAL USAGE-QUERY LANE — `POST /v1/usage/requests`.
930
1382
  *
@@ -1220,6 +1672,58 @@ interface LedgerWrite {
1220
1672
  prefill_ms: number | null;
1221
1673
  decode_ms: number | null;
1222
1674
  total_ms: number | null;
1675
+ /**
1676
+ * PROXY-OBSERVED TIME TO FIRST STREAMED CONTENT BYTE, in whole
1677
+ * milliseconds — what the CALLER experienced, and therefore the number
1678
+ * Hugging Face's 5s provider-listing gate is about (ENG-337).
1679
+ *
1680
+ * THE RECORD IS THE ONE SOURCE. This is {@link InferenceRecord.ttft_ms}
1681
+ * carried through, measured at the one place TTFT was already measured
1682
+ * (`ProxyStats.track` stamps `turn.ttftMs` on the first text delta) — the
1683
+ * same number the billing log line and the `urun_inference_ttft_seconds`
1684
+ * histogram already publish. The writer adds no second measurement, so the
1685
+ * row cannot disagree with either sink.
1686
+ *
1687
+ * NULL FOR NON-STREAM, NULL FOR UNOBSERVED, NEVER ZERO FOR EITHER. The
1688
+ * record stamps `ttft_ms` only for a streamed turn: a caller who receives
1689
+ * one JSON body at the end was never shown a "first byte", and publishing
1690
+ * the internal timing in its place would put flattering numbers into
1691
+ * exactly the series the HF gate reads. A stream that produced no text
1692
+ * delta before it ended was never observed at all. Null is the only honest
1693
+ * spelling of both; a 0 here would assert a sub-millisecond first byte
1694
+ * nobody measured. (A MEASURED sub-millisecond first byte does round to 0 —
1695
+ * that is a measurement, not an unknown; which side of the `??` a value
1696
+ * arrived on is the whole difference.)
1697
+ *
1698
+ * NOT THE RUNTIME'S `urun_timing.ttft_ms`, which is `queue_ms +
1699
+ * prefill_ms` and still deliberately does not ride the row (see
1700
+ * `TurnResidency`): that is the engine's view of its own internals, this
1701
+ * is the wall-clock the caller saw — network, proxy, cold-wake and all.
1702
+ * Neither can derive the other, so neither is a second spelling of the
1703
+ * other.
1704
+ */
1705
+ ttft_ms: number | null;
1706
+ /**
1707
+ * PROXY-OBSERVED REQUEST RECEIPT → LAST BYTE, in whole milliseconds: the
1708
+ * end-to-end latency the caller experienced for the whole answer
1709
+ * (ENG-337).
1710
+ *
1711
+ * `Math.round` of the record's `duration_ms` — THE SAME MEASUREMENT
1712
+ * {@link startedAtOf} derives the row's `started_at` from, so the row's
1713
+ * window columns cannot disagree by construction: `started_at + e2e_ms`
1714
+ * and `completed_at` describe one clock. Rounded HERE, at the one place
1715
+ * the row is built, because the ledger's column
1716
+ * (`urun_record_inference_request`'s `p_e2e_ms`) is an `integer` — never
1717
+ * rounded a second time somewhere a new spelling could drift from this
1718
+ * one.
1719
+ *
1720
+ * ALWAYS PRESENT on rows this writer produces, unlike {@link ttft_ms}: the
1721
+ * duration is measured for every completed request, streamed or not,
1722
+ * failed or not. The TYPE still admits null because the wire column is
1723
+ * nullable — the contract is stated rather than implied by what this
1724
+ * writer happens to emit today.
1725
+ */
1726
+ e2e_ms: number | null;
1223
1727
  }
1224
1728
  /** What the control plane answers with — the row as it was actually written. */
1225
1729
  interface LedgerRecorded {
@@ -1341,6 +1845,18 @@ interface LedgerFailure {
1341
1845
  */
1342
1846
  declare const LEDGER_SKIP_REASONS: readonly ["no_response_head", "no_org", "model_unresolved", "no_model_named", "queue_full"];
1343
1847
  type LedgerSkipReason = (typeof LEDGER_SKIP_REASONS)[number];
1848
+ /**
1849
+ * THE TWO PRICING STATES a landed row can be in — ENG-317's
1850
+ * NULL-is-not-zero distinction, as a metric label value.
1851
+ *
1852
+ * A CONST ARRAY rather than an inline union because the vocabulary is now
1853
+ * ENUMERATED, not merely constrained: {@link LEDGER_OUTCOMES} materialises
1854
+ * every series this counter can ever produce, and a pricing state that existed
1855
+ * only in a type could not be enumerated. Adding a third state here adds its
1856
+ * series everywhere without a second edit.
1857
+ */
1858
+ declare const LEDGER_PRICING_STATES: readonly ["priced", "unpriced"];
1859
+ type LedgerPricingState = (typeof LEDGER_PRICING_STATES)[number];
1344
1860
  /**
1345
1861
  * The TERMINAL fate of one request's ledger row, as a bounded pair of metric
1346
1862
  * labels. Every `/v1` request that reaches a lane produces exactly one of
@@ -1359,7 +1875,7 @@ type LedgerOutcome =
1359
1875
  /** The row is in the ledger. `priced` / `unpriced` is ENG-317's NULL-is-not-zero distinction. */
1360
1876
  {
1361
1877
  outcome: 'written';
1362
- reason: 'priced' | 'unpriced';
1878
+ reason: LedgerPricingState;
1363
1879
  }
1364
1880
  /**
1365
1881
  * The row was ALREADY in the ledger and it is OURS — an earlier attempt
@@ -1369,7 +1885,7 @@ type LedgerOutcome =
1369
1885
  */
1370
1886
  | {
1371
1887
  outcome: 'already_written';
1372
- reason: 'priced' | 'unpriced';
1888
+ reason: LedgerPricingState;
1373
1889
  }
1374
1890
  /** No row was attempted. */
1375
1891
  | {
@@ -1463,6 +1979,27 @@ interface ResponsesCreateParams {
1463
1979
  * Forwarded verbatim — the serve runtime is the one validator.
1464
1980
  */
1465
1981
  response_format?: unknown;
1982
+ /**
1983
+ * The SPEECH lane's per-request TTS voice (ENG-331, additive like `tools`):
1984
+ * the speaker id a `tts`-task engine synthesizes under (qwen3 CustomVoice
1985
+ * timbre, e.g. "Ryan"). Forwarded on the serve envelope so the runtime's
1986
+ * speak bridge can apply it through the engine's own per-session override
1987
+ * seam (`SpeakSessionConfig.voice` → `set_voice`, urun-python
1988
+ * tts_engine.py). Absent -> key absent: the engine's catalog-configured
1989
+ * default voice applies, today's behavior.
1990
+ */
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;
1466
2003
  }
1467
2004
  /** How one pinned session ended (the native phase machinery's terminal step). */
1468
2005
  interface SessionEndInfo {
@@ -1571,6 +2108,42 @@ interface ProxyClients {
1571
2108
  * allow-all, and never a chat model binding an audio session.
1572
2109
  */
1573
2110
  supportsAudio?(model: string | undefined): Promise<boolean>;
2111
+ /**
2112
+ * The speech lane's capability gate (proxy/speech.ts), backed by
2113
+ * ModelRouter.supportsSpeech — the caller-org catalog's `task` column,
2114
+ * which must vouch `tts` (NOT `stt`: a transcription model must never be
2115
+ * asked to synthesize) INVARIANTLY across placements. Optional at the seam
2116
+ * exactly like {@link supportsImages}: hand-built clients may omit it, in
2117
+ * which case the mounted speech lane answers the loud 501 — never a
2118
+ * silent allow-all, and never a chat model asked to speak.
2119
+ */
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>;
2132
+ /**
2133
+ * THE SPEECH LANE'S VOICE-SET RESOLUTION (proxy/speech.ts): the voices
2134
+ * `/v1/models` publishes for `model` (ModelRouter.speechVoices — the SAME
2135
+ * openrouter-doc resolver the listing's enrichment applies, over the SAME
2136
+ * catalog join the tts gate uses), so the lane validates the requested
2137
+ * `voice` against exactly the set the listing publishes — ONE list
2138
+ * everywhere, never a second copy. The resolution also carries the catalog
2139
+ * defect (`engine_args.voices` present but empty/malformed/disagreeing
2140
+ * across placements), which the lane answers with the loud 500 — never a
2141
+ * silent fallback to the qwen3 card's nine. Optional at the seam exactly
2142
+ * like {@link supportsSpeech}: hand-built clients may omit it, in which
2143
+ * case the mounted speech lane answers the loud 501 — skipping the check
2144
+ * silently would downgrade the lane to unvalidated voices.
2145
+ */
2146
+ speechVoices?(model: string | undefined): Promise<SpeechVoicesResolution>;
1574
2147
  /**
1575
2148
  * `GET /v1/images/models` — the image-capable slice of the model surface
1576
2149
  * (ModelRouter.imageModelList: deployed task-image apps + the shared block
@@ -1578,6 +2151,14 @@ interface ProxyClients {
1578
2151
  * its absence is the loud 501, never a silently empty list.
1579
2152
  */
1580
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>;
1581
2162
  /**
1582
2163
  * THE PRE-DIAL DEFAULT FOR THE LEDGER'S BILLING IDENTITY
1583
2164
  * (ModelRouter.catalogIdentityFor): the CATALOG `(model_id, variant)` a
@@ -1643,8 +2224,47 @@ interface ProxyClients {
1643
2224
  * session; repeat calls return the same lane. Optional at the seam because
1644
2225
  * text-only embeddings exist — but a surface that RECEIVES audio while the
1645
2226
  * embedder wired no `openAudio` must fail LOUD, never drop chunks.
1646
- */
1647
- openAudio?(model: string | undefined): Promise<ProxyAudioLane>;
2227
+ *
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
+ */
2238
+ openAudio?(model: string | undefined, signal?: AbortSignal): Promise<ProxyAudioLane>;
2239
+ /**
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
2244
+ * audio collector here and dials its response turn on the SAME handle
2245
+ * ({@link createResponseOn}), so BOTH legs are one session identity by
2246
+ * construction: a re-home can never move the turn to a second session
2247
+ * while the collector stays subscribed to the first (which missed the
2248
+ * replacement's audio and answered a voiceless 502, or worse, served a
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>;
1648
2268
  /**
1649
2269
  * Open (or reuse) the NATIVE video FRAME lane on the pooled session `model`
1650
2270
  * routes to (transport/media.ts `enableSessionVideo`: discrete JPEG frames →
@@ -1675,6 +2295,38 @@ interface ProxyClients {
1675
2295
  * serve-side session-affinity tag rides (urun-python#1556/#1582).
1676
2296
  */
1677
2297
  sessionHandle(model: string | undefined): Promise<string>;
2298
+ /**
2299
+ * THE SPEECH LANE'S OWN HANDLE (greptile urun-ts#500): the opaque handle
2300
+ * for the SPEECH-SCOPED pooled session (`speech:<pool key>` — routing.ts
2301
+ * {@link ModelRouter.speechSessionFor}), a slot no interactive lane can
2302
+ * resolve. The speech lane resolves its collector AND its turn through
2303
+ * THIS handle ({@link openAudioOn} + {@link createResponseOn} unchanged),
2304
+ * because the per-session AudioBridge broadcasts every untagged output
2305
+ * frame to every subscriber: a speech collection on the INTERACTIVE
2306
+ * pooled session would stitch a concurrent Realtime/Gemini Live turn's
2307
+ * frames into the returned WAV. Optional at the seam exactly like
2308
+ * {@link openAudioOn}; the speech lane answers its absence with the loud
2309
+ * 501 — NEVER a silent fall-back to the shared interactive session
2310
+ * (that "fallback" is precisely the mixed-audio defect).
2311
+ */
2312
+ speechSessionHandle?(model: string | undefined): Promise<string>;
2313
+ /**
2314
+ * EVICT the SPEECH-scoped pooled session a handle names (CodeRabbit
2315
+ * urun-ts#500, round 8) — through the pool's OWN identity-guarded
2316
+ * eviction path (ModelRouter.evict under the handle's `speech:<key>`
2317
+ * slot, never a bespoke close), so the NEXT speechSessionHandle acquire
2318
+ * opens a FRESH session. The lane calls this after a collector failure
2319
+ * that leaves the session's audio state unknown — above all a turn-cap
2320
+ * timeout, where the serialized lane is released while the pinned
2321
+ * session may still be emitting the timed-out turn's audio: a later
2322
+ * request on the SAME entry would collect those stale frames into its
2323
+ * own answer. Throws {@link SessionGoneError} when the handle names a
2324
+ * session the pool no longer holds (the eviction's goal is then already
2325
+ * true — the lane treats it as success). Optional at the seam exactly
2326
+ * like {@link speechSessionHandle}; the speech lane answers its absence
2327
+ * with the loud 501 — never a silent reuse of the possibly-stale entry.
2328
+ */
2329
+ evictSpeechSession?(handle: string): Promise<void>;
1678
2330
  /**
1679
2331
  * createResponse PINNED to the exact session a handle names
1680
2332
  * (ModelRouter.sessionForHandle). Throws SessionGoneError LOUDLY when that
@@ -1687,16 +2339,48 @@ interface ProxyClients {
1687
2339
  * Subscribe to the pinned session's terminal end via core's NATIVE phase
1688
2340
  * machinery (Session.onPhase → terminal 'expired'/'ended'/'error'). Fires
1689
2341
  * `cb` once. Throws SessionGoneError if the handle's session is already
1690
- * gone — which doubles as the loud reattach check at resume time. Returns
1691
- * the unsubscribe.
1692
- */
2342
+ * gone — which doubles as the loud reattach check at resume time. Returns
2343
+ * the unsubscribe.
2344
+ */
1693
2345
  onSessionEnd(handle: string, cb: (end: SessionEndInfo) => void): Promise<() => void>;
2346
+ /**
2347
+ * THE STATELESS DIRECT PATH (stateless-dispatch.ts): the decision dial to
2348
+ * the serving pods' `POST /v1/decision` intake, present only when the
2349
+ * deployment carries the runtime scoped-token secret. Absent ⇒ every
2350
+ * decision rides the session lane exactly as before (the staged rollout).
2351
+ * Per-org: the HOSTED registry builds one per tenant so the minted token's
2352
+ * tenant claim is always the CALLER's org.
2353
+ */
2354
+ readonly stateless?: StatelessDecisionDial;
1694
2355
  }
1695
2356
  /**
1696
2357
  * The audio lane handle `openAudio` returns — structurally the transport
1697
2358
  * AudioBridge (media.ts): base64 PCM16 @24 kHz mono in both directions.
2359
+ * `sessionId` is the pooled uRun session the lane rides (core `Session.id`),
2360
+ * when the backhaul reports it — the identity the realtime lane's structured
2361
+ * lifecycle log carries so an orphaned session is traceable from the proxy
2362
+ * log alone. Null is the HONEST ABSENCE (the same line {@link
2363
+ * PinnedAudioLane.sessionId} and the re-homing dial's `sessionIdOf` take —
2364
+ * an id-less session reports no id, never a fabricated one).
2365
+ */
2366
+ type ProxyAudioLane = Pick<AudioBridge, 'appendInputAudio' | 'onOutputAudio'> & {
2367
+ sessionId?: string | null;
2368
+ };
2369
+ /**
2370
+ * The PINNED audio lane handle `openAudioOn` returns ({@link ProxyClients.openAudioOn}):
2371
+ * the session's native audio lane PLUS the identity of the exact pooled session
2372
+ * it is bound to — the two facts the speech lane needs to keep its collector and
2373
+ * its response turn on ONE session identity.
1698
2374
  */
1699
- type ProxyAudioLane = Pick<AudioBridge, 'appendInputAudio' | 'onOutputAudio'>;
2375
+ interface PinnedAudioLane extends ProxyAudioLane {
2376
+ /**
2377
+ * The uRun session id of the pooled session this lane is bound to — the
2378
+ * honest instance link for a request whose collector and turn ride one
2379
+ * handle, the same derivation the dial report's `sessionId` uses. Null
2380
+ * when the entry carries no native session id; never a fabricated one.
2381
+ */
2382
+ readonly sessionId: string | null;
2383
+ }
1700
2384
  /**
1701
2385
  * The video frame-lane handle `openVideo` returns — structurally the
1702
2386
  * transport VideoFrameLane (media.ts): one raw encoded JPEG frame per call,