@pipedog/openai 0.0.0 → 0.3.0
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/LICENSE +25 -0
- package/NOTICE +9 -0
- package/dist/_constants.d.ts +4 -0
- package/dist/_constants.js +9 -0
- package/dist/base_llm.d.ts +272 -0
- package/dist/base_llm.js +619 -0
- package/dist/image.d.ts +116 -0
- package/dist/image.js +179 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +12 -0
- package/dist/live/events.d.ts +768 -0
- package/dist/live/events.js +1020 -0
- package/dist/live/index.d.ts +2 -0
- package/dist/live/index.js +8 -0
- package/dist/live/llm.d.ts +566 -0
- package/dist/live/llm.js +1652 -0
- package/dist/llm.d.ts +30 -0
- package/dist/llm.js +50 -0
- package/dist/open_ai_live_adapter.d.ts +94 -0
- package/dist/open_ai_live_adapter.js +192 -0
- package/dist/open_ai_realtime_adapter.d.ts +95 -0
- package/dist/open_ai_realtime_adapter.js +221 -0
- package/dist/open_ai_responses_adapter.d.ts +95 -0
- package/dist/open_ai_responses_adapter.js +358 -0
- package/dist/realtime/events.d.ts +1401 -0
- package/dist/realtime/events.js +1639 -0
- package/dist/realtime/index.d.ts +2 -0
- package/dist/realtime/index.js +8 -0
- package/dist/realtime/llm.d.ts +457 -0
- package/dist/realtime/llm.js +1550 -0
- package/dist/responses/index.d.ts +2 -0
- package/dist/responses/index.js +8 -0
- package/dist/responses/llm.d.ts +448 -0
- package/dist/responses/llm.js +1517 -0
- package/dist/stt.d.ts +409 -0
- package/dist/stt.js +741 -0
- package/dist/tts.d.ts +86 -0
- package/dist/tts.js +238 -0
- package/package.json +49 -3
- package/README.md +0 -5
|
@@ -0,0 +1,1020 @@
|
|
|
1
|
+
// Copyright (c) 2024-2026, Daily
|
|
2
|
+
// Copyright (c) 2026, pipedog contributors
|
|
3
|
+
// SPDX-License-Identifier: BSD-2-Clause
|
|
4
|
+
//
|
|
5
|
+
// Ported from pipecat: src/pipecat/services/openai/live/events.py
|
|
6
|
+
/**
|
|
7
|
+
* Event and session models for the OpenAI Live API (WebSocket transport).
|
|
8
|
+
*
|
|
9
|
+
* The Live API is a full-duplex speech-to-speech API: the model listens and
|
|
10
|
+
* speaks at the same time and decides on its own when to talk. Clients start a
|
|
11
|
+
* session, stream audio in, receive audio and transcript events out, and answer
|
|
12
|
+
* the model's *delegations* (units of work it hands to a backend model).
|
|
13
|
+
*
|
|
14
|
+
* Server events are parsed leniently: unrecognised fields are kept, and event
|
|
15
|
+
* types this module doesn't model become `UnknownServerEvent` rather than
|
|
16
|
+
* errors, so an addition to the API doesn't break the receive loop.
|
|
17
|
+
*
|
|
18
|
+
* Porting notes:
|
|
19
|
+
* - Upstream models are pydantic `BaseModel`s. Here each model extends the
|
|
20
|
+
* small `LiveModel` below: fields are camelCase and the constructor takes
|
|
21
|
+
* one options object (validated like pydantic, throwing
|
|
22
|
+
* `LiveModelValidationError`), while `modelValidate()` reads and
|
|
23
|
+
* `modelDump()` / `toPayload()` write the upstream snake_case wire keys, so
|
|
24
|
+
* the wire format is unchanged. Free-form `dict` fields are kept as-is.
|
|
25
|
+
* - Models with `extra="allow"` keep unknown wire keys in `modelExtra`.
|
|
26
|
+
*/
|
|
27
|
+
import { pyJsonLoads } from "@pipedog/core/utils/py_json";
|
|
28
|
+
/** Maximum number of startup `input` messages a session accepts. */
|
|
29
|
+
export const MAX_INPUT_ITEMS = 128;
|
|
30
|
+
/** Raised when data does not match a model, like pydantic's `ValidationError`. */
|
|
31
|
+
export class LiveModelValidationError extends Error {
|
|
32
|
+
name = "ValidationError";
|
|
33
|
+
title;
|
|
34
|
+
details;
|
|
35
|
+
constructor(title, details) {
|
|
36
|
+
const lines = details.map((d) => `${d.loc.join(".")}\n ${d.msg}`);
|
|
37
|
+
super(`${details.length} validation error${details.length === 1 ? "" : "s"} for ${title}\n${lines.join("\n")}`);
|
|
38
|
+
this.title = title;
|
|
39
|
+
this.details = details;
|
|
40
|
+
}
|
|
41
|
+
/** The list of problems (pydantic `ValidationError.errors()`). */
|
|
42
|
+
errors() {
|
|
43
|
+
return this.details;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
class FieldError extends Error {
|
|
47
|
+
detail;
|
|
48
|
+
constructor(detail) {
|
|
49
|
+
super(detail.map((d) => d.msg).join("; "));
|
|
50
|
+
this.detail = detail;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function fail(msg) {
|
|
54
|
+
throw new FieldError([{ loc: [], msg }]);
|
|
55
|
+
}
|
|
56
|
+
function field(parse, defaultValue) {
|
|
57
|
+
const spec = { parse };
|
|
58
|
+
if (defaultValue)
|
|
59
|
+
spec.default = defaultValue;
|
|
60
|
+
return spec;
|
|
61
|
+
}
|
|
62
|
+
function wireKey(name) {
|
|
63
|
+
return name.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
|
|
64
|
+
}
|
|
65
|
+
function isPlainObject(value) {
|
|
66
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
67
|
+
}
|
|
68
|
+
const P = {
|
|
69
|
+
any: () => (v) => v,
|
|
70
|
+
str: () => (v) => typeof v === "string" ? v : fail("Input should be a valid string"),
|
|
71
|
+
int: () => (v) => typeof v === "number" && Number.isInteger(v) ? v : fail("Input should be a valid integer"),
|
|
72
|
+
float: () => (v) => typeof v === "number" ? v : fail("Input should be a valid number"),
|
|
73
|
+
bool: () => (v) => typeof v === "boolean" ? v : fail("Input should be a valid boolean"),
|
|
74
|
+
dict: () => (v) => isPlainObject(v) ? v : fail("Input should be a valid dictionary"),
|
|
75
|
+
literal: (...values) => (v) => values.includes(v)
|
|
76
|
+
? v
|
|
77
|
+
: fail(`Input should be ${values.map((x) => `'${x}'`).join(" or ")}`),
|
|
78
|
+
optional: (parse) => (v, wire) => v === null || v === undefined ? null : parse(v, wire),
|
|
79
|
+
list: (parse) => (v, wire) => {
|
|
80
|
+
if (!Array.isArray(v))
|
|
81
|
+
fail("Input should be a valid list");
|
|
82
|
+
const details = [];
|
|
83
|
+
const out = v.map((item, i) => {
|
|
84
|
+
try {
|
|
85
|
+
return parse(item, wire);
|
|
86
|
+
}
|
|
87
|
+
catch (e) {
|
|
88
|
+
if (!(e instanceof FieldError))
|
|
89
|
+
throw e;
|
|
90
|
+
for (const d of e.detail)
|
|
91
|
+
details.push({ loc: [i, ...d.loc], msg: d.msg });
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
if (details.length > 0)
|
|
96
|
+
throw new FieldError(details);
|
|
97
|
+
return out;
|
|
98
|
+
},
|
|
99
|
+
union: (...parsers) => (v, wire) => {
|
|
100
|
+
const details = [];
|
|
101
|
+
for (const parse of parsers) {
|
|
102
|
+
try {
|
|
103
|
+
return parse(v, wire);
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
if (!(e instanceof FieldError))
|
|
107
|
+
throw e;
|
|
108
|
+
details.push(...e.detail);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
throw new FieldError(details);
|
|
112
|
+
},
|
|
113
|
+
model: (cls) => (v, wire) => {
|
|
114
|
+
const c = cls();
|
|
115
|
+
if (v instanceof c)
|
|
116
|
+
return v;
|
|
117
|
+
if (!isPlainObject(v))
|
|
118
|
+
fail(`Input should be a valid dictionary or instance of ${c.name}`);
|
|
119
|
+
try {
|
|
120
|
+
const ctor = c;
|
|
121
|
+
return (wire ? c.modelValidate(v) : new ctor(v));
|
|
122
|
+
}
|
|
123
|
+
catch (e) {
|
|
124
|
+
if (e instanceof LiveModelValidationError)
|
|
125
|
+
throw new FieldError(e.details);
|
|
126
|
+
throw e;
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
const EXTRA = Symbol("pipecat.openai.live.extra");
|
|
131
|
+
const WIRE = Symbol("pipecat.openai.live.wire");
|
|
132
|
+
function dumpValue(value, options) {
|
|
133
|
+
if (value instanceof LiveModel)
|
|
134
|
+
return value.modelDump(options);
|
|
135
|
+
if (Array.isArray(value))
|
|
136
|
+
return value.map((v) => dumpValue(v, options));
|
|
137
|
+
return value;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Base class of the Live API models (pydantic `BaseModel`).
|
|
141
|
+
*
|
|
142
|
+
* Subclasses declare their fields with `declare` and describe them in the
|
|
143
|
+
* static `fields` table (parser and default). The constructor validates
|
|
144
|
+
* camelCase options; `modelValidate()` validates wire (snake_case) data;
|
|
145
|
+
* `modelDump()` produces wire data.
|
|
146
|
+
*/
|
|
147
|
+
export class LiveModel {
|
|
148
|
+
/** Field table: name, parser and default. */
|
|
149
|
+
static fields = {};
|
|
150
|
+
/** What to do with unknown wire keys: drop them, or keep them in `modelExtra`. */
|
|
151
|
+
static extra = "ignore";
|
|
152
|
+
/** Unknown keys kept by models created with `extra = "allow"` (pydantic `model_extra`). */
|
|
153
|
+
modelExtra = null;
|
|
154
|
+
constructor(options = {}) {
|
|
155
|
+
const cls = new.target;
|
|
156
|
+
const input = options;
|
|
157
|
+
const details = [];
|
|
158
|
+
const self = this;
|
|
159
|
+
const wire = input[WIRE] === true;
|
|
160
|
+
for (const [name, spec] of Object.entries(cls.fields)) {
|
|
161
|
+
const raw = input[name];
|
|
162
|
+
const key = wireKey(name);
|
|
163
|
+
if (raw === undefined) {
|
|
164
|
+
if (spec.default) {
|
|
165
|
+
self[name] = spec.default();
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
details.push({ loc: [key], msg: "Field required" });
|
|
169
|
+
}
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
self[name] = spec.parse(raw, wire);
|
|
174
|
+
}
|
|
175
|
+
catch (e) {
|
|
176
|
+
if (!(e instanceof FieldError))
|
|
177
|
+
throw e;
|
|
178
|
+
for (const d of e.detail)
|
|
179
|
+
details.push({ loc: [key, ...d.loc], msg: d.msg });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (details.length > 0) {
|
|
183
|
+
throw new LiveModelValidationError(cls.name, details);
|
|
184
|
+
}
|
|
185
|
+
if (cls.extra === "allow") {
|
|
186
|
+
const extra = { ...(input[EXTRA] ?? {}) };
|
|
187
|
+
for (const [k, v] of Object.entries(options)) {
|
|
188
|
+
if (!(k in cls.fields))
|
|
189
|
+
extra[k] = v;
|
|
190
|
+
}
|
|
191
|
+
this.modelExtra = extra;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Validate wire data (snake_case keys) into a model (pydantic `model_validate`).
|
|
196
|
+
*
|
|
197
|
+
* @throws LiveModelValidationError If the data does not match the model.
|
|
198
|
+
*/
|
|
199
|
+
static modelValidate(data) {
|
|
200
|
+
// biome-ignore lint/complexity/noThisInStatic: polymorphic static.
|
|
201
|
+
const cls = this;
|
|
202
|
+
// biome-ignore lint/complexity/noThisInStatic: polymorphic static.
|
|
203
|
+
const ctor = this;
|
|
204
|
+
if (data instanceof cls)
|
|
205
|
+
return data;
|
|
206
|
+
if (!isPlainObject(data)) {
|
|
207
|
+
throw new LiveModelValidationError(cls.name, [
|
|
208
|
+
{ loc: [], msg: `Input should be a valid dictionary or instance of ${cls.name}` },
|
|
209
|
+
]);
|
|
210
|
+
}
|
|
211
|
+
const options = {};
|
|
212
|
+
const known = new Set();
|
|
213
|
+
for (const name of Object.keys(cls.fields)) {
|
|
214
|
+
const key = wireKey(name);
|
|
215
|
+
known.add(key);
|
|
216
|
+
if (key in data)
|
|
217
|
+
options[name] = data[key];
|
|
218
|
+
}
|
|
219
|
+
if (cls.extra === "allow") {
|
|
220
|
+
const extra = {};
|
|
221
|
+
for (const [k, v] of Object.entries(data)) {
|
|
222
|
+
if (!known.has(k))
|
|
223
|
+
extra[k] = v;
|
|
224
|
+
}
|
|
225
|
+
options[EXTRA] = extra;
|
|
226
|
+
}
|
|
227
|
+
options[WIRE] = true;
|
|
228
|
+
return new ctor(options);
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* The wire form of the model, with upstream's snake_case keys (pydantic
|
|
232
|
+
* `model_dump`). Nested models are dumped too.
|
|
233
|
+
*/
|
|
234
|
+
modelDump(options = {}) {
|
|
235
|
+
const cls = this.constructor;
|
|
236
|
+
const self = this;
|
|
237
|
+
const dump = {};
|
|
238
|
+
for (const name of Object.keys(cls.fields)) {
|
|
239
|
+
const value = self[name];
|
|
240
|
+
if (options.excludeNone && (value === null || value === undefined))
|
|
241
|
+
continue;
|
|
242
|
+
dump[wireKey(name)] = dumpValue(value, options);
|
|
243
|
+
}
|
|
244
|
+
if (this.modelExtra) {
|
|
245
|
+
for (const [k, v] of Object.entries(this.modelExtra)) {
|
|
246
|
+
if (options.excludeNone && (v === null || v === undefined))
|
|
247
|
+
continue;
|
|
248
|
+
dump[k] = dumpValue(v, options);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return dump;
|
|
252
|
+
}
|
|
253
|
+
/** Serialize with the wire keys, as pydantic does. */
|
|
254
|
+
toJSON() {
|
|
255
|
+
return this.modelDump();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function typeField(type) {
|
|
259
|
+
return field(P.literal(type), () => type);
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* A text content part supplied by the client.
|
|
263
|
+
*
|
|
264
|
+
* - `type`: content type, always "input_text".
|
|
265
|
+
* - `text`: the text.
|
|
266
|
+
*/
|
|
267
|
+
export class InputTextContent extends LiveModel {
|
|
268
|
+
static fields = {
|
|
269
|
+
type: typeField("input_text"),
|
|
270
|
+
text: field(P.str()),
|
|
271
|
+
};
|
|
272
|
+
constructor(options) {
|
|
273
|
+
super(options);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* A text content part attributed to the assistant.
|
|
278
|
+
*
|
|
279
|
+
* - `type`: content type, always "output_text".
|
|
280
|
+
* - `text`: the text.
|
|
281
|
+
*/
|
|
282
|
+
export class OutputTextContent extends LiveModel {
|
|
283
|
+
static fields = {
|
|
284
|
+
type: typeField("output_text"),
|
|
285
|
+
text: field(P.str()),
|
|
286
|
+
};
|
|
287
|
+
constructor(options) {
|
|
288
|
+
super(options);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* A prior text-only conversation message seeded at session start.
|
|
293
|
+
*
|
|
294
|
+
* - `type`: item type, always "message".
|
|
295
|
+
* - `role`: message role. Developer and user messages carry `input_text`
|
|
296
|
+
* content; assistant messages carry `output_text`. A `system` role is not
|
|
297
|
+
* accepted here: application instructions belong in
|
|
298
|
+
* `SessionConfig.instructions` or a developer message.
|
|
299
|
+
* - `content`: exactly one text content part.
|
|
300
|
+
*/
|
|
301
|
+
export class InputItem extends LiveModel {
|
|
302
|
+
static fields = {
|
|
303
|
+
type: typeField("message"),
|
|
304
|
+
role: field(P.literal("developer", "user", "assistant")),
|
|
305
|
+
content: field(P.list(P.union(P.model(() => InputTextContent), P.model(() => OutputTextContent)))),
|
|
306
|
+
};
|
|
307
|
+
constructor(options) {
|
|
308
|
+
super(options);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Output audio configuration.
|
|
313
|
+
*
|
|
314
|
+
* - `voice`: output voice name (defaults to `marin` server-side).
|
|
315
|
+
*/
|
|
316
|
+
export class AudioOutputConfig extends LiveModel {
|
|
317
|
+
static fields = {
|
|
318
|
+
voice: field(P.optional(P.str()), () => null),
|
|
319
|
+
};
|
|
320
|
+
constructor(options = {}) {
|
|
321
|
+
super(options);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* WebSocket audio format, selected once at session start for input and output.
|
|
326
|
+
*
|
|
327
|
+
* - `type`: `audio/pcm` (16 or 24 kHz PCM16), `audio/pcmu` or `audio/pcma`
|
|
328
|
+
* (8 kHz G.711).
|
|
329
|
+
* - `rate`: sample rate in Hz.
|
|
330
|
+
*/
|
|
331
|
+
export class AudioFormat extends LiveModel {
|
|
332
|
+
static fields = {
|
|
333
|
+
type: field(P.literal("audio/pcm", "audio/pcmu", "audio/pcma"), () => "audio/pcm"),
|
|
334
|
+
rate: field(P.int(), () => 24000),
|
|
335
|
+
};
|
|
336
|
+
constructor(options = {}) {
|
|
337
|
+
super(options);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Session audio configuration.
|
|
342
|
+
*
|
|
343
|
+
* - `output`: output audio configuration.
|
|
344
|
+
* - `format`: WebSocket audio format, shared by input and output.
|
|
345
|
+
*/
|
|
346
|
+
export class AudioConfig extends LiveModel {
|
|
347
|
+
static fields = {
|
|
348
|
+
output: field(P.optional(P.model(() => AudioOutputConfig)), () => null),
|
|
349
|
+
format: field(P.optional(P.model(() => AudioFormat)), () => null),
|
|
350
|
+
};
|
|
351
|
+
constructor(options = {}) {
|
|
352
|
+
super(options);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Delegation configuration selecting client delegation.
|
|
357
|
+
*
|
|
358
|
+
* - `type`: delegation type, always "client".
|
|
359
|
+
*/
|
|
360
|
+
export class ClientDelegationConfig extends LiveModel {
|
|
361
|
+
static fields = {
|
|
362
|
+
type: typeField("client"),
|
|
363
|
+
};
|
|
364
|
+
constructor(options = {}) {
|
|
365
|
+
super(options);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Delegation configuration selecting Responses delegation.
|
|
370
|
+
*
|
|
371
|
+
* - `type`: delegation type, always "responses".
|
|
372
|
+
* - `responses`: configuration of the backend Responses model: `model`,
|
|
373
|
+
* `instructions`, `tools`, `tool_choice`, `reasoning`,
|
|
374
|
+
* `max_output_tokens`, `service_tier`, and other Responses request fields
|
|
375
|
+
* the Live API accepts.
|
|
376
|
+
*/
|
|
377
|
+
export class ResponsesDelegationConfig extends LiveModel {
|
|
378
|
+
static fields = {
|
|
379
|
+
type: typeField("responses"),
|
|
380
|
+
responses: field(P.dict()),
|
|
381
|
+
};
|
|
382
|
+
constructor(options) {
|
|
383
|
+
super(options);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
const delegationParser = P.optional(P.union(P.model(() => ClientDelegationConfig), P.model(() => ResponsesDelegationConfig)));
|
|
387
|
+
/**
|
|
388
|
+
* Live session configuration, sent once with `session.start`.
|
|
389
|
+
*
|
|
390
|
+
* - `model`: the live model. Required, and immutable after start.
|
|
391
|
+
* - `instructions`: instructions for the live model. Immutable after start;
|
|
392
|
+
* `SessionInstructionsAppendEvent` adds more later.
|
|
393
|
+
* - `audio`: audio configuration. The output voice and format are immutable
|
|
394
|
+
* after start.
|
|
395
|
+
* - `delegation`: delegation mode. Omitted selects client delegation. The
|
|
396
|
+
* mode is immutable: switching needs a new session.
|
|
397
|
+
* - `input`: prior text-only conversation messages. Startup-only.
|
|
398
|
+
* - `store`: whether the server keeps the session for 30 days, as a
|
|
399
|
+
* downloadable recording and as a source for a fork. Startup-only; the
|
|
400
|
+
* server default is false.
|
|
401
|
+
*/
|
|
402
|
+
export class SessionConfig extends LiveModel {
|
|
403
|
+
static fields = {
|
|
404
|
+
model: field(P.str()),
|
|
405
|
+
instructions: field(P.optional(P.str()), () => null),
|
|
406
|
+
audio: field(P.optional(P.model(() => AudioConfig)), () => null),
|
|
407
|
+
delegation: field(delegationParser, () => null),
|
|
408
|
+
input: field(P.optional(P.list(P.model(() => InputItem))), () => null),
|
|
409
|
+
store: field(P.optional(P.bool()), () => null),
|
|
410
|
+
};
|
|
411
|
+
constructor(options) {
|
|
412
|
+
super(options);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* The sparse session update a running session accepts.
|
|
417
|
+
*
|
|
418
|
+
* Only delegation settings can change, and only within the mode chosen at
|
|
419
|
+
* startup. Startup fields (`model`, `instructions`, `audio`, `input`) are not
|
|
420
|
+
* update fields.
|
|
421
|
+
*
|
|
422
|
+
* - `delegation`: replacement delegation settings, of the session's mode.
|
|
423
|
+
*/
|
|
424
|
+
export class SessionUpdateConfig extends LiveModel {
|
|
425
|
+
static fields = {
|
|
426
|
+
delegation: field(delegationParser, () => null),
|
|
427
|
+
};
|
|
428
|
+
constructor(options = {}) {
|
|
429
|
+
super(options);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
const CLIENT_EVENT_FIELDS = {
|
|
433
|
+
eventId: field(P.str(), () => crypto.randomUUID()),
|
|
434
|
+
};
|
|
435
|
+
/**
|
|
436
|
+
* Base class for events sent to the Live API.
|
|
437
|
+
*
|
|
438
|
+
* - `eventId`: client-chosen identifier, echoed back as `client_event_id` on
|
|
439
|
+
* acknowledgments and errors.
|
|
440
|
+
*/
|
|
441
|
+
export class ClientEvent extends LiveModel {
|
|
442
|
+
static fields = CLIENT_EVENT_FIELDS;
|
|
443
|
+
constructor(options = {}) {
|
|
444
|
+
super(options);
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Render the event as the JSON payload to send.
|
|
448
|
+
*
|
|
449
|
+
* Unset optional fields are omitted: the session schema is strict about
|
|
450
|
+
* unknown fields, and a `null` is not the same as an absent field.
|
|
451
|
+
*
|
|
452
|
+
* @returns The payload.
|
|
453
|
+
*/
|
|
454
|
+
toPayload() {
|
|
455
|
+
return this.modelDump({ excludeNone: true });
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Start the session. The first message on a WebSocket, sent exactly once.
|
|
460
|
+
*
|
|
461
|
+
* - `type`: event type, always "session.start".
|
|
462
|
+
* - `session`: the session configuration, including the model.
|
|
463
|
+
*/
|
|
464
|
+
export class SessionStartEvent extends ClientEvent {
|
|
465
|
+
static fields = {
|
|
466
|
+
...CLIENT_EVENT_FIELDS,
|
|
467
|
+
type: typeField("session.start"),
|
|
468
|
+
session: field(P.model(() => SessionConfig)),
|
|
469
|
+
};
|
|
470
|
+
constructor(options) {
|
|
471
|
+
super(options);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Apply a sparse update to a running session.
|
|
476
|
+
*
|
|
477
|
+
* - `type`: event type, always "session.update".
|
|
478
|
+
* - `session`: the sparse update.
|
|
479
|
+
*/
|
|
480
|
+
export class SessionUpdateEvent extends ClientEvent {
|
|
481
|
+
static fields = {
|
|
482
|
+
...CLIENT_EVENT_FIELDS,
|
|
483
|
+
type: typeField("session.update"),
|
|
484
|
+
session: field(P.model(() => SessionUpdateConfig)),
|
|
485
|
+
};
|
|
486
|
+
constructor(options) {
|
|
487
|
+
super(options);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Append audio in the session's audio format. Not acknowledged.
|
|
492
|
+
*
|
|
493
|
+
* - `type`: event type, always "session.input_audio.append".
|
|
494
|
+
* - `audio`: base64-encoded audio bytes.
|
|
495
|
+
*/
|
|
496
|
+
export class InputAudioAppendEvent extends ClientEvent {
|
|
497
|
+
static fields = {
|
|
498
|
+
...CLIENT_EVENT_FIELDS,
|
|
499
|
+
type: typeField("session.input_audio.append"),
|
|
500
|
+
audio: field(P.str()),
|
|
501
|
+
};
|
|
502
|
+
constructor(options) {
|
|
503
|
+
super(options);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
const CONTEXT_APPEND_FIELDS = {
|
|
507
|
+
...CLIENT_EVENT_FIELDS,
|
|
508
|
+
// Required, but `null` is a valid value.
|
|
509
|
+
delegationId: field(P.optional(P.str())),
|
|
510
|
+
content: field(P.str()),
|
|
511
|
+
};
|
|
512
|
+
/**
|
|
513
|
+
* Base class for the three context-append events.
|
|
514
|
+
*
|
|
515
|
+
* - `delegationId`: the client delegation this content belongs to, or `null`
|
|
516
|
+
* for general session context. Always sent, including when it is `null`.
|
|
517
|
+
* - `content`: the text, at most 500 tokens.
|
|
518
|
+
*/
|
|
519
|
+
export class ContextAppendEvent extends ClientEvent {
|
|
520
|
+
static fields = CONTEXT_APPEND_FIELDS;
|
|
521
|
+
constructor(options) {
|
|
522
|
+
// A `null` delegation id is a value, not a missing field.
|
|
523
|
+
super(options);
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Render the event, keeping `delegation_id` even when it is `null`.
|
|
527
|
+
*
|
|
528
|
+
* The field is required on these events, and `null` is meaningful: it marks
|
|
529
|
+
* the content as general session context rather than belonging to a
|
|
530
|
+
* delegation.
|
|
531
|
+
*
|
|
532
|
+
* @returns The payload.
|
|
533
|
+
*/
|
|
534
|
+
toPayload() {
|
|
535
|
+
return { ...super.toPayload(), delegation_id: this.delegationId };
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Append instructions the live model will follow, without replacing the
|
|
540
|
+
* startup ones.
|
|
541
|
+
*
|
|
542
|
+
* - `type`: event type, always "session.instructions.append".
|
|
543
|
+
*/
|
|
544
|
+
export class SessionInstructionsAppendEvent extends ContextAppendEvent {
|
|
545
|
+
static fields = {
|
|
546
|
+
...CONTEXT_APPEND_FIELDS,
|
|
547
|
+
type: typeField("session.instructions.append"),
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Append information to the live model's internal reasoning.
|
|
552
|
+
*
|
|
553
|
+
* The content is not spoken when appended, though the model can draw on it
|
|
554
|
+
* when a later user request makes it relevant. It is not a secrecy boundary.
|
|
555
|
+
*
|
|
556
|
+
* - `type`: event type, always "session.thinking.append".
|
|
557
|
+
*/
|
|
558
|
+
export class SessionThinkingAppendEvent extends ContextAppendEvent {
|
|
559
|
+
static fields = {
|
|
560
|
+
...CONTEXT_APPEND_FIELDS,
|
|
561
|
+
type: typeField("session.thinking.append"),
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Append information for the live model to speak aloud, in its own words.
|
|
566
|
+
*
|
|
567
|
+
* - `type`: event type, always "session.commentary.append".
|
|
568
|
+
*/
|
|
569
|
+
export class SessionCommentaryAppendEvent extends ContextAppendEvent {
|
|
570
|
+
static fields = {
|
|
571
|
+
...CONTEXT_APPEND_FIELDS,
|
|
572
|
+
type: typeField("session.commentary.append"),
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* The result of a client-actionable function call from a Responses delegation.
|
|
577
|
+
*
|
|
578
|
+
* - `type`: item type, always "function_call_output".
|
|
579
|
+
* - `callId`: the `call_id` of the completed `function_call` item.
|
|
580
|
+
* - `output`: the function output, as a string.
|
|
581
|
+
*/
|
|
582
|
+
export class FunctionCallOutputItem extends LiveModel {
|
|
583
|
+
static fields = {
|
|
584
|
+
type: typeField("function_call_output"),
|
|
585
|
+
callId: field(P.str()),
|
|
586
|
+
output: field(P.str()),
|
|
587
|
+
};
|
|
588
|
+
constructor(options) {
|
|
589
|
+
super(options);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Queue an input item for the backend Responses model. Not acknowledged.
|
|
594
|
+
*
|
|
595
|
+
* Queueing an item does not start inference: `ResponseCreateEvent` does.
|
|
596
|
+
* Responses delegation only.
|
|
597
|
+
*
|
|
598
|
+
* - `type`: event type, always "response.item.create".
|
|
599
|
+
* - `item`: the input item, such as a function call output.
|
|
600
|
+
*/
|
|
601
|
+
export class ResponseItemCreateEvent extends ClientEvent {
|
|
602
|
+
static fields = {
|
|
603
|
+
...CLIENT_EVENT_FIELDS,
|
|
604
|
+
type: typeField("response.item.create"),
|
|
605
|
+
item: field(P.union(P.model(() => FunctionCallOutputItem), P.dict())),
|
|
606
|
+
};
|
|
607
|
+
constructor(options) {
|
|
608
|
+
super(options);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Start or continue delegated Responses work. Responses delegation only.
|
|
613
|
+
*
|
|
614
|
+
* Every output required by the pending response must be queued first, or the
|
|
615
|
+
* command is rejected with `function_call_outputs_required`.
|
|
616
|
+
*
|
|
617
|
+
* - `type`: event type, always "response.create".
|
|
618
|
+
*/
|
|
619
|
+
export class ResponseCreateEvent extends ClientEvent {
|
|
620
|
+
static fields = {
|
|
621
|
+
...CLIENT_EVENT_FIELDS,
|
|
622
|
+
type: typeField("response.create"),
|
|
623
|
+
};
|
|
624
|
+
constructor(options = {}) {
|
|
625
|
+
super(options);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Request a graceful shutdown; the server answers with `session.closed`.
|
|
630
|
+
*
|
|
631
|
+
* - `type`: event type, always "session.close".
|
|
632
|
+
*/
|
|
633
|
+
export class SessionCloseEvent extends ClientEvent {
|
|
634
|
+
static fields = {
|
|
635
|
+
...CLIENT_EVENT_FIELDS,
|
|
636
|
+
type: typeField("session.close"),
|
|
637
|
+
};
|
|
638
|
+
constructor(options = {}) {
|
|
639
|
+
super(options);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
//
|
|
643
|
+
// server events
|
|
644
|
+
//
|
|
645
|
+
const SERVER_EVENT_FIELDS = {
|
|
646
|
+
type: field(P.str()),
|
|
647
|
+
eventId: field(P.optional(P.str()), () => null),
|
|
648
|
+
clientEventId: field(P.optional(P.str()), () => null),
|
|
649
|
+
};
|
|
650
|
+
/**
|
|
651
|
+
* Base class for events received from the Live API.
|
|
652
|
+
*
|
|
653
|
+
* Unknown fields are retained (in `modelExtra`) so events keep parsing as the
|
|
654
|
+
* API grows.
|
|
655
|
+
*
|
|
656
|
+
* - `type`: the event type.
|
|
657
|
+
* - `eventId`: identifier of this server event.
|
|
658
|
+
* - `clientEventId`: the client event this one answers, when correlatable.
|
|
659
|
+
*/
|
|
660
|
+
export class ServerEvent extends LiveModel {
|
|
661
|
+
static extra = "allow";
|
|
662
|
+
static fields = SERVER_EVENT_FIELDS;
|
|
663
|
+
constructor(options) {
|
|
664
|
+
super(options);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* The resolved session, echoed by `session.started`, `session.updated` and
|
|
669
|
+
* `session.closed`.
|
|
670
|
+
*
|
|
671
|
+
* - `id`: session identifier. Opaque: forward it unchanged.
|
|
672
|
+
* - `expiresAt`: session expiry as a Unix timestamp in seconds.
|
|
673
|
+
* - `status`: session status.
|
|
674
|
+
* - `model`: the live model.
|
|
675
|
+
* - `instructions`: the startup instructions.
|
|
676
|
+
* - `audio`: audio configuration.
|
|
677
|
+
* - `delegation`: delegation configuration.
|
|
678
|
+
* - `input`: the startup conversation history.
|
|
679
|
+
* - `contextManagement`: long-session context handling.
|
|
680
|
+
*/
|
|
681
|
+
export class SessionResource extends LiveModel {
|
|
682
|
+
static extra = "allow";
|
|
683
|
+
static fields = {
|
|
684
|
+
id: field(P.optional(P.str()), () => null),
|
|
685
|
+
expiresAt: field(P.optional(P.int()), () => null),
|
|
686
|
+
status: field(P.optional(P.str()), () => null),
|
|
687
|
+
model: field(P.optional(P.str()), () => null),
|
|
688
|
+
instructions: field(P.optional(P.str()), () => null),
|
|
689
|
+
audio: field(P.optional(P.dict()), () => null),
|
|
690
|
+
delegation: field(P.optional(P.dict()), () => null),
|
|
691
|
+
input: field(P.optional(P.list(P.dict())), () => null),
|
|
692
|
+
contextManagement: field(P.optional(P.dict()), () => null),
|
|
693
|
+
};
|
|
694
|
+
constructor(options = {}) {
|
|
695
|
+
super(options);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* The session is configured and ready for audio.
|
|
700
|
+
*
|
|
701
|
+
* - `type`: event type, always "session.started".
|
|
702
|
+
* - `session`: the resolved session.
|
|
703
|
+
*/
|
|
704
|
+
export class SessionStartedEvent extends ServerEvent {
|
|
705
|
+
static fields = {
|
|
706
|
+
...SERVER_EVENT_FIELDS,
|
|
707
|
+
type: field(P.literal("session.started")),
|
|
708
|
+
session: field(P.model(() => SessionResource)),
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* A sparse `session.update` was applied.
|
|
713
|
+
*
|
|
714
|
+
* - `type`: event type, always "session.updated".
|
|
715
|
+
* - `session`: the complete resolved session, not just the changed fields.
|
|
716
|
+
*/
|
|
717
|
+
export class SessionUpdatedEvent extends ServerEvent {
|
|
718
|
+
static fields = {
|
|
719
|
+
...SERVER_EVENT_FIELDS,
|
|
720
|
+
type: field(P.literal("session.updated")),
|
|
721
|
+
session: field(P.model(() => SessionResource)),
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* A chunk of output audio in the session's audio format.
|
|
726
|
+
*
|
|
727
|
+
* - `type`: event type, always "session.output_audio.delta".
|
|
728
|
+
* - `delta`: base64-encoded audio bytes.
|
|
729
|
+
*/
|
|
730
|
+
export class OutputAudioDeltaEvent extends ServerEvent {
|
|
731
|
+
static fields = {
|
|
732
|
+
...SERVER_EVENT_FIELDS,
|
|
733
|
+
type: field(P.literal("session.output_audio.delta")),
|
|
734
|
+
delta: field(P.str()),
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* A timed transcript fragment of user or assistant speech.
|
|
739
|
+
*
|
|
740
|
+
* Fragments are frame-aligned, not word- or turn-aligned: accumulate them in
|
|
741
|
+
* order, and group them into turns in the application if it needs turns.
|
|
742
|
+
*
|
|
743
|
+
* - `type`: "session.input_transcript.delta" (the user) or
|
|
744
|
+
* "session.output_transcript.delta" (the assistant).
|
|
745
|
+
* - `delta`: the fragment text.
|
|
746
|
+
* - `startMs`: start of the fragment on the session timeline.
|
|
747
|
+
* - `endMs`: end of the fragment on the session timeline.
|
|
748
|
+
*/
|
|
749
|
+
export class TranscriptDeltaEvent extends ServerEvent {
|
|
750
|
+
static fields = {
|
|
751
|
+
...SERVER_EVENT_FIELDS,
|
|
752
|
+
type: field(P.literal("session.input_transcript.delta", "session.output_transcript.delta")),
|
|
753
|
+
delta: field(P.str(), () => ""),
|
|
754
|
+
startMs: field(P.optional(P.int()), () => null),
|
|
755
|
+
endMs: field(P.optional(P.int()), () => null),
|
|
756
|
+
};
|
|
757
|
+
/** The speaker. */
|
|
758
|
+
get role() {
|
|
759
|
+
return this.type === "session.input_transcript.delta" ? "user" : "assistant";
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* A unit of work the live model delegated.
|
|
764
|
+
*
|
|
765
|
+
* The metadata carries no task text: a client delegator works out what to do
|
|
766
|
+
* from the conversation and application state it keeps itself.
|
|
767
|
+
*
|
|
768
|
+
* - `id`: delegation identifier, used to correlate returned context. Opaque:
|
|
769
|
+
* return it unchanged.
|
|
770
|
+
* - `type`: item type, always "delegation".
|
|
771
|
+
* - `target`: "client" or "responses".
|
|
772
|
+
* - `responseId`: for Responses delegations, the id of the Responses run.
|
|
773
|
+
*/
|
|
774
|
+
export class DelegationMetadata extends LiveModel {
|
|
775
|
+
static extra = "allow";
|
|
776
|
+
static fields = {
|
|
777
|
+
id: field(P.str()),
|
|
778
|
+
type: field(P.str(), () => "delegation"),
|
|
779
|
+
target: field(P.str()),
|
|
780
|
+
responseId: field(P.optional(P.str()), () => null),
|
|
781
|
+
};
|
|
782
|
+
constructor(options) {
|
|
783
|
+
super(options);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* The live model created a delegation.
|
|
788
|
+
*
|
|
789
|
+
* - `type`: event type, always "session.delegation.created".
|
|
790
|
+
* - `offsetMs`: position on the session timeline.
|
|
791
|
+
* - `delegation`: the delegation metadata.
|
|
792
|
+
*/
|
|
793
|
+
export class SessionDelegationCreatedEvent extends ServerEvent {
|
|
794
|
+
static fields = {
|
|
795
|
+
...SERVER_EVENT_FIELDS,
|
|
796
|
+
type: field(P.literal("session.delegation.created")),
|
|
797
|
+
offsetMs: field(P.optional(P.int()), () => null),
|
|
798
|
+
delegation: field(P.model(() => DelegationMetadata)),
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* An output item of a Responses delegation.
|
|
803
|
+
*
|
|
804
|
+
* - `id`: item identifier.
|
|
805
|
+
* - `type`: item type, e.g. "function_call" or "message".
|
|
806
|
+
* - `status`: item status, e.g. "completed".
|
|
807
|
+
* - `callId`: for function calls, the call identifier to answer with.
|
|
808
|
+
* - `name`: for function calls, the function name.
|
|
809
|
+
* - `arguments`: for function calls, the JSON-encoded arguments.
|
|
810
|
+
*/
|
|
811
|
+
export class ResponseOutputItem extends LiveModel {
|
|
812
|
+
static extra = "allow";
|
|
813
|
+
static fields = {
|
|
814
|
+
id: field(P.optional(P.str()), () => null),
|
|
815
|
+
type: field(P.optional(P.str()), () => null),
|
|
816
|
+
status: field(P.optional(P.str()), () => null),
|
|
817
|
+
callId: field(P.optional(P.str()), () => null),
|
|
818
|
+
name: field(P.optional(P.str()), () => null),
|
|
819
|
+
arguments: field(P.optional(P.str()), () => null),
|
|
820
|
+
};
|
|
821
|
+
constructor(options = {}) {
|
|
822
|
+
super(options);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* A Responses lifecycle event, wrapped with the delegation it belongs to.
|
|
827
|
+
*
|
|
828
|
+
* The nested event keeps its Responses meaning and is dispatched by its own
|
|
829
|
+
* complete `type`. Lifecycle snapshots inside are reduced — `output` is an
|
|
830
|
+
* empty array even at `response.completed` — so collect output items from
|
|
831
|
+
* their individual events rather than from a snapshot.
|
|
832
|
+
*
|
|
833
|
+
* - `type`: event type, always "response.event".
|
|
834
|
+
* - `delegationId`: the delegation this response belongs to, when known.
|
|
835
|
+
* - `event`: the nested Responses event.
|
|
836
|
+
*/
|
|
837
|
+
export class ResponseEventEnvelope extends ServerEvent {
|
|
838
|
+
static fields = {
|
|
839
|
+
...SERVER_EVENT_FIELDS,
|
|
840
|
+
type: field(P.literal("response.event")),
|
|
841
|
+
delegationId: field(P.optional(P.str()), () => null),
|
|
842
|
+
event: field(P.dict(), () => ({})),
|
|
843
|
+
};
|
|
844
|
+
/** The nested Responses event's type, or `""` if it has none. */
|
|
845
|
+
get innerType() {
|
|
846
|
+
const inner = this.event.type;
|
|
847
|
+
return typeof inner === "string" ? inner : "";
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Appended context was accepted and placed on the session timeline.
|
|
852
|
+
*
|
|
853
|
+
* Acceptance is not proof the model has spoken the content.
|
|
854
|
+
*
|
|
855
|
+
* - `type`: "session.instructions.appended", "session.thinking.appended" or
|
|
856
|
+
* "session.commentary.appended".
|
|
857
|
+
* - `startMs`: start of the accepted range.
|
|
858
|
+
* - `endMs`: end of the accepted range.
|
|
859
|
+
*/
|
|
860
|
+
export class ContextAppendedEvent extends ServerEvent {
|
|
861
|
+
static fields = {
|
|
862
|
+
...SERVER_EVENT_FIELDS,
|
|
863
|
+
type: field(P.literal("session.instructions.appended", "session.thinking.appended", "session.commentary.appended")),
|
|
864
|
+
startMs: field(P.optional(P.int()), () => null),
|
|
865
|
+
endMs: field(P.optional(P.int()), () => null),
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Context utilization, when reported.
|
|
870
|
+
*
|
|
871
|
+
* - `usageRatio`: fraction of the context window in use.
|
|
872
|
+
*/
|
|
873
|
+
export class ContextWindowUsage extends LiveModel {
|
|
874
|
+
static extra = "allow";
|
|
875
|
+
static fields = {
|
|
876
|
+
usageRatio: field(P.optional(P.float()), () => null),
|
|
877
|
+
};
|
|
878
|
+
constructor(options = {}) {
|
|
879
|
+
super(options);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Cumulative live session usage.
|
|
884
|
+
*
|
|
885
|
+
* Backend token usage is not here: it belongs to the wrapped Responses
|
|
886
|
+
* lifecycle, on the nested `response.completed` event.
|
|
887
|
+
*
|
|
888
|
+
* - `seconds`: cumulative live audio duration in seconds.
|
|
889
|
+
*/
|
|
890
|
+
export class Usage extends LiveModel {
|
|
891
|
+
static extra = "allow";
|
|
892
|
+
static fields = {
|
|
893
|
+
seconds: field(P.optional(P.float()), () => null),
|
|
894
|
+
};
|
|
895
|
+
constructor(options = {}) {
|
|
896
|
+
super(options);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* Periodic cumulative usage report.
|
|
901
|
+
*
|
|
902
|
+
* - `type`: event type, always "session.usage.updated".
|
|
903
|
+
* - `usage`: cumulative usage, not an increment to sum.
|
|
904
|
+
* - `contextWindow`: context utilization, when reported.
|
|
905
|
+
*/
|
|
906
|
+
export class SessionUsageUpdatedEvent extends ServerEvent {
|
|
907
|
+
static fields = {
|
|
908
|
+
...SERVER_EVENT_FIELDS,
|
|
909
|
+
type: field(P.literal("session.usage.updated")),
|
|
910
|
+
usage: field(P.model(() => Usage)),
|
|
911
|
+
contextWindow: field(P.optional(P.model(() => ContextWindowUsage)), () => null),
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Graceful shutdown completed. Transport closure alone is not finalization.
|
|
916
|
+
*
|
|
917
|
+
* - `type`: event type, always "session.closed".
|
|
918
|
+
* - `reason`: why the session closed.
|
|
919
|
+
* - `session`: the session snapshot, as configuration rather than a live
|
|
920
|
+
* session.
|
|
921
|
+
* - `usage`: final cumulative usage.
|
|
922
|
+
*/
|
|
923
|
+
export class SessionClosedEvent extends ServerEvent {
|
|
924
|
+
static fields = {
|
|
925
|
+
...SERVER_EVENT_FIELDS,
|
|
926
|
+
type: field(P.literal("session.closed")),
|
|
927
|
+
reason: field(P.optional(P.str()), () => null),
|
|
928
|
+
session: field(P.optional(P.model(() => SessionResource)), () => null),
|
|
929
|
+
usage: field(P.optional(P.model(() => Usage)), () => null),
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* The error envelope shared by Live API and normalized Responses errors.
|
|
934
|
+
*
|
|
935
|
+
* - `type`: error type, e.g. "invalid_request_error".
|
|
936
|
+
* - `code`: error code.
|
|
937
|
+
* - `message`: human-readable message.
|
|
938
|
+
* - `param`: the offending field, when one caused the failure.
|
|
939
|
+
* - `clientEventId`: the client event the error correlates to, when known.
|
|
940
|
+
*/
|
|
941
|
+
export class ErrorDetails extends LiveModel {
|
|
942
|
+
static extra = "allow";
|
|
943
|
+
static fields = {
|
|
944
|
+
type: field(P.optional(P.str()), () => null),
|
|
945
|
+
code: field(P.optional(P.str()), () => null),
|
|
946
|
+
message: field(P.optional(P.str()), () => null),
|
|
947
|
+
param: field(P.optional(P.str()), () => null),
|
|
948
|
+
clientEventId: field(P.optional(P.str()), () => null),
|
|
949
|
+
};
|
|
950
|
+
constructor(options = {}) {
|
|
951
|
+
super(options);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* A startup, validation, command or delegated-Responses error.
|
|
956
|
+
*
|
|
957
|
+
* - `type`: event type, always "error".
|
|
958
|
+
* - `error`: the error details.
|
|
959
|
+
*/
|
|
960
|
+
export class ErrorEvent extends ServerEvent {
|
|
961
|
+
static fields = {
|
|
962
|
+
...SERVER_EVENT_FIELDS,
|
|
963
|
+
type: field(P.literal("error")),
|
|
964
|
+
error: field(P.model(() => ErrorDetails)),
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
/** A server event this module doesn't model; its fields are retained as extras. */
|
|
968
|
+
export class UnknownServerEvent extends ServerEvent {
|
|
969
|
+
}
|
|
970
|
+
const _serverEventTypes = {
|
|
971
|
+
error: ErrorEvent,
|
|
972
|
+
"session.started": SessionStartedEvent,
|
|
973
|
+
"session.updated": SessionUpdatedEvent,
|
|
974
|
+
"session.usage.updated": SessionUsageUpdatedEvent,
|
|
975
|
+
"session.closed": SessionClosedEvent,
|
|
976
|
+
"session.output_audio.delta": OutputAudioDeltaEvent,
|
|
977
|
+
"session.input_transcript.delta": TranscriptDeltaEvent,
|
|
978
|
+
"session.output_transcript.delta": TranscriptDeltaEvent,
|
|
979
|
+
"session.delegation.created": SessionDelegationCreatedEvent,
|
|
980
|
+
"session.instructions.appended": ContextAppendedEvent,
|
|
981
|
+
"session.thinking.appended": ContextAppendedEvent,
|
|
982
|
+
"session.commentary.appended": ContextAppendedEvent,
|
|
983
|
+
"response.event": ResponseEventEnvelope,
|
|
984
|
+
};
|
|
985
|
+
/**
|
|
986
|
+
* Parse a server event from its JSON text.
|
|
987
|
+
*
|
|
988
|
+
* Event types this module doesn't model become `UnknownServerEvent`.
|
|
989
|
+
*
|
|
990
|
+
* @param message The JSON text of one server event, as a string or UTF-8 bytes.
|
|
991
|
+
* @returns The parsed event.
|
|
992
|
+
* @throws Error If the text isn't a JSON object with a string `type`, or a
|
|
993
|
+
* modeled event fails validation (upstream: ValueError).
|
|
994
|
+
*/
|
|
995
|
+
export function parseServerEvent(message) {
|
|
996
|
+
const text = typeof message === "string" ? message : new TextDecoder().decode(message);
|
|
997
|
+
let data;
|
|
998
|
+
try {
|
|
999
|
+
data = pyJsonLoads(text);
|
|
1000
|
+
}
|
|
1001
|
+
catch (e) {
|
|
1002
|
+
throw new Error(`Invalid server event JSON: ${e instanceof Error ? e.message : String(e)}`, {
|
|
1003
|
+
cause: e,
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
if (!isPlainObject(data) || typeof data.type !== "string") {
|
|
1007
|
+
throw new Error(`Server event is not an object with a string type: ${text}`);
|
|
1008
|
+
}
|
|
1009
|
+
const eventType = data.type;
|
|
1010
|
+
const model = Object.hasOwn(_serverEventTypes, eventType)
|
|
1011
|
+
? _serverEventTypes[eventType]
|
|
1012
|
+
: UnknownServerEvent;
|
|
1013
|
+
try {
|
|
1014
|
+
return model.modelValidate(data);
|
|
1015
|
+
}
|
|
1016
|
+
catch (e) {
|
|
1017
|
+
throw new Error(`Invalid ${eventType} server event: ${e instanceof Error ? e.message : String(e)}`, { cause: e });
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
//# sourceMappingURL=events.js.map
|