@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,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
|
+
});
|