@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.
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 +1059 -0
  10. package/dist/internal/catalogue.js +389 -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 +699 -0
  22. package/dist/internal/grant.js +445 -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 +184 -0
  76. package/dist/public/version.js +184 -0
  77. package/openapi.json +6967 -0
  78. package/package.json +44 -4
  79. package/README.md +0 -3
@@ -0,0 +1,404 @@
1
+ /**
2
+ * The signed Oxy→Kaana request envelope (versions 1–4).
3
+ *
4
+ * Moved from `@oxy.so/contracts` 4.12.0 (`src/inference/request.ts`) by Kaana ADR 0002.
5
+ */
6
+ import { z } from 'zod';
7
+ import { inferenceApiFormatSchema, inferenceModalitySchema } from '../public/catalogue.js';
8
+ import { decisionInputSchema } from '../public/decisions.js';
9
+ import { idempotencyKeySchema, inferenceTimestampSchema } from '../public/identifiers.js';
10
+ import { inferenceAudioOutputParametersSchema, inferenceMessageSchema, inferenceReasoningSchema, inferenceSpeechParametersSchema, responseFormatSchema, samplingParametersSchema, toolChoiceSchema, toolDefinitionSchema, } from '../public/messages.js';
11
+ import { routingPolicyReferenceSchema } from '../public/policy.js';
12
+ import { inferenceAttributionSchema } from './attribution.js';
13
+ import { PRIVATE_AUTO_REQUEST_ENVELOPE_VERSION, privateAutoExecutionSchema, privateAutoInputSchema, } from './privateAutoExecution.js';
14
+ import { authorizedRouteSchema, routingTargetSchema } from './routing.js';
15
+ import { SCOPED_REQUEST_ENVELOPE_VERSION, scopedExecutionSchema } from './scopedExecution.js';
16
+ /**
17
+ * The request's input.
18
+ *
19
+ * Three formats, because the modalities genuinely differ: a chat request is a
20
+ * conversation, an embedding request is a string or a batch of strings, and
21
+ * pretending the latter is a one-message conversation loses the batch boundary
22
+ * that both metering and provider translation depend on.
23
+ */
24
+ export const inferenceInputSchema = z.discriminatedUnion('format', [
25
+ z.object({ format: z.literal('decisions'), decisions: decisionInputSchema }).strict(),
26
+ z
27
+ .object({
28
+ format: z.literal('messages'),
29
+ messages: z.array(inferenceMessageSchema).min(1),
30
+ })
31
+ .strict(),
32
+ z.object({ format: z.literal('text'), text: z.string() }).strict(),
33
+ z
34
+ .object({
35
+ format: z.literal('text_batch'),
36
+ texts: z.array(z.string()).min(1).max(2048),
37
+ })
38
+ .strict(),
39
+ ]);
40
+ /**
41
+ * What the edge records about the CALL, as opposed to its content.
42
+ *
43
+ * `.strict()` is a privacy control, not tidiness. Oxy never persists a user IP
44
+ * — raw, hashed or geo-derived — and this object is the natural place somebody
45
+ * would add one "for security". Strict means a producer that attaches `ip`,
46
+ * `country`, `userAgent` or `forwardedFor` fails the parse instead of quietly
47
+ * shipping it into the data plane and the telemetry stream behind it.
48
+ *
49
+ * `labels` is customer-supplied cost-attribution metadata (a team name, a
50
+ * feature flag). It is echoed on the receipt, so it must never be used for
51
+ * anything the customer would not want to read back to themselves.
52
+ */
53
+ export const clientRequestMetadataSchema = z
54
+ .object({
55
+ /** The public dialect the customer called. The response is rendered in it. */
56
+ apiFormat: inferenceApiFormatSchema,
57
+ /** The public path, e.g. `/v1/responses`. */
58
+ endpoint: z.string().min(1).max(256),
59
+ /** The customer's own correlation id, when they sent one. */
60
+ clientRequestId: z.string().min(1).max(128).optional(),
61
+ receivedAt: inferenceTimestampSchema,
62
+ labels: z.record(z.string(), z.string().max(256)).optional(),
63
+ })
64
+ .strict();
65
+ /**
66
+ * The model LINE a reference names, with any pinned revision dropped.
67
+ *
68
+ * Substitution is a question about the line — `anthropic/claude-opus-5` — never
69
+ * about the revision, so the comparisons below have to be made on it. Same split
70
+ * `resolveEdgeRoute` makes on the way in.
71
+ */
72
+ const modelLineOf = (reference) => {
73
+ const at = reference.indexOf('@');
74
+ return at === -1 ? reference : reference.slice(0, at);
75
+ };
76
+ /**
77
+ * The canonical internal request Oxy forwards to the data plane.
78
+ *
79
+ * `target` distinguishes the two questions a caller can ask — "serve THIS
80
+ * model" versus "choose one for me" — structurally. Everything downstream that
81
+ * must not silently substitute a model reads that discriminant rather than
82
+ * inferring intent from a string.
83
+ */
84
+ const inferenceRequestObjectSchema = z.object({
85
+ /** See `version.ts`: this is the Oxy→data-plane request envelope. */
86
+ schemaVersion: z.literal(2),
87
+ /** Recognized so v2 refuses rather than silently stripping a restriction. */
88
+ scopedExecution: scopedExecutionSchema.optional(),
89
+ attribution: inferenceAttributionSchema,
90
+ target: routingTargetSchema,
91
+ modality: inferenceModalitySchema,
92
+ input: inferenceInputSchema,
93
+ stream: z.boolean(),
94
+ maxOutputTokens: z.number().int().positive().safe().optional(),
95
+ sampling: samplingParametersSchema,
96
+ /** Absent means the route's own default reasoning behaviour. */
97
+ reasoning: inferenceReasoningSchema.optional(),
98
+ speech: inferenceSpeechParametersSchema.optional(),
99
+ /** Spoken output from a conversational model. Contract set 3.2.0. */
100
+ audioOutput: inferenceAudioOutputParametersSchema.optional(),
101
+ tools: z.array(toolDefinitionSchema).default([]),
102
+ toolChoice: toolChoiceSchema.optional(),
103
+ responseFormat: responseFormatSchema.optional(),
104
+ client: clientRequestMetadataSchema,
105
+ /** Present when the operation is safe to deduplicate on retry. */
106
+ idempotencyKey: idempotencyKeySchema.optional(),
107
+ /** The exact policy revision this request is served under. */
108
+ routingPolicy: routingPolicyReferenceSchema,
109
+ /**
110
+ * The routes the control plane has already authorized for this request, in
111
+ * PREFERENCE ORDER. The first entry is the primary route Oxy resolved; the
112
+ * data plane fails over by taking the next one.
113
+ *
114
+ * This is what closes the gap ADR 0010's amendment left open. That amendment
115
+ * assigns the data plane "failover within the destinations the policy
116
+ * authorized" — and the envelope named no destinations, so a data plane could
117
+ * only fail over by re-deriving the customer's policy from values it does not
118
+ * have. Enumerating the survivors instead means a route switch outside the
119
+ * policy is impossible BY CONSTRUCTION rather than by two enforcement engines
120
+ * agreeing in two languages.
121
+ *
122
+ * **Absent means no failover is authorized, never "choose freely."** It is
123
+ * the state every envelope built before this field existed is in, and the
124
+ * behaviour a data plane that reads no list must already have: resolve the
125
+ * `target` and serve it or fail. Permission is granted by an ENTRY, so its
126
+ * absence can only ever narrow, and there is no reading of an absent list
127
+ * that widens what may be served.
128
+ *
129
+ * An EMPTY list is refused rather than treated as that state. `[]` would say
130
+ * "no route is authorized at all", which contradicts an envelope that was
131
+ * built to be served, and it is exactly the "permission granted, destination
132
+ * unnamed" shape `authorizedRouteSchema` exists to make unrepresentable.
133
+ *
134
+ * No price rides here. Oxy sized the hold against the most expensive route
135
+ * the policy permits (`usageReservationRequestSchema.ceilingPriceVersionId`),
136
+ * and every entry is one that policy permitted, so no failover among them can
137
+ * exceed it. A per-route price would also be a second authority for ranking,
138
+ * beside the order this list already carries.
139
+ */
140
+ authorizedRoutes: z.array(authorizedRouteSchema).min(1).optional(),
141
+ });
142
+ export const inferenceRequestSchema = inferenceRequestObjectSchema.superRefine((request, ctx) => {
143
+ if (request.scopedExecution !== undefined)
144
+ ctx.addIssue({
145
+ code: 'custom',
146
+ path: ['scopedExecution'],
147
+ message: 'Scoped execution requires envelope v3.',
148
+ });
149
+ if (request.input.format === 'decisions' || request.client.apiFormat === 'decisions') {
150
+ const exactModel = request.target.kind === 'model' && request.target.modelReference.includes('@');
151
+ const foreignRoute = request.authorizedRoutes?.some((route) => request.target.kind !== 'model' ||
152
+ route.modelReference !== request.target.modelReference ||
153
+ route.substitution !== 'same_model');
154
+ if (request.input.format !== 'decisions' ||
155
+ request.client.apiFormat !== 'decisions' ||
156
+ request.modality !== 'text' ||
157
+ request.stream ||
158
+ !exactModel ||
159
+ foreignRoute ||
160
+ request.maxOutputTokens !== undefined ||
161
+ Object.keys(request.sampling).length > 0 ||
162
+ request.tools.length > 0 ||
163
+ request.toolChoice !== undefined ||
164
+ request.responseFormat !== undefined ||
165
+ request.reasoning !== undefined ||
166
+ request.speech !== undefined ||
167
+ request.audioOutput !== undefined) {
168
+ ctx.addIssue({
169
+ code: 'custom',
170
+ path: ['input'],
171
+ message: 'Decisions require exact-model typed nonstreaming input without generation controls or model substitution.',
172
+ });
173
+ }
174
+ }
175
+ const isSpeech = request.client.apiFormat === 'audio_speech';
176
+ // Contract set 4.0.0 (Kaana ADR 0002, decision 8): speech may stream. Each
177
+ // adapter still refuses in `Translate` what its provider cannot stream.
178
+ if (isSpeech &&
179
+ request.speech !== undefined &&
180
+ (request.modality !== 'audio' || request.input.format !== 'text')) {
181
+ ctx.addIssue({
182
+ code: 'custom',
183
+ path: ['speech'],
184
+ message: 'speech requires audio modality, text input and parameters',
185
+ });
186
+ }
187
+ if (!isSpeech && request.speech !== undefined) {
188
+ ctx.addIssue({
189
+ code: 'custom',
190
+ path: ['speech'],
191
+ message: 'speech parameters require the audio_speech API format',
192
+ });
193
+ }
194
+ if (request.audioOutput !== undefined) {
195
+ if (request.client.apiFormat !== 'chat_completions') {
196
+ ctx.addIssue({
197
+ code: 'custom',
198
+ path: ['audioOutput'],
199
+ message: 'spoken output from a conversational model requires the chat_completions API format',
200
+ });
201
+ }
202
+ if (request.modality !== 'audio' || request.input.format !== 'messages') {
203
+ ctx.addIssue({
204
+ code: 'custom',
205
+ path: ['audioOutput'],
206
+ message: 'spoken output requires audio modality and a messages input',
207
+ });
208
+ }
209
+ if (request.stream && request.audioOutput.format !== 'pcm') {
210
+ ctx.addIssue({
211
+ code: 'custom',
212
+ path: ['audioOutput', 'format'],
213
+ message: 'streamed spoken output is pcm; the other formats exist only whole',
214
+ });
215
+ }
216
+ }
217
+ if (request.toolChoice !== undefined && request.tools.length === 0) {
218
+ ctx.addIssue({
219
+ code: 'custom',
220
+ path: ['toolChoice'],
221
+ message: 'a tool choice requires at least one tool definition',
222
+ });
223
+ }
224
+ const toolNames = request.tools.map((tool) => tool.name);
225
+ if (new Set(toolNames).size !== toolNames.length) {
226
+ ctx.addIssue({
227
+ code: 'custom',
228
+ path: ['tools'],
229
+ message: 'tool names must be unique within one request',
230
+ });
231
+ }
232
+ const routes = request.authorizedRoutes;
233
+ // The emptiness is RE-CHECKED rather than assumed away by `.min(1)`. A
234
+ // failed `.min()` marks the parse dirty rather than aborting it, so zod runs
235
+ // this refinement with the empty array still in hand; `[]` is already
236
+ // refused above, and reading `routes[0]` here would throw instead.
237
+ if (routes === undefined || routes.length === 0)
238
+ return;
239
+ const primary = routes[0];
240
+ // The primary is not a substitution for itself, and every `substitution`
241
+ // value is read RELATIVE to it. A list whose first entry claims to be a
242
+ // cross-model substitute names no original to have substituted for.
243
+ if (primary.substitution !== 'same_model') {
244
+ ctx.addIssue({
245
+ code: 'custom',
246
+ path: ['authorizedRoutes', 0, 'substitution'],
247
+ message: 'the first authorized route is the primary and cannot be a substitution',
248
+ });
249
+ }
250
+ const primaryLine = modelLineOf(primary.modelReference);
251
+ // A request that named a concrete model is served or refused, never
252
+ // substituted, and one that PINNED a revision is served on exactly those
253
+ // weights. Both checks are on the primary, because a primary that already
254
+ // drifted makes every entry after it a substitution nobody labelled.
255
+ if (request.target.kind === 'model') {
256
+ const targetReference = request.target.modelReference;
257
+ const targetIsPinned = targetReference.includes('@');
258
+ if (targetIsPinned && primary.modelReference !== targetReference) {
259
+ ctx.addIssue({
260
+ code: 'custom',
261
+ path: ['authorizedRoutes', 0, 'modelReference'],
262
+ message: 'a pinned request is served on exactly the revision it pinned',
263
+ });
264
+ }
265
+ if (!targetIsPinned && primaryLine !== targetReference) {
266
+ ctx.addIssue({
267
+ code: 'custom',
268
+ path: ['authorizedRoutes', 0, 'modelReference'],
269
+ message: 'the primary authorized route must serve the model the request named',
270
+ });
271
+ }
272
+ if (targetIsPinned) {
273
+ for (const [index, route] of routes.entries()) {
274
+ if (route.substitution === 'cross_model') {
275
+ ctx.addIssue({
276
+ code: 'custom',
277
+ path: ['authorizedRoutes', index, 'substitution'],
278
+ message: 'a request that pinned a revision authorizes no cross-model substitute',
279
+ });
280
+ }
281
+ }
282
+ }
283
+ }
284
+ for (const [index, route] of routes.entries()) {
285
+ const line = modelLineOf(route.modelReference);
286
+ // A mislabelled entry is the whole failure mode: `same_model` on a
287
+ // different model line is a substitution wearing the label that needs no
288
+ // authorization, and `cross_model` on the same line claims an
289
+ // authorization the customer never had to give.
290
+ if (route.substitution === 'same_model' && line !== primaryLine) {
291
+ ctx.addIssue({
292
+ code: 'custom',
293
+ path: ['authorizedRoutes', index, 'substitution'],
294
+ message: `route ${index} serves ${line}, not ${primaryLine}, so it is a cross-model substitute`,
295
+ });
296
+ }
297
+ if (route.substitution === 'cross_model' && line === primaryLine) {
298
+ ctx.addIssue({
299
+ code: 'custom',
300
+ path: ['authorizedRoutes', index, 'substitution'],
301
+ message: `route ${index} serves ${primaryLine}, so it is same-model failover`,
302
+ });
303
+ }
304
+ }
305
+ // Failing over to the deployment that just failed is not failover. The
306
+ // duplicate would also make `routeSwitches` count a switch that changed
307
+ // nothing.
308
+ const deployments = routes.map((route) => route.deploymentId);
309
+ if (new Set(deployments).size !== deployments.length) {
310
+ ctx.addIssue({
311
+ code: 'custom',
312
+ path: ['authorizedRoutes'],
313
+ message: 'each deployment appears at most once in the authorized route list',
314
+ });
315
+ }
316
+ });
317
+ /** Explicit restrictive generation: old v2 receivers reject this version whole. */
318
+ export const scopedInferenceRequestSchema = inferenceRequestObjectSchema
319
+ .extend({
320
+ schemaVersion: z.literal(SCOPED_REQUEST_ENVELOPE_VERSION),
321
+ scopedExecution: scopedExecutionSchema,
322
+ })
323
+ .superRefine((request, ctx) => {
324
+ const { scopedExecution: scope, ...base } = request;
325
+ const legacy = inferenceRequestSchema.safeParse({ ...base, schemaVersion: 2 });
326
+ if (!legacy.success)
327
+ for (const issue of legacy.error.issues)
328
+ ctx.addIssue({ code: 'custom', path: issue.path, message: issue.message });
329
+ const principal = request.attribution.principal;
330
+ const route = request.authorizedRoutes?.[0];
331
+ if (request.input.format !== 'decisions' ||
332
+ request.client.apiFormat !== 'decisions' ||
333
+ scope.requestId !== request.attribution.requestId ||
334
+ scope.idempotencyKey !== request.idempotencyKey ||
335
+ scope.principal.accountId !== principal.billing.accountId ||
336
+ scope.principal.applicationId !== principal.applicationId ||
337
+ scope.principal.credentialId !== principal.credentialId ||
338
+ scope.principal.environment !== principal.environment ||
339
+ scope.policy.routingPolicyId !== request.routingPolicy.routingPolicyId ||
340
+ scope.policy.policyVersion !== request.routingPolicy.policyVersion ||
341
+ request.authorizedRoutes?.length !== 1 ||
342
+ route?.customerProviderCredential !== undefined ||
343
+ route?.deploymentId !== scope.deploymentId ||
344
+ route?.provider !== scope.provider ||
345
+ route?.modelReference !== scope.modelReference ||
346
+ request.target.kind !== 'model' ||
347
+ request.target.modelReference !== scope.modelReference) {
348
+ ctx.addIssue({
349
+ code: 'custom',
350
+ path: ['scopedExecution'],
351
+ message: 'Scoped execution must restrict the exact attributed decisions request and one platform route.',
352
+ });
353
+ }
354
+ });
355
+ /** Private repeated Auto is independently negotiated, never a v3 fixture renewal. */
356
+ export const privateAutoInferenceRequestSchema = inferenceRequestObjectSchema
357
+ .omit({ scopedExecution: true })
358
+ .extend({
359
+ schemaVersion: z.literal(PRIVATE_AUTO_REQUEST_ENVELOPE_VERSION),
360
+ privateAutoExecution: privateAutoExecutionSchema,
361
+ })
362
+ .strict()
363
+ .superRefine((request, ctx) => {
364
+ const { privateAutoExecution: permit, ...base } = request;
365
+ const legacy = inferenceRequestSchema.safeParse({ ...base, schemaVersion: 2 });
366
+ if (!legacy.success)
367
+ for (const issue of legacy.error.issues)
368
+ ctx.addIssue({ code: 'custom', path: issue.path, message: issue.message });
369
+ const input = privateAutoInputSchema.safeParse(request.input);
370
+ if (!input.success)
371
+ for (const issue of input.error.issues)
372
+ ctx.addIssue({ code: 'custom', path: ['input', ...issue.path], message: issue.message });
373
+ const principal = request.attribution.principal;
374
+ const route = request.authorizedRoutes?.[0];
375
+ if (request.attribution.userId !== undefined ||
376
+ request.client.apiFormat !== 'decisions' ||
377
+ permit.requestId !== request.attribution.requestId ||
378
+ permit.operationId !== request.idempotencyKey ||
379
+ permit.principal.accountId !== principal.billing.accountId ||
380
+ permit.principal.applicationId !== principal.applicationId ||
381
+ permit.principal.credentialId !== principal.credentialId ||
382
+ permit.principal.environment !== principal.environment ||
383
+ !principal.inferenceScopes.includes('inference:invoke') ||
384
+ request.client.endpoint !== '/internal/auto-classification' ||
385
+ permit.policy.routingPolicyId !== request.routingPolicy.routingPolicyId ||
386
+ permit.policy.policyVersion !== request.routingPolicy.policyVersion ||
387
+ request.authorizedRoutes?.length !== 1 ||
388
+ route?.customerProviderCredential !== undefined ||
389
+ route?.deploymentId !== permit.deploymentId ||
390
+ route?.provider !== permit.provider ||
391
+ route?.modelReference !== permit.modelReference ||
392
+ request.target.kind !== 'model' ||
393
+ request.target.modelReference !== permit.modelReference ||
394
+ route.regions.length !== permit.regions.length ||
395
+ route.regions.some((region) => !permit.regions.includes(region)) ||
396
+ Date.parse(permit.runtimeExpiresAt) <= Date.parse(request.client.receivedAt) ||
397
+ Date.parse(permit.runtimeExpiresAt) > Date.parse(request.client.receivedAt) + 1000) {
398
+ ctx.addIssue({
399
+ code: 'custom',
400
+ path: ['privateAutoExecution'],
401
+ message: 'Private Auto must bind one exact attributed decisions child and platform route.',
402
+ });
403
+ }
404
+ });
@@ -0,0 +1,90 @@
1
+ /**
2
+ * The routing target and the ordered authorized routes of the signed envelope.
3
+ *
4
+ * Moved from `@oxy.so/contracts` 4.12.0 (`src/inference/routingPolicy.ts`) by Kaana ADR 0002.
5
+ * The routing POLICY record (`routingPolicySchema`, its scope and fallback
6
+ * controls) stays in `@oxy.so/contracts`: it is a Console resource.
7
+ */
8
+ import { z } from 'zod';
9
+ /**
10
+ * What a policy resolves to when a caller names no model.
11
+ *
12
+ * A discriminated union rather than two optional fields, so "which one did the
13
+ * customer configure" is never a question about which field is non-null.
14
+ */
15
+ export declare const routingTargetSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
16
+ kind: z.ZodLiteral<"model">;
17
+ modelReference: z.ZodString;
18
+ }, z.core.$strict>, z.ZodObject<{
19
+ kind: z.ZodLiteral<"routing_profile_id">;
20
+ routingProfileId: z.ZodString;
21
+ }, z.core.$strict>], "kind">;
22
+ /**
23
+ * One route the control plane has already authorized for one request.
24
+ *
25
+ * **Authorization is an ENTRY, never a boolean.** The same stance
26
+ * `inference_routing_policy_fallbacks` takes in storage: being allowed to serve
27
+ * a route IS appearing here, and every entry names its destination. A flag
28
+ * saying "substitution allowed" without naming the destination is exactly the
29
+ * silent substitution the platform forbids, and it invents a "flag set, list
30
+ * empty" state somebody then has to decide what to do with.
31
+ *
32
+ * Discriminated on `substitution`, relative to the FIRST entry — the primary
33
+ * route Oxy resolved:
34
+ *
35
+ * - `same_model` serves the same model line as the primary. This is
36
+ * availability failover between deployments of one model.
37
+ * - `cross_model` serves a DIFFERENT model line, and is expressible only with
38
+ * `authorizedByPolicy: true` as a literal. So "a model was substituted
39
+ * without the customer authorizing it" is not a sentence this contract can
40
+ * say — the same construction `inferenceRouteSwitchDetailSchema` uses to make
41
+ * the resulting route-switch event unreportable.
42
+ */
43
+ export declare const authorizedRouteSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
44
+ deploymentId: z.ZodString;
45
+ modelReference: z.ZodString;
46
+ provider: z.ZodString;
47
+ regions: z.ZodArray<z.ZodString>;
48
+ maxFundingTier: z.ZodOptional<z.ZodEnum<{
49
+ free: "free";
50
+ credit: "credit";
51
+ paid: "paid";
52
+ }>>;
53
+ customerProviderCredential: z.ZodOptional<z.ZodObject<{
54
+ credentialHandle: z.ZodString;
55
+ credentialRevision: z.ZodNumber;
56
+ ownerAccountId: z.core.$ZodBranded<z.ZodString, "OxyAccountId", "out">;
57
+ connectionId: z.ZodString;
58
+ environment: z.ZodEnum<{
59
+ development: "development";
60
+ staging: "staging";
61
+ production: "production";
62
+ }>;
63
+ }, z.core.$strict>>;
64
+ substitution: z.ZodLiteral<"same_model">;
65
+ }, z.core.$strict>, z.ZodObject<{
66
+ authorizedByPolicy: z.ZodLiteral<true>;
67
+ deploymentId: z.ZodString;
68
+ modelReference: z.ZodString;
69
+ provider: z.ZodString;
70
+ regions: z.ZodArray<z.ZodString>;
71
+ maxFundingTier: z.ZodOptional<z.ZodEnum<{
72
+ free: "free";
73
+ credit: "credit";
74
+ paid: "paid";
75
+ }>>;
76
+ customerProviderCredential: z.ZodOptional<z.ZodObject<{
77
+ credentialHandle: z.ZodString;
78
+ credentialRevision: z.ZodNumber;
79
+ ownerAccountId: z.core.$ZodBranded<z.ZodString, "OxyAccountId", "out">;
80
+ connectionId: z.ZodString;
81
+ environment: z.ZodEnum<{
82
+ development: "development";
83
+ staging: "staging";
84
+ production: "production";
85
+ }>;
86
+ }, z.core.$strict>>;
87
+ substitution: z.ZodLiteral<"cross_model">;
88
+ }, z.core.$strict>], "substitution">;
89
+ export type RoutingTarget = z.infer<typeof routingTargetSchema>;
90
+ export type AuthorizedRoute = z.infer<typeof authorizedRouteSchema>;
@@ -0,0 +1,139 @@
1
+ /**
2
+ * The routing target and the ordered authorized routes of the signed envelope.
3
+ *
4
+ * Moved from `@oxy.so/contracts` 4.12.0 (`src/inference/routingPolicy.ts`) by Kaana ADR 0002.
5
+ * The routing POLICY record (`routingPolicySchema`, its scope and fallback
6
+ * controls) stays in `@oxy.so/contracts`: it is a Console resource.
7
+ */
8
+ import { z } from 'zod';
9
+ import { inferenceRegionSchema, modelReferenceSchema } from '../public/identifiers.js';
10
+ import { kaanaCredentialHandleSchema } from './credentialControl.js';
11
+ import { deploymentIdSchema, inferenceFundingTierSchema, inferenceProviderSlugSchema, oxyAccountIdSchema, routingProfileIdSchema, } from './identifiers.js';
12
+ /**
13
+ * What a policy resolves to when a caller names no model.
14
+ *
15
+ * A discriminated union rather than two optional fields, so "which one did the
16
+ * customer configure" is never a question about which field is non-null.
17
+ */
18
+ export const routingTargetSchema = z.discriminatedUnion('kind', [
19
+ z
20
+ .object({
21
+ kind: z.literal('model'),
22
+ modelReference: modelReferenceSchema,
23
+ })
24
+ .strict(),
25
+ z
26
+ .object({
27
+ kind: z.literal('routing_profile_id'),
28
+ routingProfileId: routingProfileIdSchema,
29
+ })
30
+ .strict(),
31
+ ]);
32
+ /**
33
+ * What every authorized route carries, whichever kind it is.
34
+ *
35
+ * Exactly what EXECUTING a route needs, and nothing a policy could be
36
+ * re-derived from. There is no price, no data-retention flag, no licence id and
37
+ * no availability scope here: those are the values the control plane already
38
+ * evaluated to put this entry in the list, and repeating them would invite the
39
+ * data plane to evaluate them a second time — differently, in another language.
40
+ *
41
+ * `regions` is plural and matches `modelDeploymentSchema.regions`, because a
42
+ * deployment declares every ATTESTED region it MAY serve from and choosing among
43
+ * them is routing execution (ADR 0006). Oxy checked the whole set against the
44
+ * customer's residency controls as a SUBSET, so any non-empty region in this
45
+ * list is one the policy permits and the data plane's choice among them cannot
46
+ * escape it. An empty list means no location was attested and can only survive a
47
+ * request with no explicit regional control; it never means global. Collapsing
48
+ * the set to one invented region would make Oxy take a decision the boundary
49
+ * assigns elsewhere.
50
+ */
51
+ const authorizedRouteFields = {
52
+ /** Which concrete endpoint. Opaque to customers; the data plane's own key. */
53
+ deploymentId: deploymentIdSchema,
54
+ /** Always revision-pinned: the entry names the exact weights to serve. */
55
+ modelReference: modelReferenceSchema,
56
+ provider: inferenceProviderSlugSchema,
57
+ regions: z.array(inferenceRegionSchema),
58
+ /**
59
+ * The dearest funding tier the data plane may spend on this route. Absent
60
+ * means no ceiling. Present, the data plane walks only provider keys whose
61
+ * STATED tier is at or below it — a key whose economics nobody stated is
62
+ * never admitted — and refuses the route rather than fall through to a
63
+ * dearer key. It is how Oxy authorizes a route only while credit funds it
64
+ * (Alia Pro and Ultra spend credits only, ADR 0038). Meaningless with a
65
+ * customer credential, which the provider bills to the customer.
66
+ */
67
+ maxFundingTier: inferenceFundingTierSchema.optional(),
68
+ /**
69
+ * Exact customer credential binding. Absence means a platform credential;
70
+ * presence names the immutable Kaana generation this route may use.
71
+ */
72
+ customerProviderCredential: z
73
+ .object({
74
+ credentialHandle: kaanaCredentialHandleSchema,
75
+ credentialRevision: z.number().int().positive().safe(),
76
+ ownerAccountId: oxyAccountIdSchema,
77
+ connectionId: z.string().min(1).max(128),
78
+ environment: z.enum(['development', 'staging', 'production']),
79
+ })
80
+ .strict()
81
+ .optional(),
82
+ };
83
+ /**
84
+ * One route the control plane has already authorized for one request.
85
+ *
86
+ * **Authorization is an ENTRY, never a boolean.** The same stance
87
+ * `inference_routing_policy_fallbacks` takes in storage: being allowed to serve
88
+ * a route IS appearing here, and every entry names its destination. A flag
89
+ * saying "substitution allowed" without naming the destination is exactly the
90
+ * silent substitution the platform forbids, and it invents a "flag set, list
91
+ * empty" state somebody then has to decide what to do with.
92
+ *
93
+ * Discriminated on `substitution`, relative to the FIRST entry — the primary
94
+ * route Oxy resolved:
95
+ *
96
+ * - `same_model` serves the same model line as the primary. This is
97
+ * availability failover between deployments of one model.
98
+ * - `cross_model` serves a DIFFERENT model line, and is expressible only with
99
+ * `authorizedByPolicy: true` as a literal. So "a model was substituted
100
+ * without the customer authorizing it" is not a sentence this contract can
101
+ * say — the same construction `inferenceRouteSwitchDetailSchema` uses to make
102
+ * the resulting route-switch event unreportable.
103
+ */
104
+ export const authorizedRouteSchema = z
105
+ .discriminatedUnion('substitution', [
106
+ z
107
+ .object({
108
+ substitution: z.literal('same_model'),
109
+ ...authorizedRouteFields,
110
+ })
111
+ .strict(),
112
+ z
113
+ .object({
114
+ substitution: z.literal('cross_model'),
115
+ ...authorizedRouteFields,
116
+ /** Literal `true`: an unauthorized substitution cannot be expressed. */
117
+ authorizedByPolicy: z.literal(true),
118
+ })
119
+ .strict(),
120
+ ])
121
+ .superRefine((route, ctx) => {
122
+ if (route.maxFundingTier !== undefined && route.customerProviderCredential !== undefined) {
123
+ ctx.addIssue({
124
+ code: 'custom',
125
+ path: ['maxFundingTier'],
126
+ message: 'a funding ceiling bounds platform keys; a customer credential is billed to the customer',
127
+ });
128
+ }
129
+ // Same rule as `modelDeploymentSchema`: a route serves specific weights. An
130
+ // unpinned entry would leave the data plane choosing a revision, which is
131
+ // the one substitution the customer never authorized by naming a model.
132
+ if (!route.modelReference.includes('@')) {
133
+ ctx.addIssue({
134
+ code: 'custom',
135
+ path: ['modelReference'],
136
+ message: 'an authorized route must pin an immutable revision (<publisher>/<model>@<revision>)',
137
+ });
138
+ }
139
+ });