@urun-sh/openai 0.5.3 → 0.5.5
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.
- package/dist/chunk-5BZCM3RS.js +4 -0
- package/dist/chunk-DMD5UENK.js +1 -0
- package/dist/{chunk-G3NMGP4N.js → chunk-GBBY3PCZ.js} +1 -1
- package/dist/chunk-I2Q3B3OG.js +6 -0
- package/dist/chunk-K23AZPI4.js +1 -0
- package/dist/chunk-OI2OY32M.js +1 -0
- package/dist/chunk-QVF7NF7G.js +44 -0
- package/dist/gemini-live.cjs +2 -2
- package/dist/gemini-live.d.cts +6 -0
- package/dist/gemini-live.d.ts +6 -0
- package/dist/gemini-live.js +1 -1
- package/dist/hosted/bin.cjs +36 -22
- package/dist/hosted/bin.js +4 -4
- package/dist/hosted/index.cjs +32 -18
- package/dist/hosted/index.d.cts +140 -198
- package/dist/hosted/index.d.ts +63 -82
- package/dist/hosted/index.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +2 -18
- package/dist/index.d.ts +2 -18
- package/dist/index.js +1 -1
- package/dist/models-NYMZrklp.d.cts +38 -0
- package/dist/models-NYMZrklp.d.ts +38 -0
- package/dist/pi-extension/index.cjs +6 -6
- package/dist/pi-extension/index.js +1 -1
- package/dist/pi-extension/standalone.cjs +49 -49
- package/dist/proxy/cli.cjs +41 -37
- package/dist/proxy/cli.js +1 -1
- package/dist/proxy/index.cjs +30 -21
- package/dist/proxy/index.d.cts +468 -2
- package/dist/proxy/index.d.ts +173 -2
- package/dist/proxy/index.js +6 -1
- package/dist/responses-turn-KOAoIqZ-.d.ts +200 -0
- package/dist/responses-turn-OrO4euEN.d.cts +513 -0
- package/package.json +3 -3
- package/dist/chunk-F2TEK34X.js +0 -1
- package/dist/chunk-FVD4NMXJ.js +0 -1
- package/dist/chunk-M6ICU4F5.js +0 -40
- package/dist/chunk-ODHCPZHI.js +0 -6
- package/dist/chunk-UVAY7Q7Z.js +0 -1
- package/dist/chunk-XHIIEA6Z.js +0 -1
- package/dist/server-BfME37pQ.d.cts +0 -200
- package/dist/server-l1himPxc.d.ts +0 -82
package/dist/proxy/index.d.cts
CHANGED
|
@@ -1,5 +1,471 @@
|
|
|
1
|
-
|
|
1
|
+
import { Server, IncomingMessage } from 'node:http';
|
|
2
|
+
import { a as ProxyHandlerOptions, P as ProxyClients } from '../responses-turn-OrO4euEN.cjs';
|
|
3
|
+
export { b as ProxyIdentity, c as ProxyVideoOutLane, S as SessionGoneError, U as UnknownModelError } from '../responses-turn-OrO4euEN.cjs';
|
|
2
4
|
export { h as GEMINI_LIVE_PATH, V as VIDEO_OUT_SETUP_KEY } from '../translator-CcDBEfvm.cjs';
|
|
3
|
-
import '
|
|
5
|
+
import { WebSocket } from 'ws';
|
|
6
|
+
import '../models-NYMZrklp.cjs';
|
|
4
7
|
import '../video-out-D20UuJ8G.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
|
+
interface OpenAIRealtimeOptions<S = unknown> {
|
|
334
|
+
resolver: OpenAIRealtimeResolver<S>;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Attach the OpenAI Realtime WS endpoint to `server`. Upgrades on any other
|
|
338
|
+
* path pass through untouched. Returns an async close function.
|
|
339
|
+
*/
|
|
340
|
+
declare function attachOpenAIRealtime<S = unknown>(server: Server, options: OpenAIRealtimeOptions<S>): () => Promise<void>;
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* One authenticated OpenAI Realtime WS connection — the adapter's core.
|
|
344
|
+
*
|
|
345
|
+
* OWNERSHIP MODEL (Main's contract, 2026-09-08): the binding seam hands this
|
|
346
|
+
* connection an EXCLUSIVE, connection-private native uRun session (and, when
|
|
347
|
+
* the resolver authoritatively binds voice, a PRIVATE audio bridge). The
|
|
348
|
+
* pooled tenant `openAudio` lane is NEVER used here — `binding.audioSession`
|
|
349
|
+
* comes from the resolver's own per-conversation acquisition, and
|
|
350
|
+
* {@link claimVoiceBinding} is the INVARIANT VALIDATION that an accidental
|
|
351
|
+
* cross-caller reuse of one bridge fails LOUD (it is not a capacity policy).
|
|
352
|
+
* Multi-attach on the SAME authorized session (explicit resume) is the one
|
|
353
|
+
* sanctioned shared form: one bridge owner at a time, validated by claim.
|
|
354
|
+
*
|
|
355
|
+
* NO-OP POLICY: every unsupported client event or backend gap renders as a
|
|
356
|
+
* LOUD `error` server event (or a close) — never a silently ignored frame.
|
|
357
|
+
*/
|
|
358
|
+
|
|
359
|
+
declare class OpenAIRealtimeConnection<S = unknown> {
|
|
360
|
+
readonly id: string;
|
|
361
|
+
private readonly ws;
|
|
362
|
+
private readonly binding;
|
|
363
|
+
private readonly onClosed?;
|
|
364
|
+
private config;
|
|
365
|
+
private readonly sessionId;
|
|
366
|
+
private readonly conversationId;
|
|
367
|
+
private readonly items;
|
|
368
|
+
/** Uncommitted input audio (base64 PCM16 chunks) held locally until commit. */
|
|
369
|
+
private pendingAudioBytes;
|
|
370
|
+
private pendingAudioTotalBytes;
|
|
371
|
+
/** Committed audio of the frozen turn, dispatched at response.create. */
|
|
372
|
+
private frozenAudio;
|
|
373
|
+
private inFlight;
|
|
374
|
+
private activeResponseId;
|
|
375
|
+
/** Output events already flowed for the active response (post-flush faults → error event, pre-flush → response.failed). */
|
|
376
|
+
private responseFlushed;
|
|
377
|
+
private cancelled;
|
|
378
|
+
private voiceClaimed;
|
|
379
|
+
private audioLaneOwner;
|
|
380
|
+
private audioUnsubscribe;
|
|
381
|
+
private outOfTurnAudioWarned;
|
|
382
|
+
private audioOnlyTranscriptGapWarned;
|
|
383
|
+
private closed;
|
|
384
|
+
/** The backend's turn/transcript lane consumer (binding.turns), if wired. */
|
|
385
|
+
private turnLaneAbort;
|
|
386
|
+
/** Accumulated transcript of the OPEN user turn (delta/final events). */
|
|
387
|
+
private turnTranscript;
|
|
388
|
+
/** Transcript of the LAST committed turn (the model-visible commit). */
|
|
389
|
+
private lastCommittedTranscript;
|
|
390
|
+
constructor(opts: {
|
|
391
|
+
ws: WebSocket;
|
|
392
|
+
binding: RealtimeBinding<S>;
|
|
393
|
+
onClosed?: (conn: OpenAIRealtimeConnection<S>) => void;
|
|
394
|
+
});
|
|
395
|
+
/**
|
|
396
|
+
* Consume the backend's OWN turn/transcript lane (§5 stt stream): the
|
|
397
|
+
* semantic-VAD boundary events drive REAL protocol semantics — turn_start
|
|
398
|
+
* is the native barge-in, turn_end is the native commit, deltas/finals are
|
|
399
|
+
* the REAL input transcription. This is the server-VAD primitive, not an
|
|
400
|
+
* emulation gap: the voice engine itself decides turn boundaries.
|
|
401
|
+
*/
|
|
402
|
+
private startTurnLane;
|
|
403
|
+
/** One backend turn/transcript event → REAL GA protocol events. */
|
|
404
|
+
private onTurnEvent;
|
|
405
|
+
/** The committed turn's transcript, consumed once (null = none). */
|
|
406
|
+
private takeTurnTranscript;
|
|
407
|
+
private onMessage;
|
|
408
|
+
private handle;
|
|
409
|
+
/** Refuse loudly when a text-only-resolved session is asked to carry audio. */
|
|
410
|
+
private ensureVoice;
|
|
411
|
+
/**
|
|
412
|
+
* Cross-caller reuse guard — the INVARIANT check, not capacity policy. The
|
|
413
|
+
* resolver must hand every independent conversation its OWN bridge; if two
|
|
414
|
+
* connections ever hold one, the second fails LOUD instead of mixing audio.
|
|
415
|
+
*/
|
|
416
|
+
private claimVoiceBinding;
|
|
417
|
+
private releaseVoiceBinding;
|
|
418
|
+
private warnAudioOnlyTranscriptGap;
|
|
419
|
+
/**
|
|
420
|
+
* The upstream turn seam, normalized: a binding carries EITHER
|
|
421
|
+
* `session.sendResponseCreate(params, input, requestId)` (direct
|
|
422
|
+
* SdkTransport-style) OR `clients.createResponse(params)` (the canonical
|
|
423
|
+
* ProxyClients seam the shared conversation factory hands out). A binding
|
|
424
|
+
* with neither was already rejected at upgrade time; this is the loud
|
|
425
|
+
* backstop.
|
|
426
|
+
*/
|
|
427
|
+
private upstreamStream;
|
|
428
|
+
private startResponse;
|
|
429
|
+
/** Bridge voice OUT frame → `response.output_audio.delta` on the open turn. */
|
|
430
|
+
private onOutputAudioFrame;
|
|
431
|
+
/** One assistant message item per turn (text and audio parts share it). */
|
|
432
|
+
private messageItemAdded;
|
|
433
|
+
/** true when an audio delta opened the message item this turn. */
|
|
434
|
+
private messageItemOpenForAudio;
|
|
435
|
+
private beginMessageItem;
|
|
436
|
+
private beginAudioMessageItem;
|
|
437
|
+
/**
|
|
438
|
+
* Close every item the turn opened, in first-open order, adopt them into
|
|
439
|
+
* the transcript, and emit their `done` events.
|
|
440
|
+
*/
|
|
441
|
+
private closeOpenItems;
|
|
442
|
+
private responseObject;
|
|
443
|
+
private failedResponse;
|
|
444
|
+
/**
|
|
445
|
+
* `response.done` — REAL usage only: the serve runtime emits
|
|
446
|
+
* `{input_tokens, output_tokens, total_tokens}` on terminal bodies; when it
|
|
447
|
+
* sent none, `usage: null` is delivered (never zeros dressed as counts).
|
|
448
|
+
*/
|
|
449
|
+
private doneResponse;
|
|
450
|
+
private upstreamErrorBody;
|
|
451
|
+
private emitUpstreamFault;
|
|
452
|
+
/**
|
|
453
|
+
* Cancel the in-flight response. The upstream iterator is stopped through
|
|
454
|
+
* the for-await `break` protocol (its `return()` runs — no bespoke cancel
|
|
455
|
+
* machinery); the turn stops at the NEXT upstream event and the partial
|
|
456
|
+
* output is NEVER adopted into the transcript (see closeOpenItems being
|
|
457
|
+
* skipped on the cancelled path).
|
|
458
|
+
*/
|
|
459
|
+
private cancelResponse;
|
|
460
|
+
private pushItem;
|
|
461
|
+
private itemToProtocol;
|
|
462
|
+
private send;
|
|
463
|
+
private fail;
|
|
464
|
+
/**
|
|
465
|
+
* Socket-level teardown ONLY (never session death): the native uRun session
|
|
466
|
+
* behind this conversation stays alive — resume semantics are Main-owned.
|
|
467
|
+
*/
|
|
468
|
+
private teardown;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export { type GeminiLiveCall, type GeminiLiveClients, OPENAI_REALTIME_PATH, type OpenAIProxyOptions, OpenAIRealtimeConnection, type OpenAIRealtimeOptions, type OpenAIRealtimeResolver, ProxyClients, type RealtimeBinding, ResumptionRegistry, attachGeminiLive, attachOpenAIRealtime, createOpenAIProxy, serveGeminiLiveSocket };
|
package/dist/proxy/index.d.ts
CHANGED
|
@@ -1,5 +1,176 @@
|
|
|
1
|
-
|
|
1
|
+
import { Server, IncomingMessage } from 'node:http';
|
|
2
|
+
import { a as ProxyHandlerOptions, P as ProxyClients } from '../responses-turn-KOAoIqZ-.js';
|
|
3
|
+
export { b as ProxyIdentity, c as ProxyVideoOutLane, S as SessionGoneError, U as UnknownModelError } from '../responses-turn-KOAoIqZ-.js';
|
|
2
4
|
export { h as GEMINI_LIVE_PATH, V as VIDEO_OUT_SETUP_KEY } from '../translator-C9uPKypK.js';
|
|
3
|
-
import '
|
|
5
|
+
import { WebSocket } from 'ws';
|
|
6
|
+
import '../models-NYMZrklp.js';
|
|
4
7
|
import '../video-out-CWesbk12.js';
|
|
5
8
|
import '../types-lsVTbNcH.js';
|
|
9
|
+
|
|
10
|
+
interface OpenAIProxyOptions extends ProxyHandlerOptions {
|
|
11
|
+
clients: ProxyClients;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
declare function createOpenAIProxy(options: OpenAIProxyOptions): Server;
|
|
15
|
+
|
|
16
|
+
type GeminiLiveClients = Pick<ProxyClients, 'createResponse' | 'openAudio' | 'openVideo' | 'openVideoOut' | 'sessionHandle' | 'onSessionEnd'>;
|
|
17
|
+
interface LiveSnapshot {
|
|
18
|
+
model: string;
|
|
19
|
+
|
|
20
|
+
proxyHandle: string;
|
|
21
|
+
|
|
22
|
+
transcript: Array<Record<string, unknown>>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
declare class ResumptionRegistry {
|
|
26
|
+
private readonly snapshots;
|
|
27
|
+
store(scope: string, handle: string, snapshot: LiveSnapshot): void;
|
|
28
|
+
get(scope: string, handle: string): LiveSnapshot | undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface GeminiLiveCall {
|
|
32
|
+
clients: GeminiLiveClients;
|
|
33
|
+
|
|
34
|
+
scope: string;
|
|
35
|
+
|
|
36
|
+
onBindResumeHandle?: (handle: string) => void;
|
|
37
|
+
|
|
38
|
+
clientsForResume?: (resumeHandle: string) => Promise<GeminiLiveClients>;
|
|
39
|
+
|
|
40
|
+
release?: () => Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type GeminiLiveAuthenticate = (credential: string, req: IncomingMessage) => Promise<GeminiLiveCall>;
|
|
44
|
+
interface GeminiLiveOptions {
|
|
45
|
+
|
|
46
|
+
clients?: GeminiLiveClients;
|
|
47
|
+
|
|
48
|
+
apiKey?: string;
|
|
49
|
+
|
|
50
|
+
authenticate?: GeminiLiveAuthenticate;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
declare function attachGeminiLive(server: Server, options: GeminiLiveOptions): () => Promise<void>;
|
|
54
|
+
|
|
55
|
+
declare function serveGeminiLiveSocket(ws: WebSocket, call: GeminiLiveCall, registry?: ResumptionRegistry): void;
|
|
56
|
+
|
|
57
|
+
declare const OPENAI_REALTIME_PATH = "/v1/realtime";
|
|
58
|
+
|
|
59
|
+
type OpenAIRealtimeResolver<S = unknown> = ((req: IncomingMessage) => Promise<RealtimeBinding<S>>) | {
|
|
60
|
+
resolve(req: IncomingMessage): Promise<RealtimeBinding<S>>;
|
|
61
|
+
};
|
|
62
|
+
interface RealtimeBinding<S = unknown> {
|
|
63
|
+
|
|
64
|
+
session?: S & {
|
|
65
|
+
sendResponseCreate?(params: unknown, input: unknown[], requestId: string): AsyncIterable<unknown>;
|
|
66
|
+
};
|
|
67
|
+
clients?: Pick<ProxyClients, 'createResponse'>;
|
|
68
|
+
|
|
69
|
+
voice: boolean;
|
|
70
|
+
|
|
71
|
+
audioSession?: {
|
|
72
|
+
audio: {
|
|
73
|
+
appendInputAudio(base64Pcm16: string): void;
|
|
74
|
+
onOutputAudio(handler: (b64: string) => void): () => void;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
openaiRealtimeClaims?: Set<RealtimeBinding['audioSession']>;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
model?: string;
|
|
81
|
+
|
|
82
|
+
resumed?: boolean;
|
|
83
|
+
|
|
84
|
+
turns?: () => AsyncIterable<{
|
|
85
|
+
kind: 'delta' | 'final' | 'turn_start' | 'turn_end' | 'turn_eager_end' | 'turn_resumed';
|
|
86
|
+
text?: string;
|
|
87
|
+
confidence?: number;
|
|
88
|
+
startMs?: number;
|
|
89
|
+
}>;
|
|
90
|
+
}
|
|
91
|
+
interface OpenAIRealtimeOptions<S = unknown> {
|
|
92
|
+
resolver: OpenAIRealtimeResolver<S>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
declare function attachOpenAIRealtime<S = unknown>(server: Server, options: OpenAIRealtimeOptions<S>): () => Promise<void>;
|
|
96
|
+
|
|
97
|
+
declare class OpenAIRealtimeConnection<S = unknown> {
|
|
98
|
+
readonly id: string;
|
|
99
|
+
private readonly ws;
|
|
100
|
+
private readonly binding;
|
|
101
|
+
private readonly onClosed?;
|
|
102
|
+
private config;
|
|
103
|
+
private readonly sessionId;
|
|
104
|
+
private readonly conversationId;
|
|
105
|
+
private readonly items;
|
|
106
|
+
|
|
107
|
+
private pendingAudioBytes;
|
|
108
|
+
private pendingAudioTotalBytes;
|
|
109
|
+
|
|
110
|
+
private frozenAudio;
|
|
111
|
+
private inFlight;
|
|
112
|
+
private activeResponseId;
|
|
113
|
+
|
|
114
|
+
private responseFlushed;
|
|
115
|
+
private cancelled;
|
|
116
|
+
private voiceClaimed;
|
|
117
|
+
private audioLaneOwner;
|
|
118
|
+
private audioUnsubscribe;
|
|
119
|
+
private outOfTurnAudioWarned;
|
|
120
|
+
private audioOnlyTranscriptGapWarned;
|
|
121
|
+
private closed;
|
|
122
|
+
|
|
123
|
+
private turnLaneAbort;
|
|
124
|
+
|
|
125
|
+
private turnTranscript;
|
|
126
|
+
|
|
127
|
+
private lastCommittedTranscript;
|
|
128
|
+
constructor(opts: {
|
|
129
|
+
ws: WebSocket;
|
|
130
|
+
binding: RealtimeBinding<S>;
|
|
131
|
+
onClosed?: (conn: OpenAIRealtimeConnection<S>) => void;
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
private startTurnLane;
|
|
135
|
+
|
|
136
|
+
private onTurnEvent;
|
|
137
|
+
|
|
138
|
+
private takeTurnTranscript;
|
|
139
|
+
private onMessage;
|
|
140
|
+
private handle;
|
|
141
|
+
|
|
142
|
+
private ensureVoice;
|
|
143
|
+
|
|
144
|
+
private claimVoiceBinding;
|
|
145
|
+
private releaseVoiceBinding;
|
|
146
|
+
private warnAudioOnlyTranscriptGap;
|
|
147
|
+
|
|
148
|
+
private upstreamStream;
|
|
149
|
+
private startResponse;
|
|
150
|
+
|
|
151
|
+
private onOutputAudioFrame;
|
|
152
|
+
|
|
153
|
+
private messageItemAdded;
|
|
154
|
+
|
|
155
|
+
private messageItemOpenForAudio;
|
|
156
|
+
private beginMessageItem;
|
|
157
|
+
private beginAudioMessageItem;
|
|
158
|
+
|
|
159
|
+
private closeOpenItems;
|
|
160
|
+
private responseObject;
|
|
161
|
+
private failedResponse;
|
|
162
|
+
|
|
163
|
+
private doneResponse;
|
|
164
|
+
private upstreamErrorBody;
|
|
165
|
+
private emitUpstreamFault;
|
|
166
|
+
|
|
167
|
+
private cancelResponse;
|
|
168
|
+
private pushItem;
|
|
169
|
+
private itemToProtocol;
|
|
170
|
+
private send;
|
|
171
|
+
private fail;
|
|
172
|
+
|
|
173
|
+
private teardown;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export { type GeminiLiveCall, type GeminiLiveClients, OPENAI_REALTIME_PATH, type OpenAIProxyOptions, OpenAIRealtimeConnection, type OpenAIRealtimeOptions, type OpenAIRealtimeResolver, ProxyClients, type RealtimeBinding, ResumptionRegistry, attachGeminiLive, attachOpenAIRealtime, createOpenAIProxy, serveGeminiLiveSocket };
|