@kaana.ai/contracts 0.0.0-stage → 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/LICENSE +673 -0
- package/NOTICE +21 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +29 -0
- package/dist/internal/attribution.d.ts +116 -0
- package/dist/internal/attribution.js +82 -0
- package/dist/internal/canonical.d.ts +31 -0
- package/dist/internal/canonical.js +57 -0
- package/dist/internal/catalogue.d.ts +1059 -0
- package/dist/internal/catalogue.js +389 -0
- package/dist/internal/credentialControl.d.ts +329 -0
- package/dist/internal/credentialControl.js +256 -0
- package/dist/internal/decisions.d.ts +245 -0
- package/dist/internal/decisions.js +69 -0
- package/dist/internal/economics.d.ts +27 -0
- package/dist/internal/economics.js +26 -0
- package/dist/internal/embeddings.d.ts +164 -0
- package/dist/internal/embeddings.js +59 -0
- package/dist/internal/errors.d.ts +185 -0
- package/dist/internal/errors.js +211 -0
- package/dist/internal/grant.d.ts +699 -0
- package/dist/internal/grant.js +445 -0
- package/dist/internal/identifiers.d.ts +63 -0
- package/dist/internal/identifiers.js +65 -0
- package/dist/internal/privateAutoExecution.d.ts +158 -0
- package/dist/internal/privateAutoExecution.js +188 -0
- package/dist/internal/realtime.d.ts +1005 -0
- package/dist/internal/realtime.js +187 -0
- package/dist/internal/request.d.ts +1079 -0
- package/dist/internal/request.js +404 -0
- package/dist/internal/routing.d.ts +90 -0
- package/dist/internal/routing.js +139 -0
- package/dist/internal/scopedExecution.d.ts +74 -0
- package/dist/internal/scopedExecution.js +61 -0
- package/dist/internal/settlement.d.ts +205 -0
- package/dist/internal/settlement.js +71 -0
- package/dist/internal/streamEvents.d.ts +422 -0
- package/dist/internal/streamEvents.js +171 -0
- package/dist/internal/usage.d.ts +127 -0
- package/dist/internal/usage.js +109 -0
- package/dist/internal.d.ts +30 -0
- package/dist/internal.js +30 -0
- package/dist/public/audio.d.ts +347 -0
- package/dist/public/audio.js +236 -0
- package/dist/public/audioFormats.d.ts +64 -0
- package/dist/public/audioFormats.js +60 -0
- package/dist/public/catalogue.d.ts +914 -0
- package/dist/public/catalogue.js +516 -0
- package/dist/public/chatCompletions.d.ts +381 -0
- package/dist/public/chatCompletions.js +321 -0
- package/dist/public/decisions.d.ts +182 -0
- package/dist/public/decisions.js +257 -0
- package/dist/public/embeddings.d.ts +34 -0
- package/dist/public/embeddings.js +66 -0
- package/dist/public/errors.d.ts +195 -0
- package/dist/public/errors.js +274 -0
- package/dist/public/generations.d.ts +330 -0
- package/dist/public/generations.js +121 -0
- package/dist/public/identifiers.d.ts +139 -0
- package/dist/public/identifiers.js +160 -0
- package/dist/public/images.d.ts +36 -0
- package/dist/public/images.js +36 -0
- package/dist/public/messages.d.ts +297 -0
- package/dist/public/messages.js +251 -0
- package/dist/public/money.d.ts +308 -0
- package/dist/public/money.js +237 -0
- package/dist/public/policy.d.ts +19 -0
- package/dist/public/policy.js +20 -0
- package/dist/public/realtime.d.ts +2006 -0
- package/dist/public/realtime.js +797 -0
- package/dist/public/responses.d.ts +253 -0
- package/dist/public/responses.js +95 -0
- package/dist/public/streamEvents.d.ts +372 -0
- package/dist/public/streamEvents.js +157 -0
- package/dist/public/version.d.ts +184 -0
- package/dist/public/version.js +184 -0
- package/openapi.json +6967 -0
- package/package.json +44 -4
- package/README.md +0 -3
|
@@ -0,0 +1,797 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Realtime sessions as a public client speaks them (`GET /v1/realtime`).
|
|
3
|
+
*
|
|
4
|
+
* Moved from `@oxy.so/contracts` 4.12.0 (`src/inference/realtime.ts`) by Kaana ADR 0002.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { realtimeAudioFormatSchema } from './audioFormats.js';
|
|
8
|
+
import { realtimeSessionKindSchema, realtimeSessionTransportSchema, } from './catalogue.js';
|
|
9
|
+
import { inferenceTimestampSchema, modelReferenceSchema, PADDED_BASE64_PATTERN, requestIdSchema, } from './identifiers.js';
|
|
10
|
+
import { publicErrorSchema } from './errors.js';
|
|
11
|
+
import { requestLabelsSchema, toolChoiceSchema, toolDefinitionSchema } from './messages.js';
|
|
12
|
+
import { usageQuantitySchema, usageSourceSchema } from './money.js';
|
|
13
|
+
import { inferenceFinishReasonSchema } from './streamEvents.js';
|
|
14
|
+
/**
|
|
15
|
+
* The most base64 characters one audio frame may carry: 49 152 bytes of audio,
|
|
16
|
+
* about one second of 24 kHz mono PCM16. Bounded so no frame is large enough to
|
|
17
|
+
* stall a connection, and so a byte cap can be enforced as audio arrives rather
|
|
18
|
+
* than after it has been buffered.
|
|
19
|
+
*/
|
|
20
|
+
export const MAX_REALTIME_AUDIO_FRAME_BASE64_LENGTH = 65_536;
|
|
21
|
+
/** The longest a session may be signed for: one hour. */
|
|
22
|
+
export const MAX_REALTIME_SESSION_DURATION_MS = 3_600_000;
|
|
23
|
+
/**
|
|
24
|
+
* The most audio a session may be signed for in EACH direction: one hour of
|
|
25
|
+
* 24 kHz mono PCM16 (48 000 bytes per second), the densest format a session
|
|
26
|
+
* carries.
|
|
27
|
+
*/
|
|
28
|
+
export const MAX_REALTIME_SESSION_AUDIO_BYTES = 172_800_000;
|
|
29
|
+
/** The longest a dropped connection may be resumed after: one minute. */
|
|
30
|
+
export const MAX_REALTIME_RESUME_WINDOW_MS = 60_000;
|
|
31
|
+
/** A client-chosen command identity, unique within one session. */
|
|
32
|
+
export const realtimeCommandIdSchema = z.string().min(1).max(128);
|
|
33
|
+
/** A conversation item's identity within one session. */
|
|
34
|
+
export const realtimeItemIdSchema = z.string().min(1).max(128);
|
|
35
|
+
/** A response's identity within one session. */
|
|
36
|
+
export const realtimeResponseIdSchema = z.string().min(1).max(128);
|
|
37
|
+
/** What a session's responses produce. */
|
|
38
|
+
export const realtimeOutputModalitySchema = z.enum(['text', 'audio']);
|
|
39
|
+
const audioFrameSchema = z
|
|
40
|
+
.string()
|
|
41
|
+
.min(4)
|
|
42
|
+
.max(MAX_REALTIME_AUDIO_FRAME_BASE64_LENGTH)
|
|
43
|
+
.regex(PADDED_BASE64_PATTERN);
|
|
44
|
+
const sequenceSchema = z.number().int().nonnegative().safe();
|
|
45
|
+
const milliseconds = z.number().int().nonnegative().safe();
|
|
46
|
+
/**
|
|
47
|
+
* How the end of a user's turn is detected.
|
|
48
|
+
*
|
|
49
|
+
* `none` means the client commits input audio itself. The two VAD forms name
|
|
50
|
+
* `createResponse` and `interruptResponse` explicitly rather than defaulting
|
|
51
|
+
* them: whether the model answers on its own, and whether new speech cuts it
|
|
52
|
+
* off, are the two behaviours a caller most needs to have chosen, and a default
|
|
53
|
+
* would choose them silently — and differently per provider.
|
|
54
|
+
*/
|
|
55
|
+
export const realtimeTurnDetectionSchema = z.discriminatedUnion('type', [
|
|
56
|
+
z.object({ type: z.literal('none') }).strict(),
|
|
57
|
+
z
|
|
58
|
+
.object({
|
|
59
|
+
type: z.literal('server_vad'),
|
|
60
|
+
threshold: z.number().min(0).max(1).optional(),
|
|
61
|
+
prefixPaddingMs: z.number().int().min(0).max(5_000).optional(),
|
|
62
|
+
silenceDurationMs: z.number().int().min(0).max(10_000).optional(),
|
|
63
|
+
createResponse: z.boolean(),
|
|
64
|
+
interruptResponse: z.boolean(),
|
|
65
|
+
})
|
|
66
|
+
.strict(),
|
|
67
|
+
z
|
|
68
|
+
.object({
|
|
69
|
+
type: z.literal('semantic_vad'),
|
|
70
|
+
eagerness: z.enum(['low', 'medium', 'high', 'auto']),
|
|
71
|
+
createResponse: z.boolean(),
|
|
72
|
+
interruptResponse: z.boolean(),
|
|
73
|
+
})
|
|
74
|
+
.strict(),
|
|
75
|
+
]);
|
|
76
|
+
/**
|
|
77
|
+
* Transcription of the caller's input audio. Presence enables it; the
|
|
78
|
+
* transcription model is the deployment's own, declared where the deployment
|
|
79
|
+
* is, never chosen by a string here.
|
|
80
|
+
*
|
|
81
|
+
* In a `conversation` it is served only by a model whose catalogue entry says
|
|
82
|
+
* so (`capabilities.realtime.inputAudioTranscription`, contract set 4.1.0),
|
|
83
|
+
* and each transcribed turn arrives as `transcript.delta` / `transcript.done`
|
|
84
|
+
* with `source: 'input_audio'`. What it costs is settled with the session:
|
|
85
|
+
* where the provider bills the transcription apart from the session model, as
|
|
86
|
+
* `input_transcription_milliseconds` (see `USAGE_UNITS`); where the session's
|
|
87
|
+
* own price includes it, as nothing more.
|
|
88
|
+
*
|
|
89
|
+
* `language`, when given, is one of the model's published `audio.languages`.
|
|
90
|
+
*/
|
|
91
|
+
export const realtimeInputTranscriptionSchema = z
|
|
92
|
+
.object({
|
|
93
|
+
/** A BCP 47 tag, when the caller knows the language. */
|
|
94
|
+
language: z.string().min(2).max(35).optional(),
|
|
95
|
+
prompt: z.string().max(4_096).optional(),
|
|
96
|
+
})
|
|
97
|
+
.strict();
|
|
98
|
+
/** The target of a `translation` session. */
|
|
99
|
+
export const realtimeTranslationSchema = z
|
|
100
|
+
.object({
|
|
101
|
+
/** A BCP 47 tag. */
|
|
102
|
+
targetLanguage: z.string().min(2).max(35),
|
|
103
|
+
})
|
|
104
|
+
.strict();
|
|
105
|
+
/**
|
|
106
|
+
* A session's full configuration, as signed at open and as echoed back by
|
|
107
|
+
* `session.created` and `session.updated`.
|
|
108
|
+
*
|
|
109
|
+
* `inputAudioFormat` and `turnDetection` are required because every session
|
|
110
|
+
* kind consumes audio and has to know when a turn ends; nothing else is,
|
|
111
|
+
* because each kind needs a different subset — see the refinement on
|
|
112
|
+
* {@link realtimeSessionRequestSchema}.
|
|
113
|
+
*/
|
|
114
|
+
export const realtimeSessionConfigSchema = z
|
|
115
|
+
.object({
|
|
116
|
+
instructions: z.string().max(32_768).optional(),
|
|
117
|
+
outputModalities: z.array(realtimeOutputModalitySchema).min(1).max(2).optional(),
|
|
118
|
+
voice: z.string().min(1).max(64).optional(),
|
|
119
|
+
inputAudioFormat: realtimeAudioFormatSchema,
|
|
120
|
+
outputAudioFormat: realtimeAudioFormatSchema.optional(),
|
|
121
|
+
turnDetection: realtimeTurnDetectionSchema,
|
|
122
|
+
inputAudioTranscription: realtimeInputTranscriptionSchema.optional(),
|
|
123
|
+
translation: realtimeTranslationSchema.optional(),
|
|
124
|
+
tools: z.array(toolDefinitionSchema).max(128).optional(),
|
|
125
|
+
toolChoice: toolChoiceSchema.optional(),
|
|
126
|
+
temperature: z.number().min(0).max(2).optional(),
|
|
127
|
+
/** Per response, not per session: the session's ceiling is `limits`. */
|
|
128
|
+
maxOutputTokens: z.number().int().positive().safe().optional(),
|
|
129
|
+
})
|
|
130
|
+
.strict();
|
|
131
|
+
/**
|
|
132
|
+
* What `session.update` may change mid-session.
|
|
133
|
+
*
|
|
134
|
+
* The session's kind, its audio formats and its voice are fixed at open —
|
|
135
|
+
* providers refuse to change a voice once it has spoken, and a format change
|
|
136
|
+
* mid-stream would make every buffered frame ambiguous — so they are not fields
|
|
137
|
+
* here, and the strict parse refuses them rather than ignoring them.
|
|
138
|
+
*/
|
|
139
|
+
export const realtimeSessionConfigUpdateSchema = z
|
|
140
|
+
.object({
|
|
141
|
+
instructions: z.string().max(32_768).optional(),
|
|
142
|
+
outputModalities: z.array(realtimeOutputModalitySchema).min(1).max(2).optional(),
|
|
143
|
+
turnDetection: realtimeTurnDetectionSchema.optional(),
|
|
144
|
+
inputAudioTranscription: realtimeInputTranscriptionSchema.optional(),
|
|
145
|
+
tools: z.array(toolDefinitionSchema).max(128).optional(),
|
|
146
|
+
toolChoice: toolChoiceSchema.optional(),
|
|
147
|
+
temperature: z.number().min(0).max(2).optional(),
|
|
148
|
+
maxOutputTokens: z.number().int().positive().safe().optional(),
|
|
149
|
+
})
|
|
150
|
+
.strict()
|
|
151
|
+
.superRefine((update, ctx) => {
|
|
152
|
+
if (Object.keys(update).length === 0) {
|
|
153
|
+
ctx.addIssue({
|
|
154
|
+
code: 'custom',
|
|
155
|
+
message: 'a session update changes at least one field',
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
/**
|
|
160
|
+
* The ceilings Oxy signed the session for. The data plane enforces each one and
|
|
161
|
+
* closes the session (`limit_exceeded`, `max_duration`, `idle_timeout`) rather
|
|
162
|
+
* than exceeding it: these bound what the session can cost, which is what Oxy's
|
|
163
|
+
* spend reservation was sized against.
|
|
164
|
+
*/
|
|
165
|
+
export const realtimeSessionLimitsSchema = z
|
|
166
|
+
.object({
|
|
167
|
+
maxDurationMs: z.number().int().min(1_000).max(MAX_REALTIME_SESSION_DURATION_MS),
|
|
168
|
+
idleTimeoutMs: z.number().int().min(1_000).max(MAX_REALTIME_SESSION_DURATION_MS),
|
|
169
|
+
maxInputAudioBytes: z.number().int().positive().max(MAX_REALTIME_SESSION_AUDIO_BYTES),
|
|
170
|
+
maxOutputAudioBytes: z.number().int().positive().max(MAX_REALTIME_SESSION_AUDIO_BYTES),
|
|
171
|
+
maxResponses: z.number().int().positive().max(10_000),
|
|
172
|
+
})
|
|
173
|
+
.strict();
|
|
174
|
+
/**
|
|
175
|
+
* One part of a conversation message.
|
|
176
|
+
*
|
|
177
|
+
* `input_audio` carries its audio inline (one bounded frame); `output_audio`
|
|
178
|
+
* never does — spoken output arrives as `output_audio.delta` events, and an
|
|
179
|
+
* item echoing it carries only the format and, once known, the transcript.
|
|
180
|
+
*/
|
|
181
|
+
export const realtimeContentPartSchema = z.discriminatedUnion('type', [
|
|
182
|
+
z.object({ type: z.literal('input_text'), text: z.string().max(1_048_576) }).strict(),
|
|
183
|
+
z
|
|
184
|
+
.object({
|
|
185
|
+
type: z.literal('input_audio'),
|
|
186
|
+
format: realtimeAudioFormatSchema,
|
|
187
|
+
data: audioFrameSchema.optional(),
|
|
188
|
+
transcript: z.string().optional(),
|
|
189
|
+
})
|
|
190
|
+
.strict(),
|
|
191
|
+
z.object({ type: z.literal('output_text'), text: z.string() }).strict(),
|
|
192
|
+
z
|
|
193
|
+
.object({
|
|
194
|
+
type: z.literal('output_audio'),
|
|
195
|
+
format: realtimeAudioFormatSchema,
|
|
196
|
+
transcript: z.string().optional(),
|
|
197
|
+
})
|
|
198
|
+
.strict(),
|
|
199
|
+
]);
|
|
200
|
+
const INPUT_PARTS = new Set(['input_text', 'input_audio']);
|
|
201
|
+
/**
|
|
202
|
+
* One item of the conversation a session holds.
|
|
203
|
+
*
|
|
204
|
+
* `itemId` is optional on an item a client CREATES (the server assigns one) and
|
|
205
|
+
* is always stated by the server on the events that carry an item.
|
|
206
|
+
*/
|
|
207
|
+
export const realtimeConversationItemSchema = z
|
|
208
|
+
.discriminatedUnion('type', [
|
|
209
|
+
z
|
|
210
|
+
.object({
|
|
211
|
+
type: z.literal('message'),
|
|
212
|
+
itemId: realtimeItemIdSchema.optional(),
|
|
213
|
+
role: z.enum(['system', 'user', 'assistant']),
|
|
214
|
+
content: z.array(realtimeContentPartSchema).min(1).max(64),
|
|
215
|
+
})
|
|
216
|
+
.strict(),
|
|
217
|
+
z
|
|
218
|
+
.object({
|
|
219
|
+
type: z.literal('function_call'),
|
|
220
|
+
itemId: realtimeItemIdSchema.optional(),
|
|
221
|
+
callId: z.string().min(1).max(128),
|
|
222
|
+
name: z.string().min(1).max(128),
|
|
223
|
+
arguments: z.string(),
|
|
224
|
+
})
|
|
225
|
+
.strict(),
|
|
226
|
+
z
|
|
227
|
+
.object({
|
|
228
|
+
type: z.literal('function_call_output'),
|
|
229
|
+
itemId: realtimeItemIdSchema.optional(),
|
|
230
|
+
callId: z.string().min(1).max(128),
|
|
231
|
+
output: z.string().max(1_048_576),
|
|
232
|
+
})
|
|
233
|
+
.strict(),
|
|
234
|
+
])
|
|
235
|
+
.superRefine((item, ctx) => {
|
|
236
|
+
if (item.type !== 'message')
|
|
237
|
+
return;
|
|
238
|
+
for (const [index, part] of item.content.entries()) {
|
|
239
|
+
const isInput = INPUT_PARTS.has(part.type);
|
|
240
|
+
// A user or system turn is input; an assistant turn is output. A part on
|
|
241
|
+
// the wrong side is one every provider would silently ignore.
|
|
242
|
+
if (item.role === 'assistant' ? isInput : !isInput) {
|
|
243
|
+
ctx.addIssue({
|
|
244
|
+
code: 'custom',
|
|
245
|
+
path: ['content', index, 'type'],
|
|
246
|
+
message: `a ${item.role} message cannot carry ${part.type}`,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
if (item.role === 'system' && part.type !== 'input_text') {
|
|
250
|
+
ctx.addIssue({
|
|
251
|
+
code: 'custom',
|
|
252
|
+
path: ['content', index, 'type'],
|
|
253
|
+
message: 'a system message is text',
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
/** Per-response overrides for `response.create`. Absent fields keep the session's. */
|
|
259
|
+
export const realtimeResponseParametersSchema = z
|
|
260
|
+
.object({
|
|
261
|
+
instructions: z.string().max(32_768).optional(),
|
|
262
|
+
outputModalities: z.array(realtimeOutputModalitySchema).min(1).max(2).optional(),
|
|
263
|
+
maxOutputTokens: z.number().int().positive().safe().optional(),
|
|
264
|
+
toolChoice: toolChoiceSchema.optional(),
|
|
265
|
+
})
|
|
266
|
+
.strict();
|
|
267
|
+
const commandBase = {
|
|
268
|
+
/** See `version.ts`: each command is a whole message on the wire. */
|
|
269
|
+
schemaVersion: z.literal(1),
|
|
270
|
+
requestId: requestIdSchema,
|
|
271
|
+
commandId: realtimeCommandIdSchema,
|
|
272
|
+
};
|
|
273
|
+
export const realtimeSessionUpdateCommandSchema = z.object({
|
|
274
|
+
...commandBase,
|
|
275
|
+
type: z.literal('session.update'),
|
|
276
|
+
config: realtimeSessionConfigUpdateSchema,
|
|
277
|
+
});
|
|
278
|
+
export const realtimeItemCreateCommandSchema = z.object({
|
|
279
|
+
...commandBase,
|
|
280
|
+
type: z.literal('conversation.item.create'),
|
|
281
|
+
/** Insert after this item; absent appends. */
|
|
282
|
+
previousItemId: realtimeItemIdSchema.optional(),
|
|
283
|
+
item: realtimeConversationItemSchema,
|
|
284
|
+
});
|
|
285
|
+
export const realtimeItemDeleteCommandSchema = z.object({
|
|
286
|
+
...commandBase,
|
|
287
|
+
type: z.literal('conversation.item.delete'),
|
|
288
|
+
itemId: realtimeItemIdSchema,
|
|
289
|
+
});
|
|
290
|
+
/**
|
|
291
|
+
* Cut an assistant's audio item at the point the listener actually heard, after
|
|
292
|
+
* an interruption, so the conversation the model remembers is the one that was
|
|
293
|
+
* spoken rather than the one that was generated.
|
|
294
|
+
*/
|
|
295
|
+
export const realtimeItemTruncateCommandSchema = z.object({
|
|
296
|
+
...commandBase,
|
|
297
|
+
type: z.literal('conversation.item.truncate'),
|
|
298
|
+
itemId: realtimeItemIdSchema,
|
|
299
|
+
contentIndex: z.number().int().nonnegative().safe(),
|
|
300
|
+
audioEndMs: milliseconds,
|
|
301
|
+
});
|
|
302
|
+
export const realtimeInputAudioAppendCommandSchema = z.object({
|
|
303
|
+
...commandBase,
|
|
304
|
+
type: z.literal('input_audio.append'),
|
|
305
|
+
/** One bounded frame in the session's `inputAudioFormat`. */
|
|
306
|
+
data: audioFrameSchema,
|
|
307
|
+
});
|
|
308
|
+
export const realtimeInputAudioCommitCommandSchema = z.object({
|
|
309
|
+
...commandBase,
|
|
310
|
+
type: z.literal('input_audio.commit'),
|
|
311
|
+
});
|
|
312
|
+
export const realtimeInputAudioClearCommandSchema = z.object({
|
|
313
|
+
...commandBase,
|
|
314
|
+
type: z.literal('input_audio.clear'),
|
|
315
|
+
});
|
|
316
|
+
export const realtimeResponseCreateCommandSchema = z.object({
|
|
317
|
+
...commandBase,
|
|
318
|
+
type: z.literal('response.create'),
|
|
319
|
+
response: realtimeResponseParametersSchema.optional(),
|
|
320
|
+
});
|
|
321
|
+
export const realtimeResponseCancelCommandSchema = z.object({
|
|
322
|
+
...commandBase,
|
|
323
|
+
type: z.literal('response.cancel'),
|
|
324
|
+
/** Absent cancels the response in progress, if any. */
|
|
325
|
+
responseId: realtimeResponseIdSchema.optional(),
|
|
326
|
+
});
|
|
327
|
+
/**
|
|
328
|
+
* The first frame of a reconnection: replay every event after `afterSequence`.
|
|
329
|
+
* `-1` asks for every buffered event from the start.
|
|
330
|
+
*/
|
|
331
|
+
export const realtimeSessionResumeCommandSchema = z.object({
|
|
332
|
+
...commandBase,
|
|
333
|
+
type: z.literal('session.resume'),
|
|
334
|
+
afterSequence: z.number().int().min(-1).safe(),
|
|
335
|
+
});
|
|
336
|
+
export const realtimeSessionCloseCommandSchema = z.object({
|
|
337
|
+
...commandBase,
|
|
338
|
+
type: z.literal('session.close'),
|
|
339
|
+
});
|
|
340
|
+
/** Every command a client can send. */
|
|
341
|
+
export const realtimeClientCommandSchema = z.discriminatedUnion('type', [
|
|
342
|
+
realtimeSessionUpdateCommandSchema,
|
|
343
|
+
realtimeItemCreateCommandSchema,
|
|
344
|
+
realtimeItemDeleteCommandSchema,
|
|
345
|
+
realtimeItemTruncateCommandSchema,
|
|
346
|
+
realtimeInputAudioAppendCommandSchema,
|
|
347
|
+
realtimeInputAudioCommitCommandSchema,
|
|
348
|
+
realtimeInputAudioClearCommandSchema,
|
|
349
|
+
realtimeResponseCreateCommandSchema,
|
|
350
|
+
realtimeResponseCancelCommandSchema,
|
|
351
|
+
realtimeSessionResumeCommandSchema,
|
|
352
|
+
realtimeSessionCloseCommandSchema,
|
|
353
|
+
]);
|
|
354
|
+
const eventBase = {
|
|
355
|
+
/** See `version.ts`: each event is a whole message on the wire. */
|
|
356
|
+
schemaVersion: z.literal(1),
|
|
357
|
+
requestId: requestIdSchema,
|
|
358
|
+
/** One monotonic sequence per session, from 0; what makes a replay detectable. */
|
|
359
|
+
sequence: sequenceSchema,
|
|
360
|
+
/** The command this event answers, when it answers one. */
|
|
361
|
+
commandId: realtimeCommandIdSchema.optional(),
|
|
362
|
+
};
|
|
363
|
+
export const realtimeSessionUpdatedEventSchema = z.object({
|
|
364
|
+
...eventBase,
|
|
365
|
+
type: z.literal('session.updated'),
|
|
366
|
+
config: realtimeSessionConfigSchema,
|
|
367
|
+
});
|
|
368
|
+
export const realtimeSessionResumedEventSchema = z.object({
|
|
369
|
+
...eventBase,
|
|
370
|
+
type: z.literal('session.resumed'),
|
|
371
|
+
afterSequence: z.number().int().min(-1).safe(),
|
|
372
|
+
});
|
|
373
|
+
/** Sent before a command is applied. `duplicate` means it was not applied again. */
|
|
374
|
+
export const realtimeCommandAcceptedEventSchema = z.object({
|
|
375
|
+
...eventBase,
|
|
376
|
+
type: z.literal('command.accepted'),
|
|
377
|
+
commandId: realtimeCommandIdSchema,
|
|
378
|
+
duplicate: z.boolean(),
|
|
379
|
+
});
|
|
380
|
+
export const realtimeItemAddedEventSchema = z.object({
|
|
381
|
+
...eventBase,
|
|
382
|
+
type: z.literal('conversation.item.added'),
|
|
383
|
+
itemId: realtimeItemIdSchema,
|
|
384
|
+
previousItemId: realtimeItemIdSchema.optional(),
|
|
385
|
+
item: realtimeConversationItemSchema,
|
|
386
|
+
});
|
|
387
|
+
export const realtimeItemDoneEventSchema = z.object({
|
|
388
|
+
...eventBase,
|
|
389
|
+
type: z.literal('conversation.item.done'),
|
|
390
|
+
itemId: realtimeItemIdSchema,
|
|
391
|
+
item: realtimeConversationItemSchema,
|
|
392
|
+
});
|
|
393
|
+
export const realtimeItemDeletedEventSchema = z.object({
|
|
394
|
+
...eventBase,
|
|
395
|
+
type: z.literal('conversation.item.deleted'),
|
|
396
|
+
itemId: realtimeItemIdSchema,
|
|
397
|
+
});
|
|
398
|
+
export const realtimeItemTruncatedEventSchema = z.object({
|
|
399
|
+
...eventBase,
|
|
400
|
+
type: z.literal('conversation.item.truncated'),
|
|
401
|
+
itemId: realtimeItemIdSchema,
|
|
402
|
+
contentIndex: z.number().int().nonnegative().safe(),
|
|
403
|
+
audioEndMs: milliseconds,
|
|
404
|
+
});
|
|
405
|
+
/**
|
|
406
|
+
* Voice activity began. With `interruptResponse`, a response in progress is
|
|
407
|
+
* cancelled by it: this event is the interruption, and the client should stop
|
|
408
|
+
* playback and truncate what was not heard.
|
|
409
|
+
*/
|
|
410
|
+
export const realtimeSpeechStartedEventSchema = z.object({
|
|
411
|
+
...eventBase,
|
|
412
|
+
type: z.literal('input_audio.speech_started'),
|
|
413
|
+
itemId: realtimeItemIdSchema,
|
|
414
|
+
audioStartMs: milliseconds,
|
|
415
|
+
});
|
|
416
|
+
export const realtimeSpeechStoppedEventSchema = z.object({
|
|
417
|
+
...eventBase,
|
|
418
|
+
type: z.literal('input_audio.speech_stopped'),
|
|
419
|
+
itemId: realtimeItemIdSchema,
|
|
420
|
+
audioEndMs: milliseconds,
|
|
421
|
+
});
|
|
422
|
+
export const realtimeInputAudioCommittedEventSchema = z.object({
|
|
423
|
+
...eventBase,
|
|
424
|
+
type: z.literal('input_audio.committed'),
|
|
425
|
+
itemId: realtimeItemIdSchema,
|
|
426
|
+
previousItemId: realtimeItemIdSchema.optional(),
|
|
427
|
+
});
|
|
428
|
+
export const realtimeInputAudioClearedEventSchema = z.object({
|
|
429
|
+
...eventBase,
|
|
430
|
+
type: z.literal('input_audio.cleared'),
|
|
431
|
+
});
|
|
432
|
+
export const realtimeResponseCreatedEventSchema = z.object({
|
|
433
|
+
...eventBase,
|
|
434
|
+
type: z.literal('response.created'),
|
|
435
|
+
responseId: realtimeResponseIdSchema,
|
|
436
|
+
});
|
|
437
|
+
/** One bounded frame of spoken output, in the session's `outputAudioFormat`. */
|
|
438
|
+
export const realtimeOutputAudioDeltaEventSchema = z.object({
|
|
439
|
+
...eventBase,
|
|
440
|
+
type: z.literal('output_audio.delta'),
|
|
441
|
+
responseId: realtimeResponseIdSchema,
|
|
442
|
+
itemId: realtimeItemIdSchema,
|
|
443
|
+
contentIndex: z.number().int().nonnegative().safe(),
|
|
444
|
+
format: realtimeAudioFormatSchema,
|
|
445
|
+
data: audioFrameSchema,
|
|
446
|
+
});
|
|
447
|
+
export const realtimeOutputAudioDoneEventSchema = z.object({
|
|
448
|
+
...eventBase,
|
|
449
|
+
type: z.literal('output_audio.done'),
|
|
450
|
+
responseId: realtimeResponseIdSchema,
|
|
451
|
+
itemId: realtimeItemIdSchema,
|
|
452
|
+
contentIndex: z.number().int().nonnegative().safe(),
|
|
453
|
+
});
|
|
454
|
+
/**
|
|
455
|
+
* Which audio a transcript describes: the caller's (`input_audio`) or the
|
|
456
|
+
* model's own speech (`output_audio`). Neither is answer text.
|
|
457
|
+
*/
|
|
458
|
+
export const realtimeTranscriptSourceSchema = z.enum(['input_audio', 'output_audio']);
|
|
459
|
+
export const realtimeTranscriptDeltaEventSchema = z.object({
|
|
460
|
+
...eventBase,
|
|
461
|
+
type: z.literal('transcript.delta'),
|
|
462
|
+
source: realtimeTranscriptSourceSchema,
|
|
463
|
+
itemId: realtimeItemIdSchema,
|
|
464
|
+
contentIndex: z.number().int().nonnegative().safe(),
|
|
465
|
+
/** Present for `output_audio`: the response whose speech this is. */
|
|
466
|
+
responseId: realtimeResponseIdSchema.optional(),
|
|
467
|
+
text: z.string(),
|
|
468
|
+
});
|
|
469
|
+
export const realtimeTranscriptDoneEventSchema = z.object({
|
|
470
|
+
...eventBase,
|
|
471
|
+
type: z.literal('transcript.done'),
|
|
472
|
+
source: realtimeTranscriptSourceSchema,
|
|
473
|
+
itemId: realtimeItemIdSchema,
|
|
474
|
+
contentIndex: z.number().int().nonnegative().safe(),
|
|
475
|
+
responseId: realtimeResponseIdSchema.optional(),
|
|
476
|
+
transcript: z.string(),
|
|
477
|
+
});
|
|
478
|
+
/** Written output of a response (`outputModalities` including `text`). */
|
|
479
|
+
export const realtimeTextDeltaEventSchema = z.object({
|
|
480
|
+
...eventBase,
|
|
481
|
+
type: z.literal('text.delta'),
|
|
482
|
+
responseId: realtimeResponseIdSchema,
|
|
483
|
+
itemId: realtimeItemIdSchema,
|
|
484
|
+
contentIndex: z.number().int().nonnegative().safe(),
|
|
485
|
+
text: z.string(),
|
|
486
|
+
});
|
|
487
|
+
/** A tool call being streamed; same accumulation rules as the one-shot event. */
|
|
488
|
+
export const realtimeToolCallEventSchema = z.object({
|
|
489
|
+
...eventBase,
|
|
490
|
+
type: z.literal('tool_call'),
|
|
491
|
+
responseId: realtimeResponseIdSchema,
|
|
492
|
+
itemId: realtimeItemIdSchema,
|
|
493
|
+
toolCallId: z.string().min(1).max(128),
|
|
494
|
+
name: z.string().min(1).max(128).optional(),
|
|
495
|
+
argumentsDelta: z.string().optional(),
|
|
496
|
+
complete: z.boolean(),
|
|
497
|
+
});
|
|
498
|
+
/** How a response ended. */
|
|
499
|
+
export const realtimeResponseStatusSchema = z.enum([
|
|
500
|
+
'completed',
|
|
501
|
+
'cancelled',
|
|
502
|
+
'incomplete',
|
|
503
|
+
'failed',
|
|
504
|
+
]);
|
|
505
|
+
/** Why a session ended. */
|
|
506
|
+
export const realtimeSessionCloseReasonSchema = z.enum([
|
|
507
|
+
'client_closed',
|
|
508
|
+
'max_duration',
|
|
509
|
+
'idle_timeout',
|
|
510
|
+
'limit_exceeded',
|
|
511
|
+
'resume_expired',
|
|
512
|
+
'upstream_closed',
|
|
513
|
+
'upstream_error',
|
|
514
|
+
'no_route_available',
|
|
515
|
+
'server_shutdown',
|
|
516
|
+
]);
|
|
517
|
+
/**
|
|
518
|
+
* What a session kind requires of its configuration. Each kind needs a
|
|
519
|
+
* different subset of the config, so these rules are cross-field and live
|
|
520
|
+
* here once: the public `session.open` frame and the signed session request
|
|
521
|
+
* apply the same ones, and a session a client may open is one Kaana may run.
|
|
522
|
+
*/
|
|
523
|
+
export function realtimeConfigIssues(kind, config) {
|
|
524
|
+
const issues = [];
|
|
525
|
+
const found = (path, message) => issues.push({ path, message });
|
|
526
|
+
const producesAudio = config.outputModalities?.includes('audio') === true;
|
|
527
|
+
const hasTools = config.tools !== undefined && config.tools.length > 0;
|
|
528
|
+
switch (kind) {
|
|
529
|
+
case 'conversation':
|
|
530
|
+
if (config.outputModalities === undefined) {
|
|
531
|
+
found(['outputModalities'], 'a conversation names what its responses produce');
|
|
532
|
+
}
|
|
533
|
+
if (producesAudio && (config.voice === undefined || config.outputAudioFormat === undefined)) {
|
|
534
|
+
found([], 'spoken responses need a voice and an output audio format');
|
|
535
|
+
}
|
|
536
|
+
if (config.translation !== undefined) {
|
|
537
|
+
found(['translation'], 'only a translation session translates');
|
|
538
|
+
}
|
|
539
|
+
break;
|
|
540
|
+
case 'transcription':
|
|
541
|
+
if (config.inputAudioTranscription === undefined) {
|
|
542
|
+
found(['inputAudioTranscription'], 'a transcription session transcribes its input');
|
|
543
|
+
}
|
|
544
|
+
for (const field of [
|
|
545
|
+
'outputModalities',
|
|
546
|
+
'voice',
|
|
547
|
+
'outputAudioFormat',
|
|
548
|
+
'translation',
|
|
549
|
+
'tools',
|
|
550
|
+
'toolChoice',
|
|
551
|
+
'maxOutputTokens',
|
|
552
|
+
]) {
|
|
553
|
+
if (config[field] !== undefined) {
|
|
554
|
+
found([field], 'a transcription session never responds');
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
if (config.turnDetection.type !== 'none' &&
|
|
558
|
+
(config.turnDetection.createResponse || config.turnDetection.interruptResponse)) {
|
|
559
|
+
found(['turnDetection'], 'a transcription session has no response to create or interrupt');
|
|
560
|
+
}
|
|
561
|
+
break;
|
|
562
|
+
case 'translation':
|
|
563
|
+
if (config.translation === undefined || config.outputAudioFormat === undefined) {
|
|
564
|
+
found([], 'a translation session names its target language and output audio format');
|
|
565
|
+
}
|
|
566
|
+
if (config.tools !== undefined || config.toolChoice !== undefined) {
|
|
567
|
+
found(['tools'], 'a translation session calls no tools');
|
|
568
|
+
}
|
|
569
|
+
break;
|
|
570
|
+
}
|
|
571
|
+
if (config.toolChoice !== undefined && !hasTools) {
|
|
572
|
+
found(['toolChoice'], 'a tool choice requires at least one tool definition');
|
|
573
|
+
}
|
|
574
|
+
if (config.tools !== undefined) {
|
|
575
|
+
const names = config.tools.map((tool) => tool.name);
|
|
576
|
+
if (new Set(names).size !== names.length) {
|
|
577
|
+
found(['tools'], 'tool names must be unique within one session');
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
if (config.outputModalities !== undefined &&
|
|
581
|
+
new Set(config.outputModalities).size !== config.outputModalities.length) {
|
|
582
|
+
found(['outputModalities'], 'each output modality is named at most once');
|
|
583
|
+
}
|
|
584
|
+
return issues;
|
|
585
|
+
}
|
|
586
|
+
/* -------------------------------------------------------------------------- */
|
|
587
|
+
/* Opening a session (`GET /v1/realtime`) */
|
|
588
|
+
/* -------------------------------------------------------------------------- */
|
|
589
|
+
/**
|
|
590
|
+
* The most text `conversation.item.create` commands a session may be opened
|
|
591
|
+
* for: the bound a duration-priced route's hold is sized from.
|
|
592
|
+
*/
|
|
593
|
+
export const MAX_REALTIME_TEXT_ITEMS = 10_000;
|
|
594
|
+
/**
|
|
595
|
+
* The customer's partial limits request: every signed limit, each filled from
|
|
596
|
+
* Kaana's defaults when omitted, plus `maxTextItems`, the most billable text
|
|
597
|
+
* items the session may send (absent, it defaults to `maxResponses`).
|
|
598
|
+
*/
|
|
599
|
+
export const realtimeOpenLimitsSchema = realtimeSessionLimitsSchema
|
|
600
|
+
.partial()
|
|
601
|
+
.extend({
|
|
602
|
+
maxTextItems: z.number().int().nonnegative().max(MAX_REALTIME_TEXT_ITEMS).optional(),
|
|
603
|
+
})
|
|
604
|
+
.strict();
|
|
605
|
+
/**
|
|
606
|
+
* The first frame a client sends on `GET /v1/realtime?model=…` to OPEN a
|
|
607
|
+
* session (a reconnection instead sends `session.resume`). One grant covers the
|
|
608
|
+
* whole session and it is settled exactly once, when the session closes.
|
|
609
|
+
*
|
|
610
|
+
* Moved from the Oxy edge (`realtimeOpenFrameSchema`) by Kaana ADR 0002.
|
|
611
|
+
*/
|
|
612
|
+
export const realtimeSessionOpenSchema = z
|
|
613
|
+
.object({
|
|
614
|
+
type: z.literal('session.open'),
|
|
615
|
+
kind: realtimeSessionKindSchema,
|
|
616
|
+
/** Absent means `websocket`, the only transport there is. */
|
|
617
|
+
transport: realtimeSessionTransportSchema.optional(),
|
|
618
|
+
config: realtimeSessionConfigSchema,
|
|
619
|
+
limits: realtimeOpenLimitsSchema.optional(),
|
|
620
|
+
/** The caller's own correlation id. */
|
|
621
|
+
clientSessionId: z.string().min(1).max(128).optional(),
|
|
622
|
+
labels: requestLabelsSchema.optional(),
|
|
623
|
+
})
|
|
624
|
+
.strict()
|
|
625
|
+
.superRefine((open, ctx) => {
|
|
626
|
+
for (const found of realtimeConfigIssues(open.kind, open.config)) {
|
|
627
|
+
ctx.addIssue({ code: 'custom', path: ['config', ...found.path], message: found.message });
|
|
628
|
+
}
|
|
629
|
+
const limits = open.limits;
|
|
630
|
+
if (limits?.idleTimeoutMs !== undefined &&
|
|
631
|
+
limits.maxDurationMs !== undefined &&
|
|
632
|
+
limits.idleTimeoutMs > limits.maxDurationMs) {
|
|
633
|
+
ctx.addIssue({
|
|
634
|
+
code: 'custom',
|
|
635
|
+
path: ['limits', 'idleTimeoutMs'],
|
|
636
|
+
message: 'an idle timeout cannot outlast the session',
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
/**
|
|
641
|
+
* Check a `session.open` against the model's published catalogue entry — its
|
|
642
|
+
* `capabilities.realtime` and its `audio` — the realtime counterpart of
|
|
643
|
+
* {@link checkSpeechRequest} (Kaana ADR 0002 decision 9; Alia ADR 0017 §5).
|
|
644
|
+
* Kaana refuses an open with `invalid_request` and the first issue's `param`
|
|
645
|
+
* BEFORE asking Oxy for a grant; a client may run the same check to choose a
|
|
646
|
+
* model.
|
|
647
|
+
*
|
|
648
|
+
* A field the catalogue leaves undeclared (an older producer) is not checked:
|
|
649
|
+
* absent is unknown, never "everything" and never "nothing". The one
|
|
650
|
+
* exception is input transcription in a conversation, which a model must
|
|
651
|
+
* declare to be asked for, because asking for it starts a charge.
|
|
652
|
+
*
|
|
653
|
+
* Returns every issue, in field order; an empty list means the model accepts
|
|
654
|
+
* the open.
|
|
655
|
+
*/
|
|
656
|
+
export function checkRealtimeSessionOpen(open, model) {
|
|
657
|
+
const realtime = model.capabilities.realtime;
|
|
658
|
+
if (realtime === undefined) {
|
|
659
|
+
return [{ param: 'kind', message: 'this model holds no realtime session' }];
|
|
660
|
+
}
|
|
661
|
+
const issues = [];
|
|
662
|
+
if (!realtime.sessionKinds.includes(open.kind)) {
|
|
663
|
+
issues.push({ param: 'kind', message: `this model holds no ${open.kind} session` });
|
|
664
|
+
}
|
|
665
|
+
const transport = open.transport ?? 'websocket';
|
|
666
|
+
if (!realtime.transports.includes(transport)) {
|
|
667
|
+
issues.push({ param: 'transport', message: `this model is not served over ${transport}` });
|
|
668
|
+
}
|
|
669
|
+
const { config } = open;
|
|
670
|
+
const formats = realtime.audioFormats;
|
|
671
|
+
if (formats !== undefined && !formats.input.includes(config.inputAudioFormat)) {
|
|
672
|
+
issues.push({
|
|
673
|
+
param: 'config.inputAudioFormat',
|
|
674
|
+
message: `this model does not take ${config.inputAudioFormat} input`,
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
if (formats !== undefined &&
|
|
678
|
+
config.outputAudioFormat !== undefined &&
|
|
679
|
+
!formats.output.includes(config.outputAudioFormat)) {
|
|
680
|
+
issues.push({
|
|
681
|
+
param: 'config.outputAudioFormat',
|
|
682
|
+
message: `this model does not produce ${config.outputAudioFormat}`,
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
const audio = model.audio;
|
|
686
|
+
if (audio !== undefined &&
|
|
687
|
+
config.voice !== undefined &&
|
|
688
|
+
!audio.voices.some((voice) => voice.id === config.voice)) {
|
|
689
|
+
issues.push({ param: 'config.voice', message: `this model has no voice ${config.voice}` });
|
|
690
|
+
}
|
|
691
|
+
const transcription = config.inputAudioTranscription;
|
|
692
|
+
if (transcription !== undefined &&
|
|
693
|
+
open.kind === 'conversation' &&
|
|
694
|
+
realtime.inputAudioTranscription !== true) {
|
|
695
|
+
issues.push({
|
|
696
|
+
param: 'config.inputAudioTranscription',
|
|
697
|
+
message: 'this model does not transcribe its input inside a conversation',
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
if (transcription?.language !== undefined &&
|
|
701
|
+
audio !== undefined &&
|
|
702
|
+
!audio.languages.includes(transcription.language)) {
|
|
703
|
+
issues.push({
|
|
704
|
+
param: 'config.inputAudioTranscription.language',
|
|
705
|
+
message: `this model does not declare the language ${transcription.language}`,
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
return issues;
|
|
709
|
+
}
|
|
710
|
+
/* -------------------------------------------------------------------------- */
|
|
711
|
+
/* Server events a public client reads */
|
|
712
|
+
/* -------------------------------------------------------------------------- */
|
|
713
|
+
/**
|
|
714
|
+
* The session opened: the revision-pinned model, fixed for the rest of the
|
|
715
|
+
* session, and the effective configuration and limits. The signed lane's
|
|
716
|
+
* `realtimeSessionCreatedEventSchema` also names the provider and deployment;
|
|
717
|
+
* a public client is never told either.
|
|
718
|
+
*/
|
|
719
|
+
export const realtimePublicSessionCreatedEventSchema = z.object({
|
|
720
|
+
...eventBase,
|
|
721
|
+
type: z.literal('session.created'),
|
|
722
|
+
/** The revision serving the session. */
|
|
723
|
+
model: modelReferenceSchema,
|
|
724
|
+
kind: realtimeSessionKindSchema,
|
|
725
|
+
config: realtimeSessionConfigSchema,
|
|
726
|
+
limits: realtimeSessionLimitsSchema,
|
|
727
|
+
resumeWindowMs: z.number().int().nonnegative().max(MAX_REALTIME_RESUME_WINDOW_MS),
|
|
728
|
+
startedAt: inferenceTimestampSchema,
|
|
729
|
+
expiresAt: inferenceTimestampSchema,
|
|
730
|
+
});
|
|
731
|
+
/** A response ended, with the units it consumed. */
|
|
732
|
+
export const realtimePublicResponseDoneEventSchema = z.object({
|
|
733
|
+
...eventBase,
|
|
734
|
+
type: z.literal('response.done'),
|
|
735
|
+
responseId: realtimeResponseIdSchema,
|
|
736
|
+
status: realtimeResponseStatusSchema,
|
|
737
|
+
finishReason: inferenceFinishReasonSchema.optional(),
|
|
738
|
+
units: z.array(usageQuantitySchema),
|
|
739
|
+
usageSource: usageSourceSchema,
|
|
740
|
+
});
|
|
741
|
+
/**
|
|
742
|
+
* An error in the public vocabulary. `fatal: false` refuses one command and
|
|
743
|
+
* the session continues; `fatal: true` is followed by `session.closed`.
|
|
744
|
+
*/
|
|
745
|
+
export const realtimePublicErrorEventSchema = z.object({
|
|
746
|
+
...eventBase,
|
|
747
|
+
type: z.literal('error'),
|
|
748
|
+
fatal: z.boolean(),
|
|
749
|
+
error: publicErrorSchema,
|
|
750
|
+
});
|
|
751
|
+
/** The terminal event, with the session's total units. */
|
|
752
|
+
export const realtimePublicSessionClosedEventSchema = z.object({
|
|
753
|
+
...eventBase,
|
|
754
|
+
type: z.literal('session.closed'),
|
|
755
|
+
reason: realtimeSessionCloseReasonSchema,
|
|
756
|
+
units: z.array(usageQuantitySchema),
|
|
757
|
+
usageSource: usageSourceSchema,
|
|
758
|
+
closedAt: inferenceTimestampSchema,
|
|
759
|
+
});
|
|
760
|
+
/** Every event a public session can emit. */
|
|
761
|
+
export const realtimePublicServerEventSchema = z
|
|
762
|
+
.discriminatedUnion('type', [
|
|
763
|
+
realtimePublicSessionCreatedEventSchema,
|
|
764
|
+
realtimeSessionUpdatedEventSchema,
|
|
765
|
+
realtimeSessionResumedEventSchema,
|
|
766
|
+
realtimeCommandAcceptedEventSchema,
|
|
767
|
+
realtimeItemAddedEventSchema,
|
|
768
|
+
realtimeItemDoneEventSchema,
|
|
769
|
+
realtimeItemDeletedEventSchema,
|
|
770
|
+
realtimeItemTruncatedEventSchema,
|
|
771
|
+
realtimeSpeechStartedEventSchema,
|
|
772
|
+
realtimeSpeechStoppedEventSchema,
|
|
773
|
+
realtimeInputAudioCommittedEventSchema,
|
|
774
|
+
realtimeInputAudioClearedEventSchema,
|
|
775
|
+
realtimeResponseCreatedEventSchema,
|
|
776
|
+
realtimeOutputAudioDeltaEventSchema,
|
|
777
|
+
realtimeOutputAudioDoneEventSchema,
|
|
778
|
+
realtimeTranscriptDeltaEventSchema,
|
|
779
|
+
realtimeTranscriptDoneEventSchema,
|
|
780
|
+
realtimeTextDeltaEventSchema,
|
|
781
|
+
realtimeToolCallEventSchema,
|
|
782
|
+
realtimePublicResponseDoneEventSchema,
|
|
783
|
+
realtimePublicErrorEventSchema,
|
|
784
|
+
realtimePublicSessionClosedEventSchema,
|
|
785
|
+
])
|
|
786
|
+
.superRefine((event, ctx) => {
|
|
787
|
+
if (event.type !== 'response.done' && event.type !== 'session.closed')
|
|
788
|
+
return;
|
|
789
|
+
const units = event.units.map((quantity) => quantity.unit);
|
|
790
|
+
if (new Set(units).size !== units.length) {
|
|
791
|
+
ctx.addIssue({
|
|
792
|
+
code: 'custom',
|
|
793
|
+
path: ['units'],
|
|
794
|
+
message: 'each unit is reported at most once',
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
});
|