@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.
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/index.js +72 -4
- package/dist/cjs/inference/catalogue.js +105 -2
- package/dist/cjs/inference/money.js +23 -0
- package/dist/cjs/inference/realtime.js +826 -0
- package/dist/cjs/inference/request.js +34 -11
- package/dist/cjs/inference/streamEvents.js +7 -1
- package/dist/cjs/inference/version.js +1 -1
- package/dist/cjs/stickers.js +151 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/index.js +6 -2
- package/dist/esm/inference/catalogue.js +104 -1
- package/dist/esm/inference/money.js +23 -0
- package/dist/esm/inference/realtime.js +822 -0
- package/dist/esm/inference/request.js +34 -11
- package/dist/esm/inference/streamEvents.js +7 -1
- package/dist/esm/inference/version.js +1 -1
- package/dist/esm/stickers.js +148 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/index.d.ts +7 -4
- package/dist/types/inference/catalogue.d.ts +299 -10
- package/dist/types/inference/entitlement.d.ts +6 -6
- package/dist/types/inference/inbox.d.ts +2 -2
- package/dist/types/inference/modelDocumentation.d.ts +146 -2
- package/dist/types/inference/money.d.ts +28 -8
- package/dist/types/inference/priceVersion.d.ts +12 -12
- package/dist/types/inference/realtime.d.ts +8012 -0
- package/dist/types/inference/request.d.ts +51 -0
- package/dist/types/inference/routingPolicy.d.ts +7 -7
- package/dist/types/inference/streamEvents.d.ts +28 -16
- package/dist/types/inference/usage.d.ts +28 -28
- package/dist/types/inference/version.d.ts +1 -1
- package/dist/types/stickers.d.ts +1049 -0
- 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
|
+
});
|