@urun-sh/openai 0.5.4 → 0.5.6

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 (55) hide show
  1. package/dist/{ResponsesClient-BYx3YLGo.d.ts → ResponsesClient-BE3-hx3m.d.ts} +2 -0
  2. package/dist/{ResponsesClient-Dft3bg3b.d.cts → ResponsesClient-DjZWjlFH.d.cts} +2 -0
  3. package/dist/chunk-2T2YYBVX.js +1 -0
  4. package/dist/chunk-3WAJD62J.js +4 -0
  5. package/dist/{chunk-K23AZPI4.js → chunk-5HKWNK3O.js} +1 -1
  6. package/dist/chunk-7M6LY6DR.js +2 -0
  7. package/dist/chunk-BSHT6RZZ.js +1 -0
  8. package/dist/{chunk-GBBY3PCZ.js → chunk-FP4RSAIE.js} +1 -1
  9. package/dist/chunk-HR5H6S7L.js +1 -0
  10. package/dist/chunk-NY23USZF.js +57 -0
  11. package/dist/chunk-VLRMJRLS.js +6 -0
  12. package/dist/gemini-live.cjs +2 -2
  13. package/dist/gemini-live.d.cts +17 -2
  14. package/dist/gemini-live.d.ts +7 -2
  15. package/dist/gemini-live.js +1 -1
  16. package/dist/hosted/bin.cjs +65 -37
  17. package/dist/hosted/bin.js +3 -3
  18. package/dist/hosted/index.cjs +46 -19
  19. package/dist/hosted/index.d.cts +760 -272
  20. package/dist/hosted/index.d.ts +203 -107
  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/{models-NYMZrklp.d.cts → models-DUdx_Y6X.d.cts} +1 -1
  27. package/dist/pi-extension/index.cjs +7 -7
  28. package/dist/pi-extension/index.d.cts +1 -1
  29. package/dist/pi-extension/index.d.ts +1 -1
  30. package/dist/pi-extension/index.js +1 -1
  31. package/dist/pi-extension/standalone.cjs +48 -48
  32. package/dist/proxy/cli.cjs +54 -40
  33. package/dist/proxy/cli.js +14 -14
  34. package/dist/proxy/index.cjs +33 -22
  35. package/dist/proxy/index.d.cts +546 -4
  36. package/dist/proxy/index.d.ts +185 -4
  37. package/dist/proxy/index.js +1 -1
  38. package/dist/responses-turn-Dr37N3kH.d.ts +486 -0
  39. package/dist/responses-turn-tNUHq5b0.d.cts +1807 -0
  40. package/dist/{translator-C9uPKypK.d.ts → translator-BCoRFaTs.d.ts} +1 -1
  41. package/dist/{translator-CcDBEfvm.d.cts → translator-Bh0Bp_Ie.d.cts} +3 -3
  42. package/dist/{video-out-D20UuJ8G.d.cts → video-out-CCksIcj8.d.cts} +5 -5
  43. package/package.json +12 -11
  44. package/dist/chunk-4MSBS7M6.js +0 -1
  45. package/dist/chunk-5NXM4IO3.js +0 -2
  46. package/dist/chunk-CWJRDBDC.js +0 -1
  47. package/dist/chunk-OI2OY32M.js +0 -1
  48. package/dist/chunk-RQBD4OAA.js +0 -6
  49. package/dist/chunk-STFRT5OY.js +0 -1
  50. package/dist/chunk-VNM4M4P3.js +0 -40
  51. package/dist/server-DuH_5OD9.d.ts +0 -84
  52. package/dist/server-wmUnMewW.d.cts +0 -206
  53. /package/dist/{chunk-YSFSRI3D.js → chunk-5CCJUNH7.js} +0 -0
  54. /package/dist/{models-NYMZrklp.d.ts → models-DUdx_Y6X.d.ts} +0 -0
  55. /package/dist/{video-out-CWesbk12.d.ts → video-out-IGQ4YQ-c.d.ts} +0 -0
@@ -1,5 +1,547 @@
1
- export { O as OpenAIProxyOptions, P as ProxyClients, a as ProxyIdentity, b as ProxyVideoOutLane, c as createOpenAIProxy } from '../server-wmUnMewW.cjs';
2
- export { h as GEMINI_LIVE_PATH, V as VIDEO_OUT_SETUP_KEY } from '../translator-CcDBEfvm.cjs';
3
- import 'node:http';
4
- import '../video-out-D20UuJ8G.cjs';
1
+ import { Server, IncomingMessage } from 'node:http';
2
+ import { f as ProxyHandlerOptions, P as ProxyClients } from '../responses-turn-tNUHq5b0.cjs';
3
+ export { D as DialObserver, a as DialReport, g as ProxyIdentity, h as ProxyVideoOutLane, S as SessionGoneError, U as UnknownModelError } from '../responses-turn-tNUHq5b0.cjs';
4
+ export { h as GEMINI_LIVE_PATH, V as VIDEO_OUT_SETUP_KEY } from '../translator-Bh0Bp_Ie.cjs';
5
+ import { WebSocket } from 'ws';
6
+ import '../models-DUdx_Y6X.cjs';
7
+ import '../video-out-CCksIcj8.cjs';
5
8
  import '../types-lsVTbNcH.cjs';
9
+
10
+ /**
11
+ * The local OpenAI-compatible proxy — `npx @urun-sh/openai proxy`.
12
+ *
13
+ * A loopback HTTP server speaking the OpenAI REST surface (`/v1/models`,
14
+ * `/v1/responses`, `/v1/chat/completions`, SSE streaming included), so any
15
+ * OpenAI-env-var tool — coding agents first — integrates with uRun with ZERO
16
+ * code changes: point `OPENAI_BASE_URL` at it and keep making plain, local,
17
+ * "inefficient" HTTP requests. The proxy backhauls each request over the uRun
18
+ * session transport (the `@urun-sh/openai` Responses client → session doc
19
+ * lanes + streams — no public OpenAI-style HTTP leaves the machine), which is
20
+ * also where the delta-sync chat-state doc lane (urun-python
21
+ * `urun/serve/chat_state.py`) plugs in as the transport evolves.
22
+ *
23
+ * Local-trust model: binds 127.0.0.1 by default; `apiKey` (when set) must
24
+ * match the agent's `Authorization: Bearer` — otherwise any local bearer is
25
+ * accepted (the key the agent sends is NEVER forwarded upstream; uRun auth is
26
+ * the session's own).
27
+ */
28
+
29
+ /** The proxy server's own options: a FIXED backhaul (the local CLI lane). */
30
+ interface OpenAIProxyOptions extends ProxyHandlerOptions {
31
+ clients: ProxyClients;
32
+ }
33
+ /** Build (not listen) the proxy server — the caller owns listen/close. */
34
+ declare function createOpenAIProxy(options: OpenAIProxyOptions): Server;
35
+
36
+ /**
37
+ * The Gemini Live WS surface: a minimal `ws` upgrade hook on the proxy's
38
+ * node:http server plus one per-connection protocol driver. ALL session
39
+ * plumbing is the proxy's existing seam — `ProxyClients` (lazily-opened
40
+ * pooled per-app uRun sessions behind ModelRouter) — the driver holds ONLY
41
+ * protocol translation state: the conversation transcript the Live API keeps
42
+ * server-side (Live clients never re-send history, so the driver accumulates
43
+ * Responses input items and sends the full transcript per turn; the
44
+ * backhaul's delta-sync chat-state lane makes the re-send cheap).
45
+ *
46
+ * SESSION RESUMPTION (`setup.sessionResumption`) rides the session-identity
47
+ * seam (ProxyClients.sessionHandle / onSessionEnd → ModelRouter.handleFor /
48
+ * sessionForHandle): a resumption handle snapshots the Live conversation
49
+ * PLUS the opaque handle of the pooled uRun session serving it, so resuming
50
+ * reattaches the SAME session (session-affine slot + doc state — the
51
+ * platform's native resume, urun-python#1556/#1582). A handle whose session
52
+ * is gone fails LOUD (1008) — a fresh session is never silently sold as a
53
+ * resume.
54
+ *
55
+ * HOSTED MULTI-TENANT MODE: `attachGeminiLive` either rides a FIXED local
56
+ * backhaul (the `urun compat proxy` lane: `clients` + optional `apiKey`) or
57
+ * resolves PER REQUEST through `authenticate` (the hosted lane), which runs
58
+ * to completion BEFORE `ws.handleUpgrade` — a refused credential never
59
+ * becomes a Live session. Header credentials (`x-goog-api-key` or
60
+ * `Authorization: Bearer`) and the browser `?key=` channel are
61
+ * distinguished; contradictory distinct credentials are refused, and the
62
+ * hosted lane refuses `?key=` outright (origin policy: long-lived org keys
63
+ * must never ride browser query strings) as well as missing auth.
64
+ * Resumption snapshots are bound to the verified caller's scope — another
65
+ * key/org cannot recover a transcript even holding the handle (loud 1008,
66
+ * identical to an unknown handle, so handle existence never leaks).
67
+ *
68
+ * COMPOSITION: the attach claims ONLY the BidiGenerateContent upgrade path
69
+ * and leaves every other upgrade untouched for the next `upgrade` listener
70
+ * (the /v1/responses acceptor, then — registered LAST by the embedder — the
71
+ * final refusal of unclaimed upgrades). It returns an async close handle
72
+ * (ws-native lifecycle: graceful close frame to every live socket, then
73
+ * `wss.close()`) for drain to run BEFORE HTTP-server closure.
74
+ *
75
+ * goAway: wired to core's NATIVE terminal phase signal via
76
+ * ProxyClients.onSessionEnd. MISSING PRIMITIVE (surfaced in the PR): core
77
+ * exposes `endsAt` but no PRE-expiry notice event, so goAway is emitted AT
78
+ * the moment of loss (timeLeft ≈ 0s) — per the no-invented-timers rule.
79
+ *
80
+ * Loud-failure contract (no silent degradation):
81
+ * 1007 protocol violations / declared-follow-up inputs (automatic
82
+ * server-side VAD…)
83
+ * 1008 unknown model (sendUnknownModel's WS mirror) / resumption handle
84
+ * whose session is gone (CLOSE_SESSION_GONE)
85
+ * 1011 upstream failures (error event, stream without response.completed,
86
+ * pooled session terminal loss — preceded by goAway)
87
+ *
88
+ * Manual VAD/activity + barge-in: with
89
+ * `setup.realtimeInputConfig.automaticActivityDetection.disabled: true` the
90
+ * client marks its own activity windows (`realtimeInput.activityStart` /
91
+ * `activityEnd`). An activityStart while a generation is in flight is a
92
+ * BARGE-IN (activityHandling START_OF_ACTIVITY_INTERRUPTS, the default): the
93
+ * turn loop breaks out of its for-await, which calls `return()` on the
94
+ * upstream async iterator — the async-iterator-native cancel the /v1/messages
95
+ * lane uses for client disconnects (server.ts), no bespoke abort plumbing —
96
+ * then emits the golden `serverContent.interrupted: true` and DROPS the
97
+ * partial turn (no generationComplete/turnComplete, no transcript adoption).
98
+ * Cancellation lands at the next upstream event, same as the HTTP lane.
99
+ */
100
+
101
+ /**
102
+ * The upstream seams the Live lane uses: the pooled-session Responses call,
103
+ * the pooled session's NATIVE audio lanes (`openAudio` — the same
104
+ * enableSessionAudio/AudioBridge machinery RealtimeClient.enableAudio
105
+ * rides), the NATIVE video frame lane (`openVideo` — enableSessionVideo's
106
+ * rt-video-in named-DATA frame path), and the session-identity seam
107
+ * (sessionHandle/onSessionEnd → ModelRouter.handleFor/sessionForHandle)
108
+ * sessionResumption rides.
109
+ */
110
+ type GeminiLiveClients = Pick<ProxyClients, 'createResponse' | 'openAudio' | 'openVideo' | 'openVideoOut' | 'sessionHandle' | 'onSessionEnd'>;
111
+ interface LiveSnapshot {
112
+ model: string;
113
+ /** The session-identity seam's opaque handle for the pooled uRun session. */
114
+ proxyHandle: string;
115
+ /** The Live conversation at snapshot time, as Responses input items. */
116
+ transcript: Array<Record<string, unknown>>;
117
+ }
118
+ /**
119
+ * One registry per attached proxy server (created in {@link attachGeminiLive}).
120
+ *
121
+ * Snapshots are keyed by CALLER SCOPE + handle: a handle minted for one
122
+ * verified caller is unreachable to every other caller — handle in hand or
123
+ * not, the lookup is exactly the unknown-handle loud 1008, so handle
124
+ * existence is never leaked across tenants. The scope is the authorized
125
+ * caller's identity (LOCAL_SCOPE on the local lane, the hosted lane's
126
+ * verified-caller subject); the conversation dimension lives in the handle
127
+ * itself, which names one conversation's snapshot. Multiattach is normal:
128
+ * concurrent connections of the SAME caller may resume the SAME handle.
129
+ *
130
+ * HONEST SCOPE (not HA): this registry is per-process, per-attach. A replica
131
+ * restart loses every handle (loud 1008, never a silent restart), and no
132
+ * other replica can serve one. Cross-replica / persistent resumption needs a
133
+ * shared persistence plane for snapshots + session identity — a reported
134
+ * platform requirement, never silently claimed here.
135
+ */
136
+ declare class ResumptionRegistry {
137
+ private readonly snapshots;
138
+ store(scope: string, handle: string, snapshot: LiveSnapshot): void;
139
+ get(scope: string, handle: string): LiveSnapshot | undefined;
140
+ }
141
+ /**
142
+ * One authorized Live call: the backhaul to ride and the tenant scope its
143
+ * resumption snapshots bind to.
144
+ */
145
+ interface GeminiLiveCall {
146
+ clients: GeminiLiveClients;
147
+ /**
148
+ * The verified caller's scope (hosted: derived from the verified org API
149
+ * key — e.g. TenantRegistry's non-secret `tenantSubject`). A resumption
150
+ * handle minted under one scope is UNREACHABLE under any other, and the
151
+ * failed lookup is the ordinary unknown-handle loud 1008 — never a
152
+ * "wrong tenant" error that would confirm the handle exists.
153
+ */
154
+ scope: string;
155
+ /**
156
+ * HOSTED conversation binding (optional): the lane calls it with every
157
+ * resume handle it mints, so the embedder can index handle → the private
158
+ * conversation backhaul that will serve a resume of it. Absent on the
159
+ * local lane (fixed clients).
160
+ */
161
+ onBindResumeHandle?: (handle: string) => void;
162
+ /**
163
+ * HOSTED (optional): resolve the backhaul a minted resume handle belongs
164
+ * to. Runs at resume setup BEFORE any seam call — the resumed connection
165
+ * reattaches the SAME private conversation (same native session identity,
166
+ * owner binding enforced); a throw is the loud session-gone close (1008).
167
+ * Absent on the local lane.
168
+ */
169
+ clientsForResume?: (resumeHandle: string) => Promise<GeminiLiveClients>;
170
+ /**
171
+ * HOSTED per-attachment release (optional): called ONCE when this
172
+ * connection's socket closes, so the embedder can drop THIS attachment's
173
+ * claim on the private conversation backhaul (native detach at the last
174
+ * attachment — no resource keepalive merely to preserve local resume).
175
+ * Fire-and-forget from the lane; must be idempotent. Absent on the local
176
+ * lane.
177
+ */
178
+ release?: () => Promise<void>;
179
+ }
180
+ /**
181
+ * HOSTED per-request auth: verify the single Gemini credential the client
182
+ * presented (already policy-checked — no missing auth, no `?key=`, no
183
+ * contradictory inputs) and resolve THAT caller's own backhaul + tenant
184
+ * scope. Runs to completion BEFORE `ws.handleUpgrade`; a throw refuses the
185
+ * upgrade with the error's own `status` (e.g. ProxyAuthError's 401) or 502
186
+ * when the error carries none.
187
+ */
188
+ type GeminiLiveAuthenticate = (credential: string, req: IncomingMessage) => Promise<GeminiLiveCall>;
189
+ interface GeminiLiveOptions {
190
+ /** The FIXED backhaul (local CLI lane). Required unless `authenticate`. */
191
+ clients?: GeminiLiveClients;
192
+ /**
193
+ * The proxy's local bearer (OpenAIProxyOptions.apiKey). Gemini Live clients
194
+ * authenticate with `?key=…` or `x-goog-api-key`; both are accepted here
195
+ * (never forwarded upstream — uRun auth is the session's own). Omitted ⇒
196
+ * the local lane is open (a loopback embedder's choice).
197
+ */
198
+ apiKey?: string;
199
+ /**
200
+ * HOSTED per-request resolution; mutually exclusive with a fixed
201
+ * `clients`/`apiKey` lane (throw at attach). Sets the HOSTED credential
202
+ * policy: missing auth refused, `?key=` refused (origin policy — long-lived
203
+ * org keys must never appear in browser query strings), contradictory
204
+ * distinct credentials refused.
205
+ */
206
+ authenticate?: GeminiLiveAuthenticate;
207
+ }
208
+ /**
209
+ * Attach the Gemini Live BidiGenerateContent WS endpoint to an HTTP server.
210
+ *
211
+ * COMPOSITION: this attach claims ONLY the BidiGenerateContent upgrade path.
212
+ * Upgrades on any other path are left untouched for the NEXT `upgrade`
213
+ * listener (the /v1/responses acceptor, and — registered LAST by the
214
+ * embedder — the final refusal of unclaimed upgrades). On the claimed path
215
+ * auth happens BEFORE `ws.handleUpgrade`: the local fixed bearer 401s, and
216
+ * the hosted lane runs `authenticate` to completion (401 on a
217
+ * refused/missing credential, the error's own status for a resolver
218
+ * rejection, 502 when the error carries none — never an open pass).
219
+ *
220
+ * Returns the lane's close handle — `await close()` runs the ws-native
221
+ * lifecycle: a graceful close() to every live socket (close frames, sends
222
+ * flushed), then `wss.close()` resolving when the last socket is gone.
223
+ * Disconnects are DETACHES: closing never ends a pooled session. The handle
224
+ * is idempotent.
225
+ */
226
+ declare function attachGeminiLive(server: Server, options: GeminiLiveOptions): () => Promise<void>;
227
+ /**
228
+ * Drive ONE already-authenticated Gemini Live socket — the per-connection
229
+ * driver underneath {@link attachGeminiLive}, for surfaces that own the
230
+ * upgrade + auth themselves (e.g. a destination-side SFU route that
231
+ * pre-authenticates and hands the live `WebSocket` over).
232
+ *
233
+ * Runs the full protocol loop (setup → setupComplete, clientContent /
234
+ * realtimeInput / toolResponse → turns, native audio/video lanes, session
235
+ * resumption, goAway) against `call`'s seams, and detaches cleanly when the
236
+ * socket closes: `call.release?.()` fires once (per-attachment release —
237
+ * native detach at the last attachment), sessions are never ended by a
238
+ * socket closing.
239
+ *
240
+ * `registry` collects this driver's resumption snapshots. Omit it for a
241
+ * private per-socket registry (handles then live exactly as long as the
242
+ * socket); pass a shared one to let several sockets resume each other's
243
+ * handles — always scoped by `call.scope`, never across callers.
244
+ */
245
+ declare function serveGeminiLiveSocket(ws: WebSocket, call: GeminiLiveCall, registry?: ResumptionRegistry): void;
246
+
247
+ /**
248
+ * `attachOpenAIRealtime(server, options)` — the WS upgrade hook for the OpenAI
249
+ * Realtime surface (GA protocol subset; see protocol.ts for the event subset).
250
+ *
251
+ * Contract (Main-owned composition; this lane claims ONLY `/v1/realtime`):
252
+ * - Upgrades on any other path are passed through UNTOUCHED — a plain
253
+ * `next()` listener, so sibling lanes (Gemini Live, Responses WS) and the
254
+ * hosted composition keep their own upgrade hooks.
255
+ * - Clients are resolved/authenticated ASYNCHRONOUSLY BEFORE the upgrade is
256
+ * accepted: no 101 is written until `resolver.resolve(req)` settles. A
257
+ * thrown error maps `err.status` (default 502) to the HTTP reject;
258
+ * 401/403 keep their status.
259
+ * - The resolved binding carries a PRIVATE native uRun session (fresh or
260
+ * explicitly-resumed) and, iff voice, a PRIVATE audio bridge — never the
261
+ * pooled tenant lane. The connection's cross-caller claim is invariant
262
+ * validation, not capacity policy.
263
+ * - Returns an async close function: waits for every touched socket to
264
+ * settle, then closes the internal WebSocketServer. It does NOT end the
265
+ * native uRun sessions (detach ≠ session death — resume is Main-owned;
266
+ * the binding seam decides lifetime).
267
+ */
268
+
269
+ declare const OPENAI_REALTIME_PATH = "/v1/realtime";
270
+ /**
271
+ * The resolver seam — the ONLY credential, modality, and session-allocation
272
+ * authority this adapter recognizes. Implemented by Main's composition (and
273
+ * by tests with an injected fake). Accepted in EITHER form: a bare
274
+ * `(req) => Promise<binding>` (the {@link ProxyClientsFor}-style convention)
275
+ * or a `{ resolve(req) }` object.
276
+ */
277
+ type OpenAIRealtimeResolver<S = unknown> = ((req: IncomingMessage) => Promise<RealtimeBinding<S>>) | {
278
+ resolve(req: IncomingMessage): Promise<RealtimeBinding<S>>;
279
+ };
280
+ interface RealtimeBinding<S = unknown> {
281
+ /**
282
+ * The PRIVATE native uRun session serving this conversation (the shared
283
+ * per-conversation factory in hosted/tenants.ts allocates it; reuse/multi-
284
+ * attach is sanctioned ONLY for the SAME authorized conversation on an
285
+ * explicit resume — two different conversations MUST NOT receive one
286
+ * session). The upstream turn seam is EITHER form:
287
+ * - `session.sendResponseCreate(params, input, requestId)` (the direct
288
+ * SdkTransport-style seam), or
289
+ * - `clients.createResponse(params)` — the canonical ProxyClients seam
290
+ * the conversation factory hands out (`input` rides inside `params`).
291
+ * A binding with neither is rejected at upgrade time, LOUD.
292
+ */
293
+ session?: S & {
294
+ sendResponseCreate?(params: unknown, input: unknown[], requestId: string): AsyncIterable<unknown>;
295
+ };
296
+ clients?: Pick<ProxyClients, 'createResponse'>;
297
+ /**
298
+ * Authoritative modality verdict from the resolver. A function NAME proves
299
+ * nothing — without `voice: true`, audio events are refused and no audio
300
+ * primitive is ever opened on the session.
301
+ */
302
+ voice: boolean;
303
+ /** The PRIVATE audio bridge for THIS conversation (required iff `voice`). */
304
+ audioSession?: {
305
+ audio: {
306
+ appendInputAudio(base64Pcm16: string): void;
307
+ onOutputAudio(handler: (b64: string) => void): () => void;
308
+ };
309
+ /** Cross-caller reuse registry (invariant validation; see connection.ts). */
310
+ openaiRealtimeClaims?: Set<RealtimeBinding['audioSession']>;
311
+ };
312
+ /** `model` the binding names for this conversation (overrides session config default). */
313
+ model?: string;
314
+ /** True when `session` is a resumed authorized session (vs a fresh private one). */
315
+ resumed?: boolean;
316
+ /**
317
+ * The backend's OWN turn/transcript lane — the §5 `stt` named stream
318
+ * (urun.serve.transcribe_bridge wire shape: `t` = delta/final/turn_start/
319
+ * turn_end/turn_eager_end/turn_resumed, `delta` text, additive `start_ms`
320
+ * and semantic-VAD `confidence`). The resolver maps `session.stream('stt')
321
+ * .messages()` onto this. Its `turn_end` IS the native commit and
322
+ * `turn_start` IS the native barge-in — the adapter drives server-VAD
323
+ * emulation, real transcription events, and model-visible committed audio
324
+ * from it. Absent = the backend wired no turn lane: manual mode only.
325
+ */
326
+ turns?: () => AsyncIterable<{
327
+ kind: 'delta' | 'final' | 'turn_start' | 'turn_end' | 'turn_eager_end' | 'turn_resumed';
328
+ text?: string;
329
+ confidence?: number;
330
+ startMs?: number;
331
+ }>;
332
+ /**
333
+ * Release THIS attachment when the socket closes (the hosted
334
+ * conversation-factory seam — the twin of the Gemini Live binding's
335
+ * `release`): per-attachment release, native detach underneath, the named
336
+ * session itself stays live. Absent (the local fixed-clients lane) = the
337
+ * pooled sessions are the process's own and outlive the socket.
338
+ */
339
+ release?: () => Promise<void> | void;
340
+ }
341
+ interface OpenAIRealtimeOptions<S = unknown> {
342
+ resolver: OpenAIRealtimeResolver<S>;
343
+ }
344
+ /**
345
+ * Attach the OpenAI Realtime WS endpoint to `server`. Upgrades on any other
346
+ * path pass through untouched. Returns an async close function.
347
+ */
348
+ declare function attachOpenAIRealtime<S = unknown>(server: Server, options: OpenAIRealtimeOptions<S>): () => Promise<void>;
349
+
350
+ /**
351
+ * One authenticated OpenAI Realtime WS connection — the adapter's core.
352
+ *
353
+ * OWNERSHIP MODEL (Main's contract, 2026-09-08): the binding seam hands this
354
+ * connection an EXCLUSIVE, connection-private native uRun session (and, when
355
+ * the resolver authoritatively binds voice, a PRIVATE audio bridge). The
356
+ * pooled tenant `openAudio` lane is NEVER used here — `binding.audioSession`
357
+ * comes from the resolver's own per-conversation acquisition, and
358
+ * {@link claimVoiceBinding} is the INVARIANT VALIDATION that an accidental
359
+ * cross-caller reuse of one bridge fails LOUD (it is not a capacity policy).
360
+ * Multi-attach on the SAME authorized session (explicit resume) is the one
361
+ * sanctioned shared form: one bridge owner at a time, validated by claim.
362
+ *
363
+ * NO-OP POLICY: every unsupported client event or backend gap renders as a
364
+ * LOUD `error` server event (or a close) — never a silently ignored frame.
365
+ */
366
+
367
+ declare class OpenAIRealtimeConnection<S = unknown> {
368
+ readonly id: string;
369
+ private readonly ws;
370
+ private readonly binding;
371
+ private readonly onClosed?;
372
+ private config;
373
+ private readonly sessionId;
374
+ private readonly conversationId;
375
+ private readonly items;
376
+ /** Uncommitted input audio (base64 PCM16 chunks) held locally until commit. */
377
+ private pendingAudioBytes;
378
+ private pendingAudioTotalBytes;
379
+ /** Committed audio of the frozen turn, dispatched at response.create. */
380
+ private frozenAudio;
381
+ private inFlight;
382
+ private activeResponseId;
383
+ /** Output events already flowed for the active response (post-flush faults → error event, pre-flush → response.failed). */
384
+ private responseFlushed;
385
+ private cancelled;
386
+ private voiceClaimed;
387
+ private audioLaneOwner;
388
+ private audioUnsubscribe;
389
+ private outOfTurnAudioWarned;
390
+ private audioOnlyTranscriptGapWarned;
391
+ private closed;
392
+ /** The backend's turn/transcript lane consumer (binding.turns), if wired. */
393
+ private turnLaneAbort;
394
+ /** Accumulated transcript of the OPEN user turn (delta/final events). */
395
+ private turnTranscript;
396
+ /** Transcript of the LAST committed turn (the model-visible commit). */
397
+ private lastCommittedTranscript;
398
+ constructor(opts: {
399
+ ws: WebSocket;
400
+ binding: RealtimeBinding<S>;
401
+ onClosed?: (conn: OpenAIRealtimeConnection<S>) => void;
402
+ });
403
+ /**
404
+ * Consume the backend's OWN turn/transcript lane (§5 stt stream): the
405
+ * semantic-VAD boundary events drive REAL protocol semantics — turn_start
406
+ * is the native barge-in, turn_end is the native commit, deltas/finals are
407
+ * the REAL input transcription. This is the server-VAD primitive, not an
408
+ * emulation gap: the voice engine itself decides turn boundaries.
409
+ */
410
+ private startTurnLane;
411
+ /** One backend turn/transcript event → REAL GA protocol events. */
412
+ private onTurnEvent;
413
+ /** The committed turn's transcript, consumed once (null = none). */
414
+ private takeTurnTranscript;
415
+ private onMessage;
416
+ private handle;
417
+ /** Refuse loudly when a text-only-resolved session is asked to carry audio. */
418
+ private ensureVoice;
419
+ /**
420
+ * Cross-caller reuse guard — the INVARIANT check, not capacity policy. The
421
+ * resolver must hand every independent conversation its OWN bridge; if two
422
+ * connections ever hold one, the second fails LOUD instead of mixing audio.
423
+ */
424
+ private claimVoiceBinding;
425
+ private releaseVoiceBinding;
426
+ private warnAudioOnlyTranscriptGap;
427
+ /**
428
+ * The upstream turn seam, normalized: a binding carries EITHER
429
+ * `session.sendResponseCreate(params, input, requestId)` (direct
430
+ * SdkTransport-style) OR `clients.createResponse(params)` (the canonical
431
+ * ProxyClients seam the shared conversation factory hands out). A binding
432
+ * with neither was already rejected at upgrade time; this is the loud
433
+ * backstop. The ProxyClients seam is PROMISE-VALUED
434
+ * (`rehomingCreateResponse` dials inside an async function — its
435
+ * private-backhaul cold wake can park the promise for seconds), so it is
436
+ * awaited here; awaiting the SdkTransport generator is a no-op. The cast
437
+ * states the seam's REAL union: typing it sync-iterable is what let the
438
+ * unawaited promise reach `for await` as "R is not async iterable" (the
439
+ * prod-usw2 speak-turn kill, 2026-09-14).
440
+ */
441
+ private upstreamStream;
442
+ private startResponse;
443
+ /**
444
+ * Bridge voice OUT frame → `response.output_audio.delta` on the open turn.
445
+ *
446
+ * OUT-OF-TURN frames split by CONTENT, per the platform's own liveness
447
+ * model (urun-python #1812/#1827): the runtime's egress holds a DECLARED
448
+ * quiet track — `CtxMediaTransport`'s silence keepalive emits Opus silence
449
+ * whenever the app is silent, including before the first response.create —
450
+ * so an all-quiet frame with no open turn is that muted egress, not voice.
451
+ * It is dropped quietly (the same reading as a negotiated muted WebRTC
452
+ * track). A NON-quiet frame with no open turn is real voice the adapter
453
+ * cannot attribute — still the loud, once-per-connection
454
+ * `out_of_turn_audio` error that caught the speak-lane bug class.
455
+ */
456
+ private onOutputAudioFrame;
457
+ /** One assistant message item per turn (text and audio parts share it). */
458
+ private messageItemAdded;
459
+ /** true when an audio delta opened the message item this turn. */
460
+ private messageItemOpenForAudio;
461
+ private beginMessageItem;
462
+ private beginAudioMessageItem;
463
+ /**
464
+ * Close every item the turn opened, in first-open order, adopt them into
465
+ * the transcript, and emit their `done` events.
466
+ */
467
+ private closeOpenItems;
468
+ private responseObject;
469
+ private failedResponse;
470
+ /**
471
+ * `response.done` — REAL usage only: the serve runtime emits
472
+ * `{input_tokens, output_tokens, total_tokens}` on terminal bodies; when it
473
+ * sent none, `usage: null` is delivered (never zeros dressed as counts).
474
+ */
475
+ private doneResponse;
476
+ private upstreamErrorBody;
477
+ private emitUpstreamFault;
478
+ /**
479
+ * Cancel the in-flight response. The upstream iterator is stopped through
480
+ * the for-await `break` protocol (its `return()` runs — no bespoke cancel
481
+ * machinery); the turn stops at the NEXT upstream event and the partial
482
+ * output is NEVER adopted into the transcript (see closeOpenItems being
483
+ * skipped on the cancelled path).
484
+ */
485
+ private cancelResponse;
486
+ private pushItem;
487
+ private itemToProtocol;
488
+ private send;
489
+ private fail;
490
+ /**
491
+ * Socket-level teardown ONLY (never session death): the native uRun session
492
+ * behind this conversation stays alive — resume semantics are Main-owned.
493
+ */
494
+ private teardown;
495
+ }
496
+
497
+ /**
498
+ * THE REALTIME BINDING COMPOSITION — the ONE seam every composition rides to
499
+ * turn an upgraded `/v1/realtime` request into a {@link RealtimeBinding}.
500
+ *
501
+ * The modality is read from the CATALOG (`task` stt | tts, invariant across
502
+ * placements) through the SAME router-backed gate seam the Images lane uses
503
+ * (`ProxyClients.supportsAudio` — `ModelRouter.supportsAudio`). A non-audio
504
+ * model is refused LOUDLY with a 404 before anything is opened; a voice
505
+ * binding acquires its PRIVATE audio bridge from `ProxyClients.openAudio`
506
+ * (the pooled session's native `audio` lane at 24 kHz — the ONE canonical
507
+ * session-audio wiring, `transport/media.ts` AUDIO_STREAM). There is no env-var
508
+ * switch and no chat-default fall-through: an absent model resolves through
509
+ * the router exactly like every other lane, and a chat default is refused
510
+ * exactly like an explicitly named chat model.
511
+ *
512
+ * Used by BOTH compositions — the local `urun compat proxy` (fixed clients,
513
+ * optional fixed apiKey) and the hosted multi-tenant endpoint (per-request
514
+ * org resolution + per-conversation private backhauls) — so there is exactly
515
+ * ONE binding implementation and no copy-pasted second handler.
516
+ */
517
+
518
+ /**
519
+ * The realtime lane's model name — the GA `?model=` query parameter
520
+ * (`wss://…/v1/realtime?model=…`). Absent = the router's configured default
521
+ * resolves it (locally the startup app; hosted, a loud 404 — there IS no
522
+ * default app on the hosted lane).
523
+ */
524
+ declare function realtimeModelOf(req: IncomingMessage): string | undefined;
525
+ /**
526
+ * Build the binding for `model` against `clients`:
527
+ * - NO `supportsAudio` seam ⇒ 501 (loud, never a silent allow-all).
528
+ * - {@link UnknownModelError} ⇒ 404 with the router's own loud message.
529
+ * - A non-audio verdict ⇒ 404 — the realtime lane never falls through to
530
+ * the chat lane, in either direction.
531
+ * - A voice verdict with no `openAudio` seam ⇒ 501 (a voice binding cannot
532
+ * be served without its bridge; a voiceless downgrade is never silent).
533
+ */
534
+ declare function realtimeBindingFor(clients: Pick<ProxyClients, 'createResponse'> & Partial<Pick<ProxyClients, 'supportsAudio' | 'openAudio'>>, model: string | undefined): Promise<RealtimeBinding<ProxyClients>>;
535
+ /**
536
+ * The LOCAL lane resolver: fixed pooled clients, optional fixed apiKey (the
537
+ * same shared-secret rule the local Gemini Live lane applies — a mismatched
538
+ * or missing Bearer is a 401; with no configured key the header is not
539
+ * consulted). The HOSTED lane builds its own resolver per request (org
540
+ * resolution + per-conversation private backhaul) and reuses
541
+ * {@link realtimeBindingFor} for the binding itself.
542
+ */
543
+ declare function fixedClientsResolver(clients: ProxyClients, apiKey?: string): {
544
+ resolve(req: IncomingMessage): Promise<RealtimeBinding<ProxyClients>>;
545
+ };
546
+
547
+ export { type GeminiLiveCall, type GeminiLiveClients, OPENAI_REALTIME_PATH, type OpenAIProxyOptions, OpenAIRealtimeConnection, type OpenAIRealtimeOptions, type OpenAIRealtimeResolver, ProxyClients, type RealtimeBinding, ResumptionRegistry, attachGeminiLive, attachOpenAIRealtime, createOpenAIProxy, fixedClientsResolver, realtimeBindingFor, realtimeModelOf, serveGeminiLiveSocket };