@oxy.so/contracts 4.2.0 → 4.4.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.
Files changed (34) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/index.js +72 -4
  3. package/dist/cjs/inference/catalogue.js +105 -2
  4. package/dist/cjs/inference/money.js +23 -0
  5. package/dist/cjs/inference/realtime.js +826 -0
  6. package/dist/cjs/inference/request.js +34 -11
  7. package/dist/cjs/inference/streamEvents.js +7 -1
  8. package/dist/cjs/inference/version.js +1 -1
  9. package/dist/cjs/stickers.js +151 -0
  10. package/dist/esm/.tsbuildinfo +1 -1
  11. package/dist/esm/index.js +6 -2
  12. package/dist/esm/inference/catalogue.js +104 -1
  13. package/dist/esm/inference/money.js +23 -0
  14. package/dist/esm/inference/realtime.js +822 -0
  15. package/dist/esm/inference/request.js +34 -11
  16. package/dist/esm/inference/streamEvents.js +7 -1
  17. package/dist/esm/inference/version.js +1 -1
  18. package/dist/esm/stickers.js +148 -0
  19. package/dist/types/.tsbuildinfo +1 -1
  20. package/dist/types/index.d.ts +7 -4
  21. package/dist/types/inference/catalogue.d.ts +299 -10
  22. package/dist/types/inference/entitlement.d.ts +6 -6
  23. package/dist/types/inference/inbox.d.ts +2 -2
  24. package/dist/types/inference/modelDocumentation.d.ts +146 -2
  25. package/dist/types/inference/money.d.ts +28 -8
  26. package/dist/types/inference/priceVersion.d.ts +12 -12
  27. package/dist/types/inference/realtime.d.ts +8012 -0
  28. package/dist/types/inference/request.d.ts +51 -0
  29. package/dist/types/inference/routingPolicy.d.ts +7 -7
  30. package/dist/types/inference/streamEvents.d.ts +28 -16
  31. package/dist/types/inference/usage.d.ts +28 -28
  32. package/dist/types/inference/version.d.ts +1 -1
  33. package/dist/types/stickers.d.ts +1049 -0
  34. package/package.json +1 -1
@@ -0,0 +1,826 @@
1
+ "use strict";
2
+ /**
3
+ * Realtime sessions — the one inference family that is not a request.
4
+ *
5
+ * Everything else in this contract is one-shot: an envelope goes in, a stream of
6
+ * events comes out, one terminal event ends it. A realtime session (OpenAI's
7
+ * Realtime and Live families, and any provider like them) is a long-lived,
8
+ * two-way conversation over a WebSocket: the client keeps appending audio,
9
+ * editing the conversation and asking for responses, and the server keeps
10
+ * answering, detecting speech, transcribing and being interrupted, for up to an
11
+ * hour. Forcing that through {@link inferenceRequestSchema} would make the
12
+ * request envelope mean two things, so it is its own family: a signed session
13
+ * request, a closed set of client COMMANDS and a closed set of server EVENTS.
14
+ *
15
+ * ## Identity, metering and settlement
16
+ *
17
+ * A session IS one request for attribution, metering and settlement:
18
+ * `attribution.requestId` names it, every command and event carries that
19
+ * `requestId`, and the session settles exactly once, through one
20
+ * `normalizedUsageReportSchema` for that request id, delivered by the transport
21
+ * after `session.closed` exactly as a one-shot stream delivers its report.
22
+ * `response.done` and `session.closed` carry units as MEASUREMENT EVIDENCE, the
23
+ * same standing `inferenceStreamUsageEventSchema` has: settleable units, never a
24
+ * settleable record. No upstream cost appears anywhere in this module.
25
+ *
26
+ * ## Routing
27
+ *
28
+ * A session names a model, never a routing profile, and is never substituted:
29
+ * every authorized route is `same_model`. The data plane may try the routes in
30
+ * order until one OPENS; once `session.created` has been sent the deployment is
31
+ * fixed for the session's life. There is no mid-session failover, because the
32
+ * conversation state lives upstream and a second provider does not have it.
33
+ *
34
+ * ## Framing
35
+ *
36
+ * Every WebSocket frame is ONE JSON text message: a command from the client, an
37
+ * event from the server. Binary frames are refused. Audio travels only as
38
+ * padded base64 inside `input_audio.append`, an `input_audio` content part or
39
+ * `output_audio.delta`, each bounded by {@link MAX_REALTIME_AUDIO_FRAME_BASE64_LENGTH}
40
+ * characters (48 KiB of audio, about one second of 24 kHz PCM16), and a
41
+ * session's total audio in each direction is bounded by its signed
42
+ * `limits`. Audio is never a text delta: a transcript is `transcript.delta`,
43
+ * spoken audio is `output_audio.delta`, and neither is `text.delta`.
44
+ *
45
+ * ## Commands are at-most-once
46
+ *
47
+ * Every command carries a client-chosen `commandId`, unique within the session,
48
+ * and the server answers each with `command.accepted` BEFORE applying it. So:
49
+ *
50
+ * - A command whose acknowledgement arrived was applied, once.
51
+ * - A command sent again with a `commandId` the server has already accepted is
52
+ * NOT applied again: the server answers `command.accepted` with
53
+ * `duplicate: true`. A client may therefore retry a command it is unsure of by
54
+ * resending the SAME `commandId`, and only that way.
55
+ * - A command whose write was AMBIGUOUS — the connection dropped before its
56
+ * acknowledgement — is never replayed automatically by any hop. Resending it
57
+ * under a NEW `commandId` is a new command, and appending the same audio twice
58
+ * is exactly the double-metered input this rule exists to prevent.
59
+ * - The data plane never replays a command upstream. If ITS upstream connection
60
+ * drops, the upstream conversation is gone with it and the session ends with
61
+ * `session.closed` (`upstream_closed` or `upstream_error`).
62
+ *
63
+ * ## Reconnect
64
+ *
65
+ * A dropped client connection does not end the session at once. For
66
+ * `resumeWindowMs` (declared by `session.created`, at most
67
+ * {@link MAX_REALTIME_RESUME_WINDOW_MS}) the client may open a new connection
68
+ * whose FIRST frame is a signed `session.resume` naming the last server
69
+ * `sequence` it processed. The server then replays every event after it, in
70
+ * order, emits `session.resumed`, and continues. Events are numbered from 0 by a
71
+ * single monotonic `sequence` per session, so a replayed event is recognisable
72
+ * as a duplicate rather than as new output. Past the window, or when the named
73
+ * sequence is no longer buffered, the resume is refused and the session closes
74
+ * (`resume_expired`): nothing is guessed. Commands are not part of the replay —
75
+ * see above.
76
+ *
77
+ * ## Authentication
78
+ *
79
+ * The session request and a `session.resume` are the first frame of their
80
+ * connection and are signed with the same edge signature the one-shot envelope
81
+ * carries, over that frame's exact bytes; the signature headers ride on the
82
+ * WebSocket upgrade. Every later frame is covered by the connection that
83
+ * signature opened.
84
+ *
85
+ * Decided in: OxyHQ/Kaana#90.
86
+ */
87
+ Object.defineProperty(exports, "__esModule", { value: true });
88
+ exports.realtimeTranscriptDoneEventSchema = exports.realtimeTranscriptDeltaEventSchema = exports.realtimeTranscriptSourceSchema = exports.realtimeOutputAudioDoneEventSchema = exports.realtimeOutputAudioDeltaEventSchema = exports.realtimeResponseCreatedEventSchema = exports.realtimeInputAudioClearedEventSchema = exports.realtimeInputAudioCommittedEventSchema = exports.realtimeSpeechStoppedEventSchema = exports.realtimeSpeechStartedEventSchema = exports.realtimeItemTruncatedEventSchema = exports.realtimeItemDeletedEventSchema = exports.realtimeItemDoneEventSchema = exports.realtimeItemAddedEventSchema = exports.realtimeCommandAcceptedEventSchema = exports.realtimeSessionResumedEventSchema = exports.realtimeSessionUpdatedEventSchema = exports.realtimeSessionCreatedEventSchema = exports.realtimeClientCommandSchema = exports.realtimeSessionCloseCommandSchema = exports.realtimeSessionResumeCommandSchema = exports.realtimeResponseCancelCommandSchema = exports.realtimeResponseCreateCommandSchema = exports.realtimeInputAudioClearCommandSchema = exports.realtimeInputAudioCommitCommandSchema = exports.realtimeInputAudioAppendCommandSchema = exports.realtimeItemTruncateCommandSchema = exports.realtimeItemDeleteCommandSchema = exports.realtimeItemCreateCommandSchema = exports.realtimeSessionUpdateCommandSchema = exports.realtimeResponseParametersSchema = exports.realtimeConversationItemSchema = exports.realtimeContentPartSchema = exports.realtimeSessionRequestSchema = exports.realtimeClientMetadataSchema = exports.realtimeSessionLimitsSchema = exports.realtimeSessionConfigUpdateSchema = exports.realtimeSessionConfigSchema = exports.realtimeTranslationSchema = exports.realtimeInputTranscriptionSchema = exports.realtimeTurnDetectionSchema = exports.realtimeOutputModalitySchema = exports.realtimeAudioFormatSchema = exports.realtimeResponseIdSchema = exports.realtimeItemIdSchema = exports.realtimeCommandIdSchema = exports.MAX_REALTIME_RESUME_WINDOW_MS = exports.MAX_REALTIME_SESSION_AUDIO_BYTES = exports.MAX_REALTIME_SESSION_DURATION_MS = exports.MAX_REALTIME_AUDIO_FRAME_BASE64_LENGTH = void 0;
89
+ exports.realtimeServerEventSchema = exports.realtimeSessionClosedEventSchema = exports.realtimeSessionCloseReasonSchema = exports.realtimeErrorEventSchema = exports.realtimeResponseDoneEventSchema = exports.realtimeResponseStatusSchema = exports.realtimeToolCallEventSchema = exports.realtimeTextDeltaEventSchema = void 0;
90
+ const zod_1 = require("zod");
91
+ const attribution_1 = require("./attribution");
92
+ const catalogue_1 = require("./catalogue");
93
+ const errors_1 = require("./errors");
94
+ const identifiers_1 = require("./identifiers");
95
+ const money_1 = require("./money");
96
+ const request_1 = require("./request");
97
+ const routingPolicy_1 = require("./routingPolicy");
98
+ const streamEvents_1 = require("./streamEvents");
99
+ /* -------------------------------------------------------------------------- */
100
+ /* Bounds */
101
+ /* -------------------------------------------------------------------------- */
102
+ /**
103
+ * The most base64 characters one audio frame may carry: 49 152 bytes of audio,
104
+ * about one second of 24 kHz mono PCM16. Bounded so no frame is large enough to
105
+ * stall a connection, and so a byte cap can be enforced as audio arrives rather
106
+ * than after it has been buffered.
107
+ */
108
+ exports.MAX_REALTIME_AUDIO_FRAME_BASE64_LENGTH = 65536;
109
+ /** The longest a session may be signed for: one hour. */
110
+ exports.MAX_REALTIME_SESSION_DURATION_MS = 3600000;
111
+ /**
112
+ * The most audio a session may be signed for in EACH direction: one hour of
113
+ * 24 kHz mono PCM16 (48 000 bytes per second), the densest format a session
114
+ * carries.
115
+ */
116
+ exports.MAX_REALTIME_SESSION_AUDIO_BYTES = 172800000;
117
+ /** The longest a dropped connection may be resumed after: one minute. */
118
+ exports.MAX_REALTIME_RESUME_WINDOW_MS = 60000;
119
+ /* -------------------------------------------------------------------------- */
120
+ /* Vocabulary */
121
+ /* -------------------------------------------------------------------------- */
122
+ /** A client-chosen command identity, unique within one session. */
123
+ exports.realtimeCommandIdSchema = zod_1.z.string().min(1).max(128);
124
+ /** A conversation item's identity within one session. */
125
+ exports.realtimeItemIdSchema = zod_1.z.string().min(1).max(128);
126
+ /** A response's identity within one session. */
127
+ exports.realtimeResponseIdSchema = zod_1.z.string().min(1).max(128);
128
+ /**
129
+ * The audio encodings a session carries, in both directions. Named by what they
130
+ * ARE (sample format, rate) because a realtime stream has no container to say so.
131
+ */
132
+ exports.realtimeAudioFormatSchema = zod_1.z.enum([
133
+ "pcm16_24khz",
134
+ "g711_ulaw",
135
+ "g711_alaw",
136
+ ]);
137
+ /** What a session's responses produce. */
138
+ exports.realtimeOutputModalitySchema = zod_1.z.enum(["text", "audio"]);
139
+ const audioFrameSchema = zod_1.z
140
+ .string()
141
+ .min(4)
142
+ .max(exports.MAX_REALTIME_AUDIO_FRAME_BASE64_LENGTH)
143
+ .regex(identifiers_1.PADDED_BASE64_PATTERN);
144
+ const sequenceSchema = zod_1.z.number().int().nonnegative().safe();
145
+ const milliseconds = zod_1.z.number().int().nonnegative().safe();
146
+ /* -------------------------------------------------------------------------- */
147
+ /* Session configuration */
148
+ /* -------------------------------------------------------------------------- */
149
+ /**
150
+ * How the end of a user's turn is detected.
151
+ *
152
+ * `none` means the client commits input audio itself. The two VAD forms name
153
+ * `createResponse` and `interruptResponse` explicitly rather than defaulting
154
+ * them: whether the model answers on its own, and whether new speech cuts it
155
+ * off, are the two behaviours a caller most needs to have chosen, and a default
156
+ * would choose them silently — and differently per provider.
157
+ */
158
+ exports.realtimeTurnDetectionSchema = zod_1.z.discriminatedUnion("type", [
159
+ zod_1.z.object({ type: zod_1.z.literal("none") }).strict(),
160
+ zod_1.z
161
+ .object({
162
+ type: zod_1.z.literal("server_vad"),
163
+ threshold: zod_1.z.number().min(0).max(1).optional(),
164
+ prefixPaddingMs: zod_1.z.number().int().min(0).max(5000).optional(),
165
+ silenceDurationMs: zod_1.z.number().int().min(0).max(10000).optional(),
166
+ createResponse: zod_1.z.boolean(),
167
+ interruptResponse: zod_1.z.boolean(),
168
+ })
169
+ .strict(),
170
+ zod_1.z
171
+ .object({
172
+ type: zod_1.z.literal("semantic_vad"),
173
+ eagerness: zod_1.z.enum(["low", "medium", "high", "auto"]),
174
+ createResponse: zod_1.z.boolean(),
175
+ interruptResponse: zod_1.z.boolean(),
176
+ })
177
+ .strict(),
178
+ ]);
179
+ /**
180
+ * Transcription of the caller's input audio. Presence enables it; the
181
+ * transcription model is the deployment's own, declared where the deployment
182
+ * is, never chosen by a string here.
183
+ */
184
+ exports.realtimeInputTranscriptionSchema = zod_1.z
185
+ .object({
186
+ /** A BCP 47 tag, when the caller knows the language. */
187
+ language: zod_1.z.string().min(2).max(35).optional(),
188
+ prompt: zod_1.z.string().max(4096).optional(),
189
+ })
190
+ .strict();
191
+ /** The target of a `translation` session. */
192
+ exports.realtimeTranslationSchema = zod_1.z
193
+ .object({
194
+ /** A BCP 47 tag. */
195
+ targetLanguage: zod_1.z.string().min(2).max(35),
196
+ })
197
+ .strict();
198
+ /**
199
+ * A session's full configuration, as signed at open and as echoed back by
200
+ * `session.created` and `session.updated`.
201
+ *
202
+ * `inputAudioFormat` and `turnDetection` are required because every session
203
+ * kind consumes audio and has to know when a turn ends; nothing else is,
204
+ * because each kind needs a different subset — see the refinement on
205
+ * {@link realtimeSessionRequestSchema}.
206
+ */
207
+ exports.realtimeSessionConfigSchema = zod_1.z
208
+ .object({
209
+ instructions: zod_1.z.string().max(32768).optional(),
210
+ outputModalities: zod_1.z.array(exports.realtimeOutputModalitySchema).min(1).max(2).optional(),
211
+ voice: zod_1.z.string().min(1).max(64).optional(),
212
+ inputAudioFormat: exports.realtimeAudioFormatSchema,
213
+ outputAudioFormat: exports.realtimeAudioFormatSchema.optional(),
214
+ turnDetection: exports.realtimeTurnDetectionSchema,
215
+ inputAudioTranscription: exports.realtimeInputTranscriptionSchema.optional(),
216
+ translation: exports.realtimeTranslationSchema.optional(),
217
+ tools: zod_1.z.array(request_1.toolDefinitionSchema).max(128).optional(),
218
+ toolChoice: request_1.toolChoiceSchema.optional(),
219
+ temperature: zod_1.z.number().min(0).max(2).optional(),
220
+ /** Per response, not per session: the session's ceiling is `limits`. */
221
+ maxOutputTokens: zod_1.z.number().int().positive().safe().optional(),
222
+ })
223
+ .strict();
224
+ /**
225
+ * What `session.update` may change mid-session.
226
+ *
227
+ * The session's kind, its audio formats and its voice are fixed at open —
228
+ * providers refuse to change a voice once it has spoken, and a format change
229
+ * mid-stream would make every buffered frame ambiguous — so they are not fields
230
+ * here, and the strict parse refuses them rather than ignoring them.
231
+ */
232
+ exports.realtimeSessionConfigUpdateSchema = zod_1.z
233
+ .object({
234
+ instructions: zod_1.z.string().max(32768).optional(),
235
+ outputModalities: zod_1.z.array(exports.realtimeOutputModalitySchema).min(1).max(2).optional(),
236
+ turnDetection: exports.realtimeTurnDetectionSchema.optional(),
237
+ inputAudioTranscription: exports.realtimeInputTranscriptionSchema.optional(),
238
+ tools: zod_1.z.array(request_1.toolDefinitionSchema).max(128).optional(),
239
+ toolChoice: request_1.toolChoiceSchema.optional(),
240
+ temperature: zod_1.z.number().min(0).max(2).optional(),
241
+ maxOutputTokens: zod_1.z.number().int().positive().safe().optional(),
242
+ })
243
+ .strict()
244
+ .superRefine((update, ctx) => {
245
+ if (Object.keys(update).length === 0) {
246
+ ctx.addIssue({
247
+ code: zod_1.z.ZodIssueCode.custom,
248
+ message: "a session update changes at least one field",
249
+ });
250
+ }
251
+ });
252
+ /**
253
+ * The ceilings Oxy signed the session for. The data plane enforces each one and
254
+ * closes the session (`limit_exceeded`, `max_duration`, `idle_timeout`) rather
255
+ * than exceeding it: these bound what the session can cost, which is what Oxy's
256
+ * spend reservation was sized against.
257
+ */
258
+ exports.realtimeSessionLimitsSchema = zod_1.z
259
+ .object({
260
+ maxDurationMs: zod_1.z.number().int().min(1000).max(exports.MAX_REALTIME_SESSION_DURATION_MS),
261
+ idleTimeoutMs: zod_1.z.number().int().min(1000).max(exports.MAX_REALTIME_SESSION_DURATION_MS),
262
+ maxInputAudioBytes: zod_1.z.number().int().positive().max(exports.MAX_REALTIME_SESSION_AUDIO_BYTES),
263
+ maxOutputAudioBytes: zod_1.z.number().int().positive().max(exports.MAX_REALTIME_SESSION_AUDIO_BYTES),
264
+ maxResponses: zod_1.z.number().int().positive().max(10000),
265
+ })
266
+ .strict();
267
+ /**
268
+ * What the edge records about the session CALL. Strict for the reason
269
+ * `clientRequestMetadataSchema` is: this object is where an IP would be added.
270
+ */
271
+ exports.realtimeClientMetadataSchema = zod_1.z
272
+ .object({
273
+ /** The public path, e.g. `/v1/realtime`. */
274
+ endpoint: zod_1.z.string().min(1).max(256),
275
+ clientSessionId: zod_1.z.string().min(1).max(128).optional(),
276
+ receivedAt: identifiers_1.inferenceTimestampSchema,
277
+ labels: zod_1.z.record(zod_1.z.string().max(256)).optional(),
278
+ })
279
+ .strict();
280
+ /* -------------------------------------------------------------------------- */
281
+ /* The session request */
282
+ /* -------------------------------------------------------------------------- */
283
+ const modelLineOf = (reference) => {
284
+ const at = reference.indexOf("@");
285
+ return at === -1 ? reference : reference.slice(0, at);
286
+ };
287
+ /**
288
+ * The signed request that opens a session: the first frame of its connection.
289
+ */
290
+ exports.realtimeSessionRequestSchema = zod_1.z
291
+ .object({
292
+ schemaVersion: zod_1.z.literal(1),
293
+ attribution: attribution_1.inferenceAttributionSchema,
294
+ /** The model the caller named, pinned or not. Never a routing profile. */
295
+ modelReference: identifiers_1.modelReferenceSchema,
296
+ kind: catalogue_1.realtimeSessionKindSchema,
297
+ transport: catalogue_1.realtimeSessionTransportSchema,
298
+ config: exports.realtimeSessionConfigSchema,
299
+ limits: exports.realtimeSessionLimitsSchema,
300
+ client: exports.realtimeClientMetadataSchema,
301
+ routingPolicy: routingPolicy_1.routingPolicyReferenceSchema,
302
+ /** In preference order, tried only until one opens. All `same_model`. */
303
+ authorizedRoutes: zod_1.z.array(routingPolicy_1.authorizedRouteSchema).min(1),
304
+ })
305
+ .superRefine((request, ctx) => {
306
+ const { config } = request;
307
+ const issue = (path, message) => ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, path, message });
308
+ if (request.limits.idleTimeoutMs > request.limits.maxDurationMs) {
309
+ issue(["limits", "idleTimeoutMs"], "an idle timeout cannot outlast the session");
310
+ }
311
+ const producesAudio = config.outputModalities?.includes("audio") === true;
312
+ const hasTools = config.tools !== undefined && config.tools.length > 0;
313
+ switch (request.kind) {
314
+ case "conversation":
315
+ if (config.outputModalities === undefined) {
316
+ issue(["config", "outputModalities"], "a conversation names what its responses produce");
317
+ }
318
+ if (producesAudio && (config.voice === undefined || config.outputAudioFormat === undefined)) {
319
+ issue(["config"], "spoken responses need a voice and an output audio format");
320
+ }
321
+ if (config.translation !== undefined) {
322
+ issue(["config", "translation"], "only a translation session translates");
323
+ }
324
+ break;
325
+ case "transcription":
326
+ if (config.inputAudioTranscription === undefined) {
327
+ issue(["config", "inputAudioTranscription"], "a transcription session transcribes its input");
328
+ }
329
+ for (const field of ["outputModalities", "voice", "outputAudioFormat", "translation", "tools", "toolChoice", "maxOutputTokens"]) {
330
+ if (config[field] !== undefined) {
331
+ issue(["config", field], "a transcription session never responds");
332
+ }
333
+ }
334
+ if (config.turnDetection.type !== "none" && (config.turnDetection.createResponse || config.turnDetection.interruptResponse)) {
335
+ issue(["config", "turnDetection"], "a transcription session has no response to create or interrupt");
336
+ }
337
+ break;
338
+ case "translation":
339
+ if (config.translation === undefined || config.outputAudioFormat === undefined) {
340
+ issue(["config"], "a translation session names its target language and output audio format");
341
+ }
342
+ if (config.tools !== undefined || config.toolChoice !== undefined) {
343
+ issue(["config", "tools"], "a translation session calls no tools");
344
+ }
345
+ break;
346
+ }
347
+ if (config.toolChoice !== undefined && !hasTools) {
348
+ issue(["config", "toolChoice"], "a tool choice requires at least one tool definition");
349
+ }
350
+ if (config.tools !== undefined) {
351
+ const names = config.tools.map((tool) => tool.name);
352
+ if (new Set(names).size !== names.length) {
353
+ issue(["config", "tools"], "tool names must be unique within one session");
354
+ }
355
+ }
356
+ if (config.outputModalities !== undefined && new Set(config.outputModalities).size !== config.outputModalities.length) {
357
+ issue(["config", "outputModalities"], "each output modality is named at most once");
358
+ }
359
+ // A session is never substituted: the conversation it holds belongs to one
360
+ // model, so every route serves the line the caller named, and a pinned
361
+ // request is served on exactly the revision it pinned.
362
+ const line = modelLineOf(request.modelReference);
363
+ const pinned = request.modelReference.includes("@");
364
+ for (const [index, route] of request.authorizedRoutes.entries()) {
365
+ if (route.substitution !== "same_model" || modelLineOf(route.modelReference) !== line) {
366
+ issue(["authorizedRoutes", index], "every route of a session serves the model it named");
367
+ }
368
+ if (pinned && route.modelReference !== request.modelReference) {
369
+ issue(["authorizedRoutes", index, "modelReference"], "a pinned session is served on exactly the revision it pinned");
370
+ }
371
+ }
372
+ const deployments = request.authorizedRoutes.map((route) => route.deploymentId);
373
+ if (new Set(deployments).size !== deployments.length) {
374
+ issue(["authorizedRoutes"], "each deployment appears at most once in the authorized route list");
375
+ }
376
+ });
377
+ /* -------------------------------------------------------------------------- */
378
+ /* Conversation items */
379
+ /* -------------------------------------------------------------------------- */
380
+ /**
381
+ * One part of a conversation message.
382
+ *
383
+ * `input_audio` carries its audio inline (one bounded frame); `output_audio`
384
+ * never does — spoken output arrives as `output_audio.delta` events, and an
385
+ * item echoing it carries only the format and, once known, the transcript.
386
+ */
387
+ exports.realtimeContentPartSchema = zod_1.z.discriminatedUnion("type", [
388
+ zod_1.z.object({ type: zod_1.z.literal("input_text"), text: zod_1.z.string().max(1048576) }).strict(),
389
+ zod_1.z
390
+ .object({
391
+ type: zod_1.z.literal("input_audio"),
392
+ format: exports.realtimeAudioFormatSchema,
393
+ data: audioFrameSchema.optional(),
394
+ transcript: zod_1.z.string().optional(),
395
+ })
396
+ .strict(),
397
+ zod_1.z.object({ type: zod_1.z.literal("output_text"), text: zod_1.z.string() }).strict(),
398
+ zod_1.z
399
+ .object({
400
+ type: zod_1.z.literal("output_audio"),
401
+ format: exports.realtimeAudioFormatSchema,
402
+ transcript: zod_1.z.string().optional(),
403
+ })
404
+ .strict(),
405
+ ]);
406
+ const INPUT_PARTS = new Set(["input_text", "input_audio"]);
407
+ /**
408
+ * One item of the conversation a session holds.
409
+ *
410
+ * `itemId` is optional on an item a client CREATES (the server assigns one) and
411
+ * is always stated by the server on the events that carry an item.
412
+ */
413
+ exports.realtimeConversationItemSchema = zod_1.z
414
+ .discriminatedUnion("type", [
415
+ zod_1.z
416
+ .object({
417
+ type: zod_1.z.literal("message"),
418
+ itemId: exports.realtimeItemIdSchema.optional(),
419
+ role: zod_1.z.enum(["system", "user", "assistant"]),
420
+ content: zod_1.z.array(exports.realtimeContentPartSchema).min(1).max(64),
421
+ })
422
+ .strict(),
423
+ zod_1.z
424
+ .object({
425
+ type: zod_1.z.literal("function_call"),
426
+ itemId: exports.realtimeItemIdSchema.optional(),
427
+ callId: zod_1.z.string().min(1).max(128),
428
+ name: zod_1.z.string().min(1).max(128),
429
+ arguments: zod_1.z.string(),
430
+ })
431
+ .strict(),
432
+ zod_1.z
433
+ .object({
434
+ type: zod_1.z.literal("function_call_output"),
435
+ itemId: exports.realtimeItemIdSchema.optional(),
436
+ callId: zod_1.z.string().min(1).max(128),
437
+ output: zod_1.z.string().max(1048576),
438
+ })
439
+ .strict(),
440
+ ])
441
+ .superRefine((item, ctx) => {
442
+ if (item.type !== "message")
443
+ return;
444
+ for (const [index, part] of item.content.entries()) {
445
+ const isInput = INPUT_PARTS.has(part.type);
446
+ // A user or system turn is input; an assistant turn is output. A part on
447
+ // the wrong side is one every provider would silently ignore.
448
+ if (item.role === "assistant" ? isInput : !isInput) {
449
+ ctx.addIssue({
450
+ code: zod_1.z.ZodIssueCode.custom,
451
+ path: ["content", index, "type"],
452
+ message: `a ${item.role} message cannot carry ${part.type}`,
453
+ });
454
+ }
455
+ if (item.role === "system" && part.type !== "input_text") {
456
+ ctx.addIssue({
457
+ code: zod_1.z.ZodIssueCode.custom,
458
+ path: ["content", index, "type"],
459
+ message: "a system message is text",
460
+ });
461
+ }
462
+ }
463
+ });
464
+ /** Per-response overrides for `response.create`. Absent fields keep the session's. */
465
+ exports.realtimeResponseParametersSchema = zod_1.z
466
+ .object({
467
+ instructions: zod_1.z.string().max(32768).optional(),
468
+ outputModalities: zod_1.z.array(exports.realtimeOutputModalitySchema).min(1).max(2).optional(),
469
+ maxOutputTokens: zod_1.z.number().int().positive().safe().optional(),
470
+ toolChoice: request_1.toolChoiceSchema.optional(),
471
+ })
472
+ .strict();
473
+ /* -------------------------------------------------------------------------- */
474
+ /* Client commands */
475
+ /* -------------------------------------------------------------------------- */
476
+ const commandBase = {
477
+ /** See `version.ts`: each command is a whole message on the wire. */
478
+ schemaVersion: zod_1.z.literal(1),
479
+ requestId: identifiers_1.requestIdSchema,
480
+ commandId: exports.realtimeCommandIdSchema,
481
+ };
482
+ exports.realtimeSessionUpdateCommandSchema = zod_1.z.object({
483
+ ...commandBase,
484
+ type: zod_1.z.literal("session.update"),
485
+ config: exports.realtimeSessionConfigUpdateSchema,
486
+ });
487
+ exports.realtimeItemCreateCommandSchema = zod_1.z.object({
488
+ ...commandBase,
489
+ type: zod_1.z.literal("conversation.item.create"),
490
+ /** Insert after this item; absent appends. */
491
+ previousItemId: exports.realtimeItemIdSchema.optional(),
492
+ item: exports.realtimeConversationItemSchema,
493
+ });
494
+ exports.realtimeItemDeleteCommandSchema = zod_1.z.object({
495
+ ...commandBase,
496
+ type: zod_1.z.literal("conversation.item.delete"),
497
+ itemId: exports.realtimeItemIdSchema,
498
+ });
499
+ /**
500
+ * Cut an assistant's audio item at the point the listener actually heard, after
501
+ * an interruption, so the conversation the model remembers is the one that was
502
+ * spoken rather than the one that was generated.
503
+ */
504
+ exports.realtimeItemTruncateCommandSchema = zod_1.z.object({
505
+ ...commandBase,
506
+ type: zod_1.z.literal("conversation.item.truncate"),
507
+ itemId: exports.realtimeItemIdSchema,
508
+ contentIndex: zod_1.z.number().int().nonnegative().safe(),
509
+ audioEndMs: milliseconds,
510
+ });
511
+ exports.realtimeInputAudioAppendCommandSchema = zod_1.z.object({
512
+ ...commandBase,
513
+ type: zod_1.z.literal("input_audio.append"),
514
+ /** One bounded frame in the session's `inputAudioFormat`. */
515
+ data: audioFrameSchema,
516
+ });
517
+ exports.realtimeInputAudioCommitCommandSchema = zod_1.z.object({
518
+ ...commandBase,
519
+ type: zod_1.z.literal("input_audio.commit"),
520
+ });
521
+ exports.realtimeInputAudioClearCommandSchema = zod_1.z.object({
522
+ ...commandBase,
523
+ type: zod_1.z.literal("input_audio.clear"),
524
+ });
525
+ exports.realtimeResponseCreateCommandSchema = zod_1.z.object({
526
+ ...commandBase,
527
+ type: zod_1.z.literal("response.create"),
528
+ response: exports.realtimeResponseParametersSchema.optional(),
529
+ });
530
+ exports.realtimeResponseCancelCommandSchema = zod_1.z.object({
531
+ ...commandBase,
532
+ type: zod_1.z.literal("response.cancel"),
533
+ /** Absent cancels the response in progress, if any. */
534
+ responseId: exports.realtimeResponseIdSchema.optional(),
535
+ });
536
+ /**
537
+ * The first frame of a reconnection: replay every event after `afterSequence`.
538
+ * `-1` asks for every buffered event from the start.
539
+ */
540
+ exports.realtimeSessionResumeCommandSchema = zod_1.z.object({
541
+ ...commandBase,
542
+ type: zod_1.z.literal("session.resume"),
543
+ afterSequence: zod_1.z.number().int().min(-1).safe(),
544
+ });
545
+ exports.realtimeSessionCloseCommandSchema = zod_1.z.object({
546
+ ...commandBase,
547
+ type: zod_1.z.literal("session.close"),
548
+ });
549
+ /** Every command a client can send. */
550
+ exports.realtimeClientCommandSchema = zod_1.z.discriminatedUnion("type", [
551
+ exports.realtimeSessionUpdateCommandSchema,
552
+ exports.realtimeItemCreateCommandSchema,
553
+ exports.realtimeItemDeleteCommandSchema,
554
+ exports.realtimeItemTruncateCommandSchema,
555
+ exports.realtimeInputAudioAppendCommandSchema,
556
+ exports.realtimeInputAudioCommitCommandSchema,
557
+ exports.realtimeInputAudioClearCommandSchema,
558
+ exports.realtimeResponseCreateCommandSchema,
559
+ exports.realtimeResponseCancelCommandSchema,
560
+ exports.realtimeSessionResumeCommandSchema,
561
+ exports.realtimeSessionCloseCommandSchema,
562
+ ]);
563
+ /* -------------------------------------------------------------------------- */
564
+ /* Server events */
565
+ /* -------------------------------------------------------------------------- */
566
+ const eventBase = {
567
+ /** See `version.ts`: each event is a whole message on the wire. */
568
+ schemaVersion: zod_1.z.literal(1),
569
+ requestId: identifiers_1.requestIdSchema,
570
+ /** One monotonic sequence per session, from 0; what makes a replay detectable. */
571
+ sequence: sequenceSchema,
572
+ /** The command this event answers, when it answers one. */
573
+ commandId: exports.realtimeCommandIdSchema.optional(),
574
+ };
575
+ /**
576
+ * The session opened. Names the revision-pinned model, the serving provider and
577
+ * the exact deployment — fixed for the rest of the session — and the effective
578
+ * configuration and limits. `deploymentId` is for settlement: like the usage
579
+ * event's, it is not rendered to the customer.
580
+ */
581
+ exports.realtimeSessionCreatedEventSchema = zod_1.z.object({
582
+ ...eventBase,
583
+ type: zod_1.z.literal("session.created"),
584
+ resolvedModelReference: identifiers_1.modelReferenceSchema,
585
+ servingProvider: identifiers_1.inferenceProviderSlugSchema,
586
+ deploymentId: identifiers_1.deploymentIdSchema,
587
+ kind: catalogue_1.realtimeSessionKindSchema,
588
+ config: exports.realtimeSessionConfigSchema,
589
+ limits: exports.realtimeSessionLimitsSchema,
590
+ resumeWindowMs: zod_1.z.number().int().nonnegative().max(exports.MAX_REALTIME_RESUME_WINDOW_MS),
591
+ startedAt: identifiers_1.inferenceTimestampSchema,
592
+ expiresAt: identifiers_1.inferenceTimestampSchema,
593
+ });
594
+ exports.realtimeSessionUpdatedEventSchema = zod_1.z.object({
595
+ ...eventBase,
596
+ type: zod_1.z.literal("session.updated"),
597
+ config: exports.realtimeSessionConfigSchema,
598
+ });
599
+ exports.realtimeSessionResumedEventSchema = zod_1.z.object({
600
+ ...eventBase,
601
+ type: zod_1.z.literal("session.resumed"),
602
+ afterSequence: zod_1.z.number().int().min(-1).safe(),
603
+ });
604
+ /** Sent before a command is applied. `duplicate` means it was not applied again. */
605
+ exports.realtimeCommandAcceptedEventSchema = zod_1.z.object({
606
+ ...eventBase,
607
+ type: zod_1.z.literal("command.accepted"),
608
+ commandId: exports.realtimeCommandIdSchema,
609
+ duplicate: zod_1.z.boolean(),
610
+ });
611
+ exports.realtimeItemAddedEventSchema = zod_1.z.object({
612
+ ...eventBase,
613
+ type: zod_1.z.literal("conversation.item.added"),
614
+ itemId: exports.realtimeItemIdSchema,
615
+ previousItemId: exports.realtimeItemIdSchema.optional(),
616
+ item: exports.realtimeConversationItemSchema,
617
+ });
618
+ exports.realtimeItemDoneEventSchema = zod_1.z.object({
619
+ ...eventBase,
620
+ type: zod_1.z.literal("conversation.item.done"),
621
+ itemId: exports.realtimeItemIdSchema,
622
+ item: exports.realtimeConversationItemSchema,
623
+ });
624
+ exports.realtimeItemDeletedEventSchema = zod_1.z.object({
625
+ ...eventBase,
626
+ type: zod_1.z.literal("conversation.item.deleted"),
627
+ itemId: exports.realtimeItemIdSchema,
628
+ });
629
+ exports.realtimeItemTruncatedEventSchema = zod_1.z.object({
630
+ ...eventBase,
631
+ type: zod_1.z.literal("conversation.item.truncated"),
632
+ itemId: exports.realtimeItemIdSchema,
633
+ contentIndex: zod_1.z.number().int().nonnegative().safe(),
634
+ audioEndMs: milliseconds,
635
+ });
636
+ /**
637
+ * Voice activity began. With `interruptResponse`, a response in progress is
638
+ * cancelled by it: this event is the interruption, and the client should stop
639
+ * playback and truncate what was not heard.
640
+ */
641
+ exports.realtimeSpeechStartedEventSchema = zod_1.z.object({
642
+ ...eventBase,
643
+ type: zod_1.z.literal("input_audio.speech_started"),
644
+ itemId: exports.realtimeItemIdSchema,
645
+ audioStartMs: milliseconds,
646
+ });
647
+ exports.realtimeSpeechStoppedEventSchema = zod_1.z.object({
648
+ ...eventBase,
649
+ type: zod_1.z.literal("input_audio.speech_stopped"),
650
+ itemId: exports.realtimeItemIdSchema,
651
+ audioEndMs: milliseconds,
652
+ });
653
+ exports.realtimeInputAudioCommittedEventSchema = zod_1.z.object({
654
+ ...eventBase,
655
+ type: zod_1.z.literal("input_audio.committed"),
656
+ itemId: exports.realtimeItemIdSchema,
657
+ previousItemId: exports.realtimeItemIdSchema.optional(),
658
+ });
659
+ exports.realtimeInputAudioClearedEventSchema = zod_1.z.object({
660
+ ...eventBase,
661
+ type: zod_1.z.literal("input_audio.cleared"),
662
+ });
663
+ exports.realtimeResponseCreatedEventSchema = zod_1.z.object({
664
+ ...eventBase,
665
+ type: zod_1.z.literal("response.created"),
666
+ responseId: exports.realtimeResponseIdSchema,
667
+ });
668
+ /** One bounded frame of spoken output, in the session's `outputAudioFormat`. */
669
+ exports.realtimeOutputAudioDeltaEventSchema = zod_1.z.object({
670
+ ...eventBase,
671
+ type: zod_1.z.literal("output_audio.delta"),
672
+ responseId: exports.realtimeResponseIdSchema,
673
+ itemId: exports.realtimeItemIdSchema,
674
+ contentIndex: zod_1.z.number().int().nonnegative().safe(),
675
+ format: exports.realtimeAudioFormatSchema,
676
+ data: audioFrameSchema,
677
+ });
678
+ exports.realtimeOutputAudioDoneEventSchema = zod_1.z.object({
679
+ ...eventBase,
680
+ type: zod_1.z.literal("output_audio.done"),
681
+ responseId: exports.realtimeResponseIdSchema,
682
+ itemId: exports.realtimeItemIdSchema,
683
+ contentIndex: zod_1.z.number().int().nonnegative().safe(),
684
+ });
685
+ /**
686
+ * Which audio a transcript describes: the caller's (`input_audio`) or the
687
+ * model's own speech (`output_audio`). Neither is answer text.
688
+ */
689
+ exports.realtimeTranscriptSourceSchema = zod_1.z.enum(["input_audio", "output_audio"]);
690
+ exports.realtimeTranscriptDeltaEventSchema = zod_1.z.object({
691
+ ...eventBase,
692
+ type: zod_1.z.literal("transcript.delta"),
693
+ source: exports.realtimeTranscriptSourceSchema,
694
+ itemId: exports.realtimeItemIdSchema,
695
+ contentIndex: zod_1.z.number().int().nonnegative().safe(),
696
+ /** Present for `output_audio`: the response whose speech this is. */
697
+ responseId: exports.realtimeResponseIdSchema.optional(),
698
+ text: zod_1.z.string(),
699
+ });
700
+ exports.realtimeTranscriptDoneEventSchema = zod_1.z.object({
701
+ ...eventBase,
702
+ type: zod_1.z.literal("transcript.done"),
703
+ source: exports.realtimeTranscriptSourceSchema,
704
+ itemId: exports.realtimeItemIdSchema,
705
+ contentIndex: zod_1.z.number().int().nonnegative().safe(),
706
+ responseId: exports.realtimeResponseIdSchema.optional(),
707
+ transcript: zod_1.z.string(),
708
+ });
709
+ /** Written output of a response (`outputModalities` including `text`). */
710
+ exports.realtimeTextDeltaEventSchema = zod_1.z.object({
711
+ ...eventBase,
712
+ type: zod_1.z.literal("text.delta"),
713
+ responseId: exports.realtimeResponseIdSchema,
714
+ itemId: exports.realtimeItemIdSchema,
715
+ contentIndex: zod_1.z.number().int().nonnegative().safe(),
716
+ text: zod_1.z.string(),
717
+ });
718
+ /** A tool call being streamed; same accumulation rules as the one-shot event. */
719
+ exports.realtimeToolCallEventSchema = zod_1.z.object({
720
+ ...eventBase,
721
+ type: zod_1.z.literal("tool_call"),
722
+ responseId: exports.realtimeResponseIdSchema,
723
+ itemId: exports.realtimeItemIdSchema,
724
+ toolCallId: zod_1.z.string().min(1).max(128),
725
+ name: zod_1.z.string().min(1).max(128).optional(),
726
+ argumentsDelta: zod_1.z.string().optional(),
727
+ complete: zod_1.z.boolean(),
728
+ });
729
+ /** How a response ended. */
730
+ exports.realtimeResponseStatusSchema = zod_1.z.enum([
731
+ "completed",
732
+ "cancelled",
733
+ "incomplete",
734
+ "failed",
735
+ ]);
736
+ /**
737
+ * A response ended, with the units it consumed — measurement evidence, as on
738
+ * the one-shot usage event. A cancelled or failed response still consumed what
739
+ * it consumed, so its units are reported too.
740
+ */
741
+ exports.realtimeResponseDoneEventSchema = zod_1.z.object({
742
+ ...eventBase,
743
+ type: zod_1.z.literal("response.done"),
744
+ responseId: exports.realtimeResponseIdSchema,
745
+ status: exports.realtimeResponseStatusSchema,
746
+ finishReason: streamEvents_1.inferenceFinishReasonSchema.optional(),
747
+ deploymentId: identifiers_1.deploymentIdSchema,
748
+ units: zod_1.z.array(money_1.usageQuantitySchema),
749
+ usageSource: money_1.usageSourceSchema,
750
+ });
751
+ /**
752
+ * An error. `fatal: false` refuses one command and the session continues;
753
+ * `fatal: true` is followed by `session.closed` and nothing else.
754
+ */
755
+ exports.realtimeErrorEventSchema = zod_1.z.object({
756
+ ...eventBase,
757
+ type: zod_1.z.literal("error"),
758
+ fatal: zod_1.z.boolean(),
759
+ error: errors_1.inferenceErrorSchema,
760
+ });
761
+ /** Why a session ended. */
762
+ exports.realtimeSessionCloseReasonSchema = zod_1.z.enum([
763
+ "client_closed",
764
+ "max_duration",
765
+ "idle_timeout",
766
+ "limit_exceeded",
767
+ "resume_expired",
768
+ "upstream_closed",
769
+ "upstream_error",
770
+ "no_route_available",
771
+ "server_shutdown",
772
+ ]);
773
+ /**
774
+ * The terminal event. Carries the session's total units; `deploymentId` is
775
+ * absent only when no route ever opened, in which case nothing was consumed.
776
+ */
777
+ exports.realtimeSessionClosedEventSchema = zod_1.z.object({
778
+ ...eventBase,
779
+ type: zod_1.z.literal("session.closed"),
780
+ reason: exports.realtimeSessionCloseReasonSchema,
781
+ deploymentId: identifiers_1.deploymentIdSchema.optional(),
782
+ units: zod_1.z.array(money_1.usageQuantitySchema),
783
+ usageSource: money_1.usageSourceSchema,
784
+ closedAt: identifiers_1.inferenceTimestampSchema,
785
+ });
786
+ /** Every event a session can emit. */
787
+ exports.realtimeServerEventSchema = zod_1.z
788
+ .discriminatedUnion("type", [
789
+ exports.realtimeSessionCreatedEventSchema,
790
+ exports.realtimeSessionUpdatedEventSchema,
791
+ exports.realtimeSessionResumedEventSchema,
792
+ exports.realtimeCommandAcceptedEventSchema,
793
+ exports.realtimeItemAddedEventSchema,
794
+ exports.realtimeItemDoneEventSchema,
795
+ exports.realtimeItemDeletedEventSchema,
796
+ exports.realtimeItemTruncatedEventSchema,
797
+ exports.realtimeSpeechStartedEventSchema,
798
+ exports.realtimeSpeechStoppedEventSchema,
799
+ exports.realtimeInputAudioCommittedEventSchema,
800
+ exports.realtimeInputAudioClearedEventSchema,
801
+ exports.realtimeResponseCreatedEventSchema,
802
+ exports.realtimeOutputAudioDeltaEventSchema,
803
+ exports.realtimeOutputAudioDoneEventSchema,
804
+ exports.realtimeTranscriptDeltaEventSchema,
805
+ exports.realtimeTranscriptDoneEventSchema,
806
+ exports.realtimeTextDeltaEventSchema,
807
+ exports.realtimeToolCallEventSchema,
808
+ exports.realtimeResponseDoneEventSchema,
809
+ exports.realtimeErrorEventSchema,
810
+ exports.realtimeSessionClosedEventSchema,
811
+ ])
812
+ .superRefine((event, ctx) => {
813
+ // Each unit is reported once, as a total — the rule every usage record in
814
+ // this contract holds. Checked on the union because a discriminated union's
815
+ // options must stay plain objects.
816
+ if (event.type !== "response.done" && event.type !== "session.closed")
817
+ return;
818
+ const units = event.units.map((quantity) => quantity.unit);
819
+ if (new Set(units).size !== units.length) {
820
+ ctx.addIssue({
821
+ code: zod_1.z.ZodIssueCode.custom,
822
+ path: ["units"],
823
+ message: "each unit is reported at most once",
824
+ });
825
+ }
826
+ });