@kaana.ai/contracts 0.0.0-stage → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/LICENSE +673 -0
  2. package/NOTICE +21 -0
  3. package/dist/index.d.ts +29 -0
  4. package/dist/index.js +29 -0
  5. package/dist/internal/attribution.d.ts +116 -0
  6. package/dist/internal/attribution.js +82 -0
  7. package/dist/internal/canonical.d.ts +31 -0
  8. package/dist/internal/canonical.js +57 -0
  9. package/dist/internal/catalogue.d.ts +1030 -0
  10. package/dist/internal/catalogue.js +349 -0
  11. package/dist/internal/credentialControl.d.ts +329 -0
  12. package/dist/internal/credentialControl.js +256 -0
  13. package/dist/internal/decisions.d.ts +245 -0
  14. package/dist/internal/decisions.js +69 -0
  15. package/dist/internal/economics.d.ts +27 -0
  16. package/dist/internal/economics.js +26 -0
  17. package/dist/internal/embeddings.d.ts +164 -0
  18. package/dist/internal/embeddings.js +59 -0
  19. package/dist/internal/errors.d.ts +185 -0
  20. package/dist/internal/errors.js +211 -0
  21. package/dist/internal/grant.d.ts +696 -0
  22. package/dist/internal/grant.js +437 -0
  23. package/dist/internal/identifiers.d.ts +63 -0
  24. package/dist/internal/identifiers.js +65 -0
  25. package/dist/internal/privateAutoExecution.d.ts +158 -0
  26. package/dist/internal/privateAutoExecution.js +188 -0
  27. package/dist/internal/realtime.d.ts +1005 -0
  28. package/dist/internal/realtime.js +187 -0
  29. package/dist/internal/request.d.ts +1079 -0
  30. package/dist/internal/request.js +404 -0
  31. package/dist/internal/routing.d.ts +90 -0
  32. package/dist/internal/routing.js +139 -0
  33. package/dist/internal/scopedExecution.d.ts +74 -0
  34. package/dist/internal/scopedExecution.js +61 -0
  35. package/dist/internal/settlement.d.ts +205 -0
  36. package/dist/internal/settlement.js +71 -0
  37. package/dist/internal/streamEvents.d.ts +422 -0
  38. package/dist/internal/streamEvents.js +171 -0
  39. package/dist/internal/usage.d.ts +127 -0
  40. package/dist/internal/usage.js +109 -0
  41. package/dist/internal.d.ts +30 -0
  42. package/dist/internal.js +30 -0
  43. package/dist/public/audio.d.ts +347 -0
  44. package/dist/public/audio.js +236 -0
  45. package/dist/public/audioFormats.d.ts +64 -0
  46. package/dist/public/audioFormats.js +60 -0
  47. package/dist/public/catalogue.d.ts +914 -0
  48. package/dist/public/catalogue.js +516 -0
  49. package/dist/public/chatCompletions.d.ts +381 -0
  50. package/dist/public/chatCompletions.js +321 -0
  51. package/dist/public/decisions.d.ts +182 -0
  52. package/dist/public/decisions.js +257 -0
  53. package/dist/public/embeddings.d.ts +34 -0
  54. package/dist/public/embeddings.js +66 -0
  55. package/dist/public/errors.d.ts +195 -0
  56. package/dist/public/errors.js +274 -0
  57. package/dist/public/generations.d.ts +330 -0
  58. package/dist/public/generations.js +121 -0
  59. package/dist/public/identifiers.d.ts +139 -0
  60. package/dist/public/identifiers.js +160 -0
  61. package/dist/public/images.d.ts +36 -0
  62. package/dist/public/images.js +36 -0
  63. package/dist/public/messages.d.ts +297 -0
  64. package/dist/public/messages.js +251 -0
  65. package/dist/public/money.d.ts +308 -0
  66. package/dist/public/money.js +237 -0
  67. package/dist/public/policy.d.ts +19 -0
  68. package/dist/public/policy.js +20 -0
  69. package/dist/public/realtime.d.ts +2006 -0
  70. package/dist/public/realtime.js +797 -0
  71. package/dist/public/responses.d.ts +253 -0
  72. package/dist/public/responses.js +95 -0
  73. package/dist/public/streamEvents.d.ts +372 -0
  74. package/dist/public/streamEvents.js +157 -0
  75. package/dist/public/version.d.ts +177 -0
  76. package/dist/public/version.js +177 -0
  77. package/openapi.json +6967 -0
  78. package/package.json +44 -4
  79. package/README.md +0 -3
@@ -0,0 +1,516 @@
1
+ /**
2
+ * The public catalogue: what `GET /v1/models` and `GET /v1/power-levels` say.
3
+ *
4
+ * Moved from `@oxy.so/contracts` 4.12.0 (`src/inference/catalogue.ts`) by Kaana ADR 0002.
5
+ */
6
+ import { z } from 'zod';
7
+ import { inferenceDateSchema, inferenceHttpsUrlSchema, inferenceRegionSchema, inferenceTimestampSchema, modelIdSchema, modelReferenceSchema, modelRevisionLabelSchema, publisherSlugSchema, } from './identifiers.js';
8
+ import { audioInputMediaTypeSchema, languageTagSchema, realtimeAudioFormatSchema, } from './audioFormats.js';
9
+ import { priceSnapshotSchema } from './money.js';
10
+ import { inferenceAudioMediaTypeSchema } from './streamEvents.js';
11
+ /** Input/request modalities; structured decisions are an output, never an input. */
12
+ export const inferenceModalitySchema = z.enum(['text', 'image', 'audio', 'video', 'embedding']);
13
+ /** Declared model outputs, separate from request/input modalities. */
14
+ export const inferenceOutputModalitySchema = z.enum([
15
+ ...inferenceModalitySchema.options,
16
+ 'decisions',
17
+ ]);
18
+ /**
19
+ * How hard a reasoning model is asked to think before it answers.
20
+ *
21
+ * A closed, provider-neutral vocabulary: Kaana translates each value into the
22
+ * serving provider's own control. A model advertises the subset it accepts in
23
+ * `modelCapabilitiesSchema.reasoningEfforts`, and Oxy refuses a request for an
24
+ * effort the resolved model does not advertise rather than dropping it.
25
+ */
26
+ export const reasoningEffortSchema = z.enum(['low', 'medium', 'high']);
27
+ /**
28
+ * A POWER LEVEL: the public name of a product-preset routing profile that asks
29
+ * Oxy to choose a model by capability tier instead of naming one (contract set
30
+ * 3.4.0). Ordered cheapest first; `auto` asks Oxy to pick the cheapest level
31
+ * that suffices for the request.
32
+ *
33
+ * - `instant` — very cheap, fast small models; the least reasoning each model
34
+ * accepts (`low` on a reasoning model, nothing on one without effort control)
35
+ * - `medium` — mid-size models, low reasoning effort
36
+ * - `high` — strong models, medium reasoning effort
37
+ * - `xhigh` — strong models, high reasoning effort
38
+ * - `pro` — frontier models, high reasoning effort
39
+ * - `ultra` — the heaviest frontier models, maximum reasoning effort
40
+ *
41
+ * A power level is a routing profile, never a model: its slug has no `/`, and
42
+ * the response always reports the concrete model that ran.
43
+ */
44
+ export const powerLevelSchema = z.enum([
45
+ 'auto',
46
+ 'instant',
47
+ 'medium',
48
+ 'high',
49
+ 'xhigh',
50
+ 'pro',
51
+ 'ultra',
52
+ ]);
53
+ /**
54
+ * The reviewed capability tier of one MODEL, which power levels choose from.
55
+ * `xhigh` and `auto` are not classes: `xhigh` runs `high`-class models at a
56
+ * higher effort, and `auto` chooses a level. Reviewed catalogue data with a
57
+ * cited public benchmark source — never inferred from a model's name.
58
+ */
59
+ export const modelPowerClassSchema = z.enum(['instant', 'medium', 'high', 'pro', 'ultra']);
60
+ /**
61
+ * The public dialects a request can arrive in — `client.apiFormat` on the
62
+ * envelope, and the list a deployment declares it can execute.
63
+ *
64
+ * Named (contract set 3.2.0) rather than inline on the client metadata so the
65
+ * two uses cannot drift: a capability naming a dialect the envelope cannot
66
+ * carry, or the reverse, would authorize a request no route can serve.
67
+ */
68
+ export const inferenceApiFormatSchema = z.enum([
69
+ 'responses',
70
+ 'chat_completions',
71
+ 'embeddings',
72
+ 'decisions',
73
+ 'images_generations',
74
+ 'audio_transcriptions',
75
+ 'audio_speech',
76
+ 'rerank',
77
+ 'batches',
78
+ ]);
79
+ /**
80
+ * What a realtime session is FOR. The three are different upstream products
81
+ * with different event families and different metering, not options of one:
82
+ *
83
+ * - `conversation` — a speech-to-speech (or text) model holding a dialogue,
84
+ * with responses, tool calls and interruption.
85
+ * - `transcription` — live speech-to-text; the model never responds, it only
86
+ * transcribes committed input audio.
87
+ * - `translation` — live speech-to-speech translation into one target language.
88
+ */
89
+ export const realtimeSessionKindSchema = z.enum(['conversation', 'transcription', 'translation']);
90
+ /**
91
+ * How a realtime session is carried. One member today: a WebSocket, whose every
92
+ * frame is one JSON message (see `realtime.ts`). WebRTC and SIP are separate
93
+ * transports with separate media handling, and are added as members only when a
94
+ * deployment can actually execute them.
95
+ */
96
+ export const realtimeSessionTransportSchema = z.enum(['websocket']);
97
+ /**
98
+ * What a deployment can execute as a realtime session. Present only on a model
99
+ * that holds sessions; absent means it holds none, which is the state of every
100
+ * catalogue written before contract set 3.2.0.
101
+ *
102
+ * Two optional fields arrived in contract set 4.1.0 (Alia ADR 0017, Kaana
103
+ * phase 1), and ABSENT means undeclared — a catalogue from an older producer —
104
+ * never "every value":
105
+ *
106
+ * - `inputAudioTranscription` — the model transcribes the caller's speech
107
+ * inside a `conversation` when the session asks for it
108
+ * (`config.inputAudioTranscription`), and delivers each turn as a
109
+ * `transcript.done` with `source: 'input_audio'`. Absent or `false`: a
110
+ * session that asks is refused before anything is held.
111
+ * - `audioFormats` — the realtime encodings a session may name as
112
+ * `config.inputAudioFormat` and `config.outputAudioFormat`. These are not the
113
+ * media types of `audio.audioFormats`, which describe files.
114
+ */
115
+ export const modelRealtimeCapabilitiesSchema = z
116
+ .object({
117
+ transports: z.array(realtimeSessionTransportSchema).min(1),
118
+ sessionKinds: z.array(realtimeSessionKindSchema).min(1),
119
+ inputAudioTranscription: z.boolean().optional(),
120
+ audioFormats: z
121
+ .object({
122
+ input: z.array(realtimeAudioFormatSchema).min(1),
123
+ output: z.array(realtimeAudioFormatSchema),
124
+ })
125
+ .strict()
126
+ .optional(),
127
+ })
128
+ .strict();
129
+ /**
130
+ * What a model can do, in the terms a caller has to decide against before
131
+ * sending a request: can it call tools, does it accept images, will it honour a
132
+ * JSON schema, how much context does it take, how much can it emit.
133
+ */
134
+ export const modelCapabilitiesSchema = z
135
+ .object({
136
+ inputModalities: z.array(inferenceModalitySchema).min(1),
137
+ outputModalities: z.array(inferenceOutputModalitySchema).min(1),
138
+ tools: z.boolean(),
139
+ parallelToolCalls: z.boolean(),
140
+ structuredOutput: z.boolean(),
141
+ jsonMode: z.boolean(),
142
+ reasoning: z.boolean(),
143
+ /**
144
+ * The reasoning efforts a request may name for this model, in the order
145
+ * low → high. Empty means the model takes no effort control at all (it may
146
+ * still reason: `reasoning` answers that). Added in contract set 3.1.0;
147
+ * absent on the wire from an older producer, it parses as `[]`.
148
+ */
149
+ reasoningEfforts: z.array(reasoningEffortSchema).default([]),
150
+ streaming: z.boolean(),
151
+ promptCaching: z.boolean(),
152
+ maxContextTokens: z.number().int().positive().safe(),
153
+ maxOutputTokens: z.number().int().positive().safe(),
154
+ /**
155
+ * The request dialects a route to this model can EXECUTE. Modalities say
156
+ * what a model consumes and produces; they do not say which request shape a
157
+ * route can carry, and catalogue presence is not that evidence either — a
158
+ * speech-to-speech model is `audio` in and out and still cannot answer a
159
+ * `/v1/audio/transcriptions` upload. Oxy authorizes a request only when its
160
+ * `client.apiFormat` is listed here.
161
+ *
162
+ * Added in contract set 3.2.0. ABSENT means undeclared — a catalogue from an
163
+ * older producer — never "every format": a consumer then applies the rules it
164
+ * applied before this field existed, and authorizes no format that needs a
165
+ * declaration (audio output on `chat_completions`, a realtime session).
166
+ */
167
+ apiFormats: z.array(inferenceApiFormatSchema).min(1).optional(),
168
+ /** Realtime sessions this model holds; absent means none. Contract set 3.2.0. */
169
+ realtime: modelRealtimeCapabilitiesSchema.optional(),
170
+ })
171
+ .strict()
172
+ .superRefine((capabilities, ctx) => {
173
+ const formats = capabilities.apiFormats;
174
+ if (formats !== undefined && new Set(formats).size !== formats.length) {
175
+ ctx.addIssue({
176
+ code: 'custom',
177
+ path: ['apiFormats'],
178
+ message: 'each API format is declared at most once',
179
+ });
180
+ }
181
+ const realtime = capabilities.realtime;
182
+ if (realtime === undefined)
183
+ return;
184
+ if (new Set(realtime.sessionKinds).size !== realtime.sessionKinds.length) {
185
+ ctx.addIssue({
186
+ code: 'custom',
187
+ path: ['realtime', 'sessionKinds'],
188
+ message: 'each session kind is declared at most once',
189
+ });
190
+ }
191
+ if (new Set(realtime.transports).size !== realtime.transports.length) {
192
+ ctx.addIssue({
193
+ code: 'custom',
194
+ path: ['realtime', 'transports'],
195
+ message: 'each transport is declared at most once',
196
+ });
197
+ }
198
+ for (const direction of ['input', 'output']) {
199
+ const formats = realtime.audioFormats?.[direction];
200
+ if (formats !== undefined && new Set(formats).size !== formats.length) {
201
+ ctx.addIssue({
202
+ code: 'custom',
203
+ path: ['realtime', 'audioFormats', direction],
204
+ message: 'each realtime audio format is declared at most once',
205
+ });
206
+ }
207
+ }
208
+ // A realtime session consumes audio: a model that declares sessions and no
209
+ // audio input is a declaration no session could honour.
210
+ if (!capabilities.inputModalities.includes('audio')) {
211
+ ctx.addIssue({
212
+ code: 'custom',
213
+ path: ['realtime'],
214
+ message: 'a model that holds realtime sessions consumes audio',
215
+ });
216
+ }
217
+ });
218
+ /**
219
+ * License terms, including the two questions that decide whether Oxy may serve
220
+ * a model to third parties at all: is commercial use permitted, and must the
221
+ * base model be attributed.
222
+ */
223
+ export const modelLicenseSchema = z
224
+ .object({
225
+ /** SPDX identifier where one exists, otherwise the publisher's own name. */
226
+ licenseId: z.string().min(1).max(128),
227
+ displayName: z.string().min(1).max(200),
228
+ url: inferenceHttpsUrlSchema.optional(),
229
+ commercialUseAllowed: z.boolean(),
230
+ requiresAttribution: z.boolean(),
231
+ acceptableUsePolicyUrl: inferenceHttpsUrlSchema.optional(),
232
+ })
233
+ .strict();
234
+ /**
235
+ * How a model came to exist.
236
+ *
237
+ * `releaseKind` is what stops `alia/*` from becoming a re-badging namespace:
238
+ * a first-party namespace may only carry models Alia actually trained or
239
+ * derived, never a third-party route wearing an Oxy name.
240
+ */
241
+ export const modelProvenanceSchema = z
242
+ .object({
243
+ releaseKind: z.enum([
244
+ 'first_party_original',
245
+ 'first_party_derived',
246
+ 'open_weight',
247
+ 'third_party_hosted',
248
+ ]),
249
+ /** The model this one was derived/fine-tuned from, where there is one. */
250
+ baseModelId: modelIdSchema.optional(),
251
+ trainingOrganization: z.string().min(1).max(200).optional(),
252
+ })
253
+ .strict();
254
+ /**
255
+ * What happens to the customer's prompts and responses on a given route.
256
+ *
257
+ * Every field here is enforceable by a routing policy, which is the reason they
258
+ * are structured rather than a prose paragraph on a docs page.
259
+ */
260
+ export const inferenceDataPolicySchema = z
261
+ .object({
262
+ retainsPayloads: z.boolean(),
263
+ /** Zero when nothing is retained. */
264
+ retentionDays: z.number().int().nonnegative().max(3650),
265
+ trainsOnCustomerData: z.boolean(),
266
+ zeroDataRetentionAvailable: z.boolean(),
267
+ /** Named subprocessors a payload may pass through, for compliance review. */
268
+ subprocessors: z.array(z.string().min(1).max(200)).default([]),
269
+ policyUrl: inferenceHttpsUrlSchema.optional(),
270
+ })
271
+ .strict()
272
+ .superRefine((policy, ctx) => {
273
+ if (!policy.retainsPayloads && policy.retentionDays > 0) {
274
+ ctx.addIssue({
275
+ code: 'custom',
276
+ path: ['retentionDays'],
277
+ message: 'a route that retains no payloads cannot have a retention window',
278
+ });
279
+ }
280
+ // Training requires having the data to train on. A route claiming both is
281
+ // reporting one of the two fields wrongly, and a routing policy that
282
+ // prohibits training would be enforced against a value that is not true.
283
+ if (!policy.retainsPayloads && policy.trainsOnCustomerData) {
284
+ ctx.addIssue({
285
+ code: 'custom',
286
+ path: ['trainsOnCustomerData'],
287
+ message: 'a route that retains no payloads cannot train on customer data',
288
+ });
289
+ }
290
+ });
291
+ /** Deprecation state, with the replacement a customer should migrate to. */
292
+ export const modelDeprecationSchema = z
293
+ .object({
294
+ status: z.enum(['active', 'deprecated', 'retired']),
295
+ replacementModelReference: modelReferenceSchema.optional(),
296
+ announcedAt: inferenceTimestampSchema.optional(),
297
+ sunsetAt: inferenceTimestampSchema.optional(),
298
+ })
299
+ .strict()
300
+ .superRefine((deprecation, ctx) => {
301
+ if (deprecation.status === 'active' && deprecation.sunsetAt !== undefined) {
302
+ ctx.addIssue({
303
+ code: 'custom',
304
+ path: ['sunsetAt'],
305
+ message: 'an active model has no sunset date; announce the deprecation first',
306
+ });
307
+ }
308
+ });
309
+ /** One published evaluation result for a revision. */
310
+ export const modelEvaluationResultSchema = z
311
+ .object({
312
+ suite: z.string().min(1).max(128),
313
+ metric: z.string().min(1).max(128),
314
+ /** Kept as a string: an eval score is published text, not arithmetic. */
315
+ score: z.string().min(1).max(64),
316
+ evaluatedAt: inferenceTimestampSchema.optional(),
317
+ reportUrl: inferenceHttpsUrlSchema.optional(),
318
+ })
319
+ .strict();
320
+ /**
321
+ * Safety metadata a downstream developer needs, and the EU AI Act / GPAI
322
+ * documentation trail expects to find beside a served model.
323
+ */
324
+ export const modelSafetyMetadataSchema = z
325
+ .object({
326
+ safetyCardUrl: inferenceHttpsUrlSchema.optional(),
327
+ contentFilteringDefault: z.enum(['none', 'provider_default', 'strict']),
328
+ knownLimitations: z.array(z.string().min(1).max(500)).default([]),
329
+ /** Content-provenance marking the modality requires, where it requires one. */
330
+ provenanceMarking: z.enum(['none', 'visible_watermark', 'invisible_watermark', 'c2pa']),
331
+ })
332
+ .strict();
333
+ /** The publisher fields a customer sees beside a model. */
334
+ export const cataloguePublisherSummarySchema = z
335
+ .object({
336
+ slug: publisherSlugSchema,
337
+ displayName: z.string().min(1).max(200),
338
+ websiteUrl: inferenceHttpsUrlSchema.optional(),
339
+ })
340
+ .strict();
341
+ /* -------------------------------------------------------------------------- */
342
+ /* Audio capabilities (Kaana ADR 0002, decision 9) */
343
+ /* -------------------------------------------------------------------------- */
344
+ /** One voice a speech or realtime model can speak in. */
345
+ export const voiceSchema = z
346
+ .object({
347
+ /** What a speech request names in `voice`. */
348
+ id: z.string().min(1).max(64),
349
+ name: z.string().min(1).max(200),
350
+ /** The languages this voice speaks; each is one of the model's `languages`. */
351
+ languages: z.array(languageTagSchema),
352
+ gender: z.enum(['female', 'male', 'neutral']).optional(),
353
+ })
354
+ .strict();
355
+ /**
356
+ * What an audio model accepts, published so a request that cannot be served
357
+ * is refused BEFORE a grant or a reservation (`invalid_request` with `param`),
358
+ * never by an adapter after money was held.
359
+ *
360
+ * When several providers serve one model line, the published value is the
361
+ * INTERSECTION — what every one of them accepts — so a request that passes
362
+ * here is servable by any route of the line.
363
+ *
364
+ * - `voices` — empty for a model that reads no text aloud (transcription).
365
+ * - `audioFormats.input` — the media types an upload may have (transcription).
366
+ * - `audioFormats.output` — the media types speech can be produced in.
367
+ * - `speed` — absent when the model accepts no speed control; a request that
368
+ * names one is then refused.
369
+ * - `maxInputCharacters` — the longest text one speech request may carry,
370
+ * counted in UTF-16 code units, as JavaScript's `length` counts.
371
+ *
372
+ * A REALTIME model publishes the same shape (contract set 4.1.0): `voices` are
373
+ * what a session's `config.voice` may name, `languages` what it converses in
374
+ * and what `config.inputAudioTranscription.language` may name, and
375
+ * `audioFormats` is empty in both directions because a session carries no
376
+ * files — its encodings are `capabilities.realtime.audioFormats`. An empty
377
+ * `languages` means the provider documents none, not that the model speaks
378
+ * none; a caller that needs a language then cannot choose the model.
379
+ * `gender` is published only where it has been reviewed against the
380
+ * provider's own catalogue; absent is unknown.
381
+ */
382
+ export const modelAudioCapabilitiesSchema = z
383
+ .object({
384
+ voices: z.array(voiceSchema),
385
+ languages: z.array(languageTagSchema),
386
+ audioFormats: z
387
+ .object({
388
+ input: z.array(audioInputMediaTypeSchema),
389
+ output: z.array(inferenceAudioMediaTypeSchema),
390
+ })
391
+ .strict(),
392
+ speed: z
393
+ .object({
394
+ min: z.number().min(0.25).max(4),
395
+ max: z.number().min(0.25).max(4),
396
+ })
397
+ .strict()
398
+ .optional(),
399
+ maxInputCharacters: z.number().int().positive().safe().optional(),
400
+ })
401
+ .strict()
402
+ .superRefine((audio, ctx) => {
403
+ const ids = audio.voices.map((voice) => voice.id);
404
+ if (new Set(ids).size !== ids.length) {
405
+ ctx.addIssue({ code: 'custom', path: ['voices'], message: 'each voice id is declared once' });
406
+ }
407
+ const languages = new Set(audio.languages);
408
+ if (languages.size !== audio.languages.length) {
409
+ ctx.addIssue({
410
+ code: 'custom',
411
+ path: ['languages'],
412
+ message: 'each language is declared once',
413
+ });
414
+ }
415
+ for (const [index, voice] of audio.voices.entries()) {
416
+ for (const [languageIndex, language] of voice.languages.entries()) {
417
+ if (!languages.has(language)) {
418
+ ctx.addIssue({
419
+ code: 'custom',
420
+ path: ['voices', index, 'languages', languageIndex],
421
+ message: `voice ${voice.id} speaks ${language}, which the model does not declare`,
422
+ });
423
+ }
424
+ }
425
+ }
426
+ if (audio.speed !== undefined && audio.speed.min > audio.speed.max) {
427
+ ctx.addIssue({
428
+ code: 'custom',
429
+ path: ['speed'],
430
+ message: 'a speed range cannot end below where it starts',
431
+ });
432
+ }
433
+ });
434
+ /* -------------------------------------------------------------------------- */
435
+ /* The public catalogue (`GET /v1/models`) */
436
+ /* -------------------------------------------------------------------------- */
437
+ /**
438
+ * One model as `GET /v1/models` and `GET /v1/models/{publisher}/{model}`
439
+ * serve it: modalities, power class, limits, parameters, audio capabilities and
440
+ * the customer price.
441
+ *
442
+ * Derived from Oxy's `modelCatalogueEntrySchema` without what names who serves
443
+ * the model: no serving provider, no deployment, no availability scope and no
444
+ * commercial permission. Those are in the operational catalogue under
445
+ * `@kaana.ai/contracts/internal`.
446
+ */
447
+ export const modelSchema = z.object({
448
+ /** A whole message on the wire, so it is versioned. */
449
+ schemaVersion: z.literal(1),
450
+ modelId: modelIdSchema,
451
+ publisher: cataloguePublisherSummarySchema,
452
+ displayName: z.string().min(1).max(200),
453
+ description: z.string().max(4000).optional(),
454
+ currentRevision: modelRevisionLabelSchema,
455
+ /** Revisions a customer may pin today, newest first. */
456
+ availableRevisions: z.array(modelRevisionLabelSchema).min(1),
457
+ capabilities: modelCapabilitiesSchema,
458
+ /** Present for a model that consumes or produces audio. */
459
+ audio: modelAudioCapabilitiesSchema.optional(),
460
+ license: modelLicenseSchema,
461
+ provenance: modelProvenanceSchema,
462
+ knowledgeCutoff: inferenceDateSchema.optional(),
463
+ releasedOn: inferenceDateSchema.optional(),
464
+ /** When the model was first published upstream, as reported; never an observation time. */
465
+ releasedAt: inferenceTimestampSchema.optional(),
466
+ /** Attested regions some route of this model serves from; empty means none attested. */
467
+ regions: z.array(inferenceRegionSchema).default([]),
468
+ /** The data policy every route of this model satisfies. */
469
+ dataPolicy: inferenceDataPolicySchema,
470
+ /** Oxy's list price. Absent for a model a customer cannot buy per unit (BYOK-only). */
471
+ pricing: priceSnapshotSchema.optional(),
472
+ deprecation: modelDeprecationSchema,
473
+ evaluations: z.array(modelEvaluationResultSchema).default([]),
474
+ safety: modelSafetyMetadataSchema.optional(),
475
+ modelCardUrl: inferenceHttpsUrlSchema.optional(),
476
+ /** The reviewed power class. Absent: callable by name only, chosen by no power level. */
477
+ powerClass: modelPowerClassSchema.optional(),
478
+ });
479
+ /** `GET /v1/models`. */
480
+ export const modelListSchema = z.object({ data: z.array(modelSchema) }).strict();
481
+ /**
482
+ * One power level as `GET /v1/power-levels` lists it for the calling
483
+ * credential: the level, the class it draws from, the effort it asks for, and
484
+ * the model lines it can currently choose among.
485
+ *
486
+ * `auto` draws from no single class (it chooses a level), so it carries no
487
+ * `powerClass` and lists every line of the levels it may climb to.
488
+ */
489
+ export const powerLevelEntrySchema = z
490
+ .object({
491
+ level: powerLevelSchema,
492
+ powerClass: modelPowerClassSchema.optional(),
493
+ /** The effort the level asks for when the request names none. */
494
+ reasoningEffort: reasoningEffortSchema.optional(),
495
+ /** Model lines (`publisher/model`) the level can choose today, for this credential. */
496
+ modelLines: z.array(modelIdSchema),
497
+ })
498
+ .strict()
499
+ .superRefine((entry, ctx) => {
500
+ if ((entry.level === 'auto') !== (entry.powerClass === undefined)) {
501
+ ctx.addIssue({
502
+ code: 'custom',
503
+ path: ['powerClass'],
504
+ message: 'every level but auto draws from exactly one power class',
505
+ });
506
+ }
507
+ if (new Set(entry.modelLines).size !== entry.modelLines.length) {
508
+ ctx.addIssue({
509
+ code: 'custom',
510
+ path: ['modelLines'],
511
+ message: 'each model line is listed once',
512
+ });
513
+ }
514
+ });
515
+ /** `GET /v1/power-levels`. Replaces Oxy's `/v1/models/routing-profiles`. */
516
+ export const powerLevelListSchema = z.object({ data: z.array(powerLevelEntrySchema) }).strict();