@oxyhq/contracts 0.28.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/identity.js +33 -1
- package/dist/cjs/index.js +20 -7
- package/dist/cjs/inference/aliaModelRelease.js +250 -0
- package/dist/cjs/inference/catalogue.js +1 -4
- package/dist/cjs/inference/errors.js +108 -9
- package/dist/cjs/inference/identifiers.js +23 -3
- package/dist/cjs/inference/request.js +188 -5
- package/dist/cjs/inference/routingPolicy.js +81 -2
- package/dist/cjs/inference/streamEvents.js +38 -1
- package/dist/cjs/inference/usage.js +32 -1
- package/dist/cjs/inference/version.js +24 -4
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/identity.js +32 -0
- package/dist/esm/index.js +8 -3
- package/dist/esm/inference/aliaModelRelease.js +247 -0
- package/dist/esm/inference/catalogue.js +2 -5
- package/dist/esm/inference/errors.js +108 -9
- package/dist/esm/inference/identifiers.js +22 -2
- package/dist/esm/inference/request.js +189 -6
- package/dist/esm/inference/routingPolicy.js +81 -2
- package/dist/esm/inference/streamEvents.js +38 -1
- package/dist/esm/inference/usage.js +32 -1
- package/dist/esm/inference/version.js +24 -4
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/identity.d.ts +59 -1
- package/dist/types/index.d.ts +7 -5
- package/dist/types/inference/accountBilling.d.ts +26 -26
- package/dist/types/inference/aliaModelRelease.d.ts +597 -0
- package/dist/types/inference/catalogue.d.ts +6 -6
- package/dist/types/inference/entitlement.d.ts +4 -4
- package/dist/types/inference/errors.d.ts +46 -10
- package/dist/types/inference/identifiers.d.ts +20 -2
- package/dist/types/inference/money.d.ts +4 -4
- package/dist/types/inference/priceVersion.d.ts +14 -14
- package/dist/types/inference/providerConnection.d.ts +4 -4
- package/dist/types/inference/request.d.ts +291 -5
- package/dist/types/inference/routingPolicy.d.ts +109 -13
- package/dist/types/inference/streamEvents.d.ts +84 -48
- package/dist/types/inference/usage.d.ts +103 -79
- package/dist/types/inference/version.d.ts +24 -4
- package/dist/types/keyRecovery.d.ts +4 -4
- package/package.json +1 -1
|
@@ -12,11 +12,17 @@
|
|
|
12
12
|
*
|
|
13
13
|
* What the envelope carries that a provider request does not: the resolved
|
|
14
14
|
* attribution block (who pays, which application, which credential, which
|
|
15
|
-
* delegated user), the exact routing policy reference
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* delegated user), the exact routing policy reference, the routes that policy
|
|
16
|
+
* has already authorized, and the customer's idempotency key. Those are the
|
|
17
|
+
* fields that make a request billable and explainable, and they are resolved
|
|
18
|
+
* BEFORE the request enters the data plane.
|
|
18
19
|
*
|
|
19
|
-
*
|
|
20
|
+
* The policy VALUES never travel. `routingPolicy` is a reference — provenance
|
|
21
|
+
* for the receipt — and `authorizedRoutes` is the result of applying the policy,
|
|
22
|
+
* in preference order, so the data plane needs no policy semantics to fail over.
|
|
23
|
+
*
|
|
24
|
+
* Decided in: docs/adr/0010-public-api-compatibility.md,
|
|
25
|
+
* docs/adr/0017-authorized-routes-in-the-envelope.md.
|
|
20
26
|
*/
|
|
21
27
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
28
|
exports.inferenceRequestSchema = exports.clientRequestMetadataSchema = exports.responseFormatSchema = exports.toolChoiceSchema = exports.toolDefinitionSchema = exports.samplingParametersSchema = exports.inferenceInputSchema = exports.inferenceMessageSchema = exports.inferenceMessageRoleSchema = exports.inferenceToolCallSchema = exports.inferenceContentPartSchema = exports.inferenceContentSourceSchema = void 0;
|
|
@@ -45,7 +51,38 @@ exports.inferenceContentSourceSchema = zod_1.z.discriminatedUnion('kind', [
|
|
|
45
51
|
})
|
|
46
52
|
.strict(),
|
|
47
53
|
]);
|
|
48
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* One part of a message's content. A message is always a list of parts.
|
|
56
|
+
*
|
|
57
|
+
* ## `refusal` is a member and `reasoning` is not, and the asymmetry is the point
|
|
58
|
+
*
|
|
59
|
+
* A model that DECLINES says why, and those words are meant for the customer:
|
|
60
|
+
* they are the difference between "rephrase this" and "stop asking". Before this
|
|
61
|
+
* member existed there was nowhere in an {@link InferenceMessage} to put them, so
|
|
62
|
+
* a non-streaming fold kept `finishReason: 'refusal'` and dropped the sentence —
|
|
63
|
+
* the customer learned they were refused and not why, while a streaming caller of
|
|
64
|
+
* the same request got the whole explanation on the `refusal` delta channel.
|
|
65
|
+
*
|
|
66
|
+
* Reasoning is the opposite case and stays absent. It is the model's private
|
|
67
|
+
* working, and a `text` part is where somebody would put it — which renders
|
|
68
|
+
* private reasoning to the customer AS the answer, the product bug the delta
|
|
69
|
+
* channels exist to prevent. An opaque per-block reasoning blob crossing this
|
|
70
|
+
* boundary needs a home nobody has chosen yet, and inventing one here would
|
|
71
|
+
* choose it by accident.
|
|
72
|
+
*
|
|
73
|
+
* Both public dialects can carry a refusal, which is what separates the two
|
|
74
|
+
* cases at the boundary as well as in principle: `refusal` is OpenAI's OWN field
|
|
75
|
+
* in both of its shapes (`delta.refusal` streaming, `message.refusal`
|
|
76
|
+
* non-streaming), while reasoning has no OpenAI field at all — the
|
|
77
|
+
* `reasoning_content`/`reasoning` spellings an OpenAI-compatible provider emits
|
|
78
|
+
* are provider extensions. So carrying the refusal costs no dialect its
|
|
79
|
+
* standard-client parseability, and carrying reasoning would.
|
|
80
|
+
*
|
|
81
|
+
* The part is a MEMBER rather than a field because a refusal need not have text:
|
|
82
|
+
* an Anthropic `stop_reason: "refusal"` maps to the finish reason and separates
|
|
83
|
+
* no words from the answer, while an OpenAI-compatible `refusal` does. A required
|
|
84
|
+
* field would force the first provider to invent a sentence.
|
|
85
|
+
*/
|
|
49
86
|
exports.inferenceContentPartSchema = zod_1.z.discriminatedUnion('type', [
|
|
50
87
|
zod_1.z.object({ type: zod_1.z.literal('text'), text: zod_1.z.string() }).strict(),
|
|
51
88
|
zod_1.z
|
|
@@ -64,6 +101,11 @@ exports.inferenceContentPartSchema = zod_1.z.discriminatedUnion('type', [
|
|
|
64
101
|
filename: zod_1.z.string().max(255).optional(),
|
|
65
102
|
})
|
|
66
103
|
.strict(),
|
|
104
|
+
/**
|
|
105
|
+
* A model's explanation for declining. Its own part, never a `text` one, so no
|
|
106
|
+
* renderer can present a refusal as the answer that was asked for.
|
|
107
|
+
*/
|
|
108
|
+
zod_1.z.object({ type: zod_1.z.literal('refusal'), text: zod_1.z.string() }).strict(),
|
|
67
109
|
]);
|
|
68
110
|
/**
|
|
69
111
|
* A tool call an assistant made, in the normalized form.
|
|
@@ -128,6 +170,20 @@ exports.inferenceMessageSchema = zod_1.z
|
|
|
128
170
|
message: 'only an assistant message makes tool calls',
|
|
129
171
|
});
|
|
130
172
|
}
|
|
173
|
+
// Same rule as `toolCalls`, for the same reason: only the assistant can
|
|
174
|
+
// decline, so a refusal on any other role is a part every provider would
|
|
175
|
+
// silently ignore — and one a renderer might not.
|
|
176
|
+
if (message.role !== 'assistant') {
|
|
177
|
+
for (const [index, part] of message.content.entries()) {
|
|
178
|
+
if (part.type === 'refusal') {
|
|
179
|
+
ctx.addIssue({
|
|
180
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
181
|
+
path: ['content', index, 'type'],
|
|
182
|
+
message: 'only an assistant message carries a refusal',
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
131
187
|
});
|
|
132
188
|
/**
|
|
133
189
|
* The request's input.
|
|
@@ -241,6 +297,17 @@ exports.clientRequestMetadataSchema = zod_1.z
|
|
|
241
297
|
/* -------------------------------------------------------------------------- */
|
|
242
298
|
/* The envelope */
|
|
243
299
|
/* -------------------------------------------------------------------------- */
|
|
300
|
+
/**
|
|
301
|
+
* The model LINE a reference names, with any pinned revision dropped.
|
|
302
|
+
*
|
|
303
|
+
* Substitution is a question about the line — `anthropic/claude-opus-5` — never
|
|
304
|
+
* about the revision, so the comparisons below have to be made on it. Same split
|
|
305
|
+
* `resolveEdgeRoute` makes on the way in.
|
|
306
|
+
*/
|
|
307
|
+
const modelLineOf = (reference) => {
|
|
308
|
+
const at = reference.indexOf('@');
|
|
309
|
+
return at === -1 ? reference : reference.slice(0, at);
|
|
310
|
+
};
|
|
244
311
|
/**
|
|
245
312
|
* The canonical internal request Oxy forwards to the data plane.
|
|
246
313
|
*
|
|
@@ -268,6 +335,38 @@ exports.inferenceRequestSchema = zod_1.z
|
|
|
268
335
|
idempotencyKey: identifiers_1.idempotencyKeySchema.optional(),
|
|
269
336
|
/** The exact policy revision this request is served under. */
|
|
270
337
|
routingPolicy: routingPolicy_1.routingPolicyReferenceSchema,
|
|
338
|
+
/**
|
|
339
|
+
* The routes the control plane has already authorized for this request, in
|
|
340
|
+
* PREFERENCE ORDER. The first entry is the primary route Oxy resolved; the
|
|
341
|
+
* data plane fails over by taking the next one.
|
|
342
|
+
*
|
|
343
|
+
* This is what closes the gap ADR 0010's amendment left open. That amendment
|
|
344
|
+
* assigns the data plane "failover within the destinations the policy
|
|
345
|
+
* authorized" — and the envelope named no destinations, so a data plane could
|
|
346
|
+
* only fail over by re-deriving the customer's policy from values it does not
|
|
347
|
+
* have. Enumerating the survivors instead means a route switch outside the
|
|
348
|
+
* policy is impossible BY CONSTRUCTION rather than by two enforcement engines
|
|
349
|
+
* agreeing in two languages.
|
|
350
|
+
*
|
|
351
|
+
* **Absent means no failover is authorized, never "choose freely."** It is
|
|
352
|
+
* the state every envelope built before this field existed is in, and the
|
|
353
|
+
* behaviour a data plane that reads no list must already have: resolve the
|
|
354
|
+
* `target` and serve it or fail. Permission is granted by an ENTRY, so its
|
|
355
|
+
* absence can only ever narrow, and there is no reading of an absent list
|
|
356
|
+
* that widens what may be served.
|
|
357
|
+
*
|
|
358
|
+
* An EMPTY list is refused rather than treated as that state. `[]` would say
|
|
359
|
+
* "no route is authorized at all", which contradicts an envelope that was
|
|
360
|
+
* built to be served, and it is exactly the "permission granted, destination
|
|
361
|
+
* unnamed" shape `authorizedRouteSchema` exists to make unrepresentable.
|
|
362
|
+
*
|
|
363
|
+
* No price rides here. Oxy sized the hold against the most expensive route
|
|
364
|
+
* the policy permits (`usageReservationRequestSchema.ceilingPriceVersionId`),
|
|
365
|
+
* and every entry is one that policy permitted, so no failover among them can
|
|
366
|
+
* exceed it. A per-route price would also be a second authority for ranking,
|
|
367
|
+
* beside the order this list already carries.
|
|
368
|
+
*/
|
|
369
|
+
authorizedRoutes: zod_1.z.array(routingPolicy_1.authorizedRouteSchema).min(1).optional(),
|
|
271
370
|
})
|
|
272
371
|
.superRefine((request, ctx) => {
|
|
273
372
|
if (request.toolChoice !== undefined && request.tools.length === 0) {
|
|
@@ -285,4 +384,88 @@ exports.inferenceRequestSchema = zod_1.z
|
|
|
285
384
|
message: 'tool names must be unique within one request',
|
|
286
385
|
});
|
|
287
386
|
}
|
|
387
|
+
const routes = request.authorizedRoutes;
|
|
388
|
+
// The emptiness is RE-CHECKED rather than assumed away by `.min(1)`. A
|
|
389
|
+
// failed `.min()` marks the parse dirty rather than aborting it, so zod runs
|
|
390
|
+
// this refinement with the empty array still in hand; `[]` is already
|
|
391
|
+
// refused above, and reading `routes[0]` here would throw instead.
|
|
392
|
+
if (routes === undefined || routes.length === 0)
|
|
393
|
+
return;
|
|
394
|
+
const primary = routes[0];
|
|
395
|
+
// The primary is not a substitution for itself, and every `substitution`
|
|
396
|
+
// value is read RELATIVE to it. A list whose first entry claims to be a
|
|
397
|
+
// cross-model substitute names no original to have substituted for.
|
|
398
|
+
if (primary.substitution !== 'same_model') {
|
|
399
|
+
ctx.addIssue({
|
|
400
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
401
|
+
path: ['authorizedRoutes', 0, 'substitution'],
|
|
402
|
+
message: 'the first authorized route is the primary and cannot be a substitution',
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
const primaryLine = modelLineOf(primary.modelReference);
|
|
406
|
+
// A request that named a concrete model is served or refused, never
|
|
407
|
+
// substituted, and one that PINNED a revision is served on exactly those
|
|
408
|
+
// weights. Both checks are on the primary, because a primary that already
|
|
409
|
+
// drifted makes every entry after it a substitution nobody labelled.
|
|
410
|
+
if (request.target.kind === 'model') {
|
|
411
|
+
const targetReference = request.target.modelReference;
|
|
412
|
+
const targetIsPinned = targetReference.includes('@');
|
|
413
|
+
if (targetIsPinned && primary.modelReference !== targetReference) {
|
|
414
|
+
ctx.addIssue({
|
|
415
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
416
|
+
path: ['authorizedRoutes', 0, 'modelReference'],
|
|
417
|
+
message: 'a pinned request is served on exactly the revision it pinned',
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
if (!targetIsPinned && primaryLine !== targetReference) {
|
|
421
|
+
ctx.addIssue({
|
|
422
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
423
|
+
path: ['authorizedRoutes', 0, 'modelReference'],
|
|
424
|
+
message: 'the primary authorized route must serve the model the request named',
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
if (targetIsPinned) {
|
|
428
|
+
for (const [index, route] of routes.entries()) {
|
|
429
|
+
if (route.substitution === 'cross_model') {
|
|
430
|
+
ctx.addIssue({
|
|
431
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
432
|
+
path: ['authorizedRoutes', index, 'substitution'],
|
|
433
|
+
message: 'a request that pinned a revision authorizes no cross-model substitute',
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
for (const [index, route] of routes.entries()) {
|
|
440
|
+
const line = modelLineOf(route.modelReference);
|
|
441
|
+
// A mislabelled entry is the whole failure mode: `same_model` on a
|
|
442
|
+
// different model line is a substitution wearing the label that needs no
|
|
443
|
+
// authorization, and `cross_model` on the same line claims an
|
|
444
|
+
// authorization the customer never had to give.
|
|
445
|
+
if (route.substitution === 'same_model' && line !== primaryLine) {
|
|
446
|
+
ctx.addIssue({
|
|
447
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
448
|
+
path: ['authorizedRoutes', index, 'substitution'],
|
|
449
|
+
message: `route ${index} serves ${line}, not ${primaryLine}, so it is a cross-model substitute`,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
if (route.substitution === 'cross_model' && line === primaryLine) {
|
|
453
|
+
ctx.addIssue({
|
|
454
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
455
|
+
path: ['authorizedRoutes', index, 'substitution'],
|
|
456
|
+
message: `route ${index} serves ${primaryLine}, so it is same-model failover`,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
// Failing over to the deployment that just failed is not failover. The
|
|
461
|
+
// duplicate would also make `routeSwitches` count a switch that changed
|
|
462
|
+
// nothing.
|
|
463
|
+
const deployments = routes.map((route) => route.deploymentId);
|
|
464
|
+
if (new Set(deployments).size !== deployments.length) {
|
|
465
|
+
ctx.addIssue({
|
|
466
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
467
|
+
path: ['authorizedRoutes'],
|
|
468
|
+
message: 'each deployment appears at most once in the authorized route list',
|
|
469
|
+
});
|
|
470
|
+
}
|
|
288
471
|
});
|
|
@@ -23,10 +23,17 @@
|
|
|
23
23
|
* REJECTED, rather than being quietly resolved by whichever field the executor
|
|
24
24
|
* happens to read first. That rejection is `routingPolicySchema`'s refinement.
|
|
25
25
|
*
|
|
26
|
-
*
|
|
26
|
+
* **The policy itself never crosses to the data plane.** What crosses is
|
|
27
|
+
* {@link authorizedRouteSchema} — the candidate routes that SURVIVED these
|
|
28
|
+
* controls, in preference order — plus {@link routingPolicyReferenceSchema} as
|
|
29
|
+
* provenance for the receipt. The data plane holds no control value and needs
|
|
30
|
+
* none: it fails over by taking the next entry. See ADR 0017.
|
|
31
|
+
*
|
|
32
|
+
* Decided in: docs/adr/0008-catalogue-concept-separation.md,
|
|
33
|
+
* docs/adr/0017-authorized-routes-in-the-envelope.md, issue #972 workstream 6.
|
|
27
34
|
*/
|
|
28
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
-
exports.routingPolicyReferenceSchema = exports.routingPolicySchema = exports.routingFallbackPolicySchema = exports.routingPolicyScopeSchema = exports.routingTargetSchema = void 0;
|
|
36
|
+
exports.authorizedRouteSchema = exports.routingPolicyReferenceSchema = exports.routingPolicySchema = exports.routingFallbackPolicySchema = exports.routingPolicyScopeSchema = exports.routingTargetSchema = void 0;
|
|
30
37
|
const zod_1 = require("zod");
|
|
31
38
|
const identifiers_1 = require("./identifiers");
|
|
32
39
|
const money_1 = require("./money");
|
|
@@ -211,3 +218,75 @@ exports.routingPolicyReferenceSchema = zod_1.z
|
|
|
211
218
|
policyVersion: zod_1.z.number().int().positive().safe(),
|
|
212
219
|
})
|
|
213
220
|
.strict();
|
|
221
|
+
/* -------------------------------------------------------------------------- */
|
|
222
|
+
/* Pre-authorized routes */
|
|
223
|
+
/* -------------------------------------------------------------------------- */
|
|
224
|
+
/**
|
|
225
|
+
* What every authorized route carries, whichever kind it is.
|
|
226
|
+
*
|
|
227
|
+
* Exactly what EXECUTING a route needs, and nothing a policy could be
|
|
228
|
+
* re-derived from. There is no price, no data-retention flag, no licence id and
|
|
229
|
+
* no availability scope here: those are the values the control plane already
|
|
230
|
+
* evaluated to put this entry in the list, and repeating them would invite the
|
|
231
|
+
* data plane to evaluate them a second time — differently, in another language.
|
|
232
|
+
*
|
|
233
|
+
* `regions` is plural and matches `modelDeploymentSchema.regions`, because a
|
|
234
|
+
* deployment declares every region it MAY serve from and choosing among them is
|
|
235
|
+
* routing execution (ADR 0006). Oxy checked the whole set against the customer's
|
|
236
|
+
* residency controls as a SUBSET, so any region in this list is one the policy
|
|
237
|
+
* permits and the data plane's choice among them cannot escape it. Collapsing it
|
|
238
|
+
* to one region would make Oxy take a decision the boundary assigns elsewhere.
|
|
239
|
+
*/
|
|
240
|
+
const authorizedRouteFields = {
|
|
241
|
+
/** Which concrete endpoint. Opaque to customers; the data plane's own key. */
|
|
242
|
+
deploymentId: identifiers_1.deploymentIdSchema,
|
|
243
|
+
/** Always revision-pinned: the entry names the exact weights to serve. */
|
|
244
|
+
modelReference: identifiers_1.modelReferenceSchema,
|
|
245
|
+
provider: identifiers_1.inferenceProviderSlugSchema,
|
|
246
|
+
regions: zod_1.z.array(identifiers_1.inferenceRegionSchema).min(1),
|
|
247
|
+
};
|
|
248
|
+
/**
|
|
249
|
+
* One route the control plane has already authorized for one request.
|
|
250
|
+
*
|
|
251
|
+
* **Authorization is an ENTRY, never a boolean.** The same stance
|
|
252
|
+
* `inference_routing_policy_fallbacks` takes in storage: being allowed to serve
|
|
253
|
+
* a route IS appearing here, and every entry names its destination. A flag
|
|
254
|
+
* saying "substitution allowed" without naming the destination is exactly the
|
|
255
|
+
* silent substitution the platform forbids, and it invents a "flag set, list
|
|
256
|
+
* empty" state somebody then has to decide what to do with.
|
|
257
|
+
*
|
|
258
|
+
* Discriminated on `substitution`, relative to the FIRST entry — the primary
|
|
259
|
+
* route Oxy resolved:
|
|
260
|
+
*
|
|
261
|
+
* - `same_model` serves the same model line as the primary. This is
|
|
262
|
+
* availability failover between deployments of one model.
|
|
263
|
+
* - `cross_model` serves a DIFFERENT model line, and is expressible only with
|
|
264
|
+
* `authorizedByPolicy: true` as a literal. So "a model was substituted
|
|
265
|
+
* without the customer authorizing it" is not a sentence this contract can
|
|
266
|
+
* say — the same construction `inferenceRouteSwitchDetailSchema` uses to make
|
|
267
|
+
* the resulting route-switch event unreportable.
|
|
268
|
+
*/
|
|
269
|
+
exports.authorizedRouteSchema = zod_1.z
|
|
270
|
+
.discriminatedUnion('substitution', [
|
|
271
|
+
zod_1.z.object({ substitution: zod_1.z.literal('same_model'), ...authorizedRouteFields }).strict(),
|
|
272
|
+
zod_1.z
|
|
273
|
+
.object({
|
|
274
|
+
substitution: zod_1.z.literal('cross_model'),
|
|
275
|
+
...authorizedRouteFields,
|
|
276
|
+
/** Literal `true`: an unauthorized substitution cannot be expressed. */
|
|
277
|
+
authorizedByPolicy: zod_1.z.literal(true),
|
|
278
|
+
})
|
|
279
|
+
.strict(),
|
|
280
|
+
])
|
|
281
|
+
.superRefine((route, ctx) => {
|
|
282
|
+
// Same rule as `modelDeploymentSchema`: a route serves specific weights. An
|
|
283
|
+
// unpinned entry would leave the data plane choosing a revision, which is
|
|
284
|
+
// the one substitution the customer never authorized by naming a model.
|
|
285
|
+
if (!route.modelReference.includes('@')) {
|
|
286
|
+
ctx.addIssue({
|
|
287
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
288
|
+
path: ['modelReference'],
|
|
289
|
+
message: 'an authorized route must pin an immutable revision (<publisher>/<model>@<revision>)',
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
});
|
|
@@ -90,6 +90,33 @@ exports.inferenceStreamToolCallEventSchema = zod_1.z.object({
|
|
|
90
90
|
* derived from these units and a price version at settlement; a cost quoted by
|
|
91
91
|
* the data plane would be a second, unauthoritative answer to the same
|
|
92
92
|
* question.
|
|
93
|
+
*
|
|
94
|
+
* ## This event is MEASUREMENT EVIDENCE, never a settleable record
|
|
95
|
+
*
|
|
96
|
+
* `normalizedUsageReportSchema` is the only shape a settlement is written from,
|
|
97
|
+
* and this event is deliberately NOT a subset of it that could be widened into
|
|
98
|
+
* one. It carries units and a source; a report additionally carries the
|
|
99
|
+
* attribution block, the outcome, the resolved route, the route-switch count and
|
|
100
|
+
* the two timestamps. Every one of those is knowable only by one END of the
|
|
101
|
+
* request rather than by the frame: the outcome only by the edge, which is the
|
|
102
|
+
* only party that knows whether the CLIENT cancelled, and the route record only
|
|
103
|
+
* by the data plane.
|
|
104
|
+
*
|
|
105
|
+
* So the event is not widened, and that is a decision rather than an omission.
|
|
106
|
+
* Adding the route record and the attribution block here would repeat both on
|
|
107
|
+
* EVERY usage frame of every stream, and each repetition is one more place two
|
|
108
|
+
* frames of one stream could disagree about which route served it — a second
|
|
109
|
+
* source of truth per frame, for fields no consumer of a progress signal reads.
|
|
110
|
+
*
|
|
111
|
+
* **What the edge may do with it, and what it may not.** The units are exact and
|
|
112
|
+
* may be settled; the record around them may not be inferred. When no terminal
|
|
113
|
+
* report arrives — the ordinary case for a client disconnect, since the report
|
|
114
|
+
* frame can no longer be delivered to a connection that is gone — the edge
|
|
115
|
+
* settles the units from the last such event and takes the OUTCOME from itself,
|
|
116
|
+
* never from the event. Its outcome is then `cancelled`, `partial` or `failed`;
|
|
117
|
+
* it is never `completed`, because nothing here can witness that the customer
|
|
118
|
+
* received the whole answer. Zero units marked `estimated` is the arm for no
|
|
119
|
+
* evidence at ALL, not for a disconnect that reported some.
|
|
93
120
|
*/
|
|
94
121
|
exports.inferenceStreamUsageEventSchema = zod_1.z.object({
|
|
95
122
|
/** See `version.ts`: each stream event is a whole message on the wire. */
|
|
@@ -177,12 +204,22 @@ exports.inferenceStreamErrorEventSchema = zod_1.z.object({
|
|
|
177
204
|
/** Carries its own `schemaVersion`: the same body is returned non-streaming. */
|
|
178
205
|
error: errors_1.inferenceErrorSchema,
|
|
179
206
|
});
|
|
180
|
-
/**
|
|
207
|
+
/**
|
|
208
|
+
* Why generation stopped.
|
|
209
|
+
*
|
|
210
|
+
* `refusal` and `content_filter` are separate members because they are separate
|
|
211
|
+
* events: the MODEL declining to answer is a property of the answer, while a
|
|
212
|
+
* filter is an upstream system removing one. The delta channels already carry
|
|
213
|
+
* that distinction (`channel: 'refusal'` beside the filter's own error code),
|
|
214
|
+
* so collapsing it here would have made the terminal event less specific than
|
|
215
|
+
* the stream that produced it.
|
|
216
|
+
*/
|
|
181
217
|
exports.inferenceFinishReasonSchema = zod_1.z.enum([
|
|
182
218
|
'stop',
|
|
183
219
|
'length',
|
|
184
220
|
'tool_calls',
|
|
185
221
|
'content_filter',
|
|
222
|
+
'refusal',
|
|
186
223
|
'cancelled',
|
|
187
224
|
]);
|
|
188
225
|
/**
|
|
@@ -121,7 +121,13 @@ exports.inferenceRequestOutcomeSchema = zod_1.z.enum([
|
|
|
121
121
|
'failed',
|
|
122
122
|
]);
|
|
123
123
|
/**
|
|
124
|
-
* The data plane's technical account of one request
|
|
124
|
+
* The data plane's technical account of one request, and the ONLY shape a
|
|
125
|
+
* settlement is written from.
|
|
126
|
+
*
|
|
127
|
+
* `inferenceStreamUsageEventSchema` is not a narrower version of this one that
|
|
128
|
+
* could be widened into it — see that event's own comment. Its units are exact
|
|
129
|
+
* and settleable; the record around them is not inferable, so an edge settling
|
|
130
|
+
* from a stream event supplies the outcome itself and never promotes the event.
|
|
125
131
|
*
|
|
126
132
|
* No money and no price: the data plane measures units and names the route it
|
|
127
133
|
* used, and the control plane decides what that costs. Keeping the two apart is
|
|
@@ -137,6 +143,24 @@ exports.inferenceRequestOutcomeSchema = zod_1.z.enum([
|
|
|
137
143
|
* nested `prompt_tokens`/`completion_tokens` verbatim charges the cached and
|
|
138
144
|
* reasoning tokens twice, so subtracting the children out is part of what
|
|
139
145
|
* "normalized" means in this shape's name.
|
|
146
|
+
*
|
|
147
|
+
* **A `completed` report carries at least one unit, and the other outcomes need
|
|
148
|
+
* not.** `completed` is the one outcome that asserts the customer received the
|
|
149
|
+
* whole answer, so "delivered in full, consumed nothing measurable" is a
|
|
150
|
+
* contradiction — and it is one that BILLS NOTHING: settlement prices every
|
|
151
|
+
* reported unit and sums, so an empty list is a free request produced by a
|
|
152
|
+
* provider that simply omitted its usage block. The refinement makes that shape
|
|
153
|
+
* unparseable, and the policy behind it is refuse-and-release: the report is
|
|
154
|
+
* rejected and the hold is released, never estimated and charged.
|
|
155
|
+
*
|
|
156
|
+
* The three other outcomes legitimately carry none, which is why the rule is
|
|
157
|
+
* conditional rather than a `.min(1)` on the field. In the reference data plane
|
|
158
|
+
* `failed` is DERIVED from having no units — `outcomeFor` in
|
|
159
|
+
* `internal/relay/executor.go` returns `partial` when units exist and `failed`
|
|
160
|
+
* when they do not — and `cancelled` is reported for a client that stopped
|
|
161
|
+
* before anything was measured. An unconditional minimum would refuse those
|
|
162
|
+
* reports, and a refused report is a request that ran, cost money upstream and
|
|
163
|
+
* can never be settled or refunded.
|
|
140
164
|
*/
|
|
141
165
|
exports.normalizedUsageReportSchema = zod_1.z
|
|
142
166
|
.object({
|
|
@@ -174,6 +198,13 @@ exports.normalizedUsageReportSchema = zod_1.z
|
|
|
174
198
|
message: 'each unit is reported once, as a total',
|
|
175
199
|
});
|
|
176
200
|
}
|
|
201
|
+
if (report.outcome === 'completed' && report.units.length === 0) {
|
|
202
|
+
ctx.addIssue({
|
|
203
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
204
|
+
path: ['units'],
|
|
205
|
+
message: 'a completed request consumed something; report at least one unit',
|
|
206
|
+
});
|
|
207
|
+
}
|
|
177
208
|
});
|
|
178
209
|
/* -------------------------------------------------------------------------- */
|
|
179
210
|
/* 3. Receipt (settlement) */
|
|
@@ -63,7 +63,20 @@
|
|
|
63
63
|
* escapes, and the billing and entitlement records, where one is a second
|
|
64
64
|
* number beside an exact amount.
|
|
65
65
|
*
|
|
66
|
-
*
|
|
66
|
+
* A SIGNED document is strict at its top level for a third reason, and there it
|
|
67
|
+
* is forced rather than chosen. `aliaModelReleaseManifestSchema` carries
|
|
68
|
+
* signatures over its own canonical bytes, so a field stripped at this parse is a
|
|
69
|
+
* field missing from the bytes a verifier re-canonicalizes: a tolerant parse
|
|
70
|
+
* would report an invalid SIGNATURE where the truth is that this build does not
|
|
71
|
+
* understand the DOCUMENT. Its producer can run ahead of this package — Alia's
|
|
72
|
+
* release tooling is deployed independently — so the usual argument applies here
|
|
73
|
+
* and is outweighed, because the refusal costs an operator one retry after Oxy
|
|
74
|
+
* takes the newer contract, while the tolerant parse costs a misdiagnosis of a
|
|
75
|
+
* cryptographic failure.
|
|
76
|
+
*
|
|
77
|
+
* Decided in: docs/adr/0006-oxy-relay-boundary.md,
|
|
78
|
+
* docs/adr/0010-public-api-compatibility.md,
|
|
79
|
+
* docs/adr/0017-authorized-routes-in-the-envelope.md.
|
|
67
80
|
*/
|
|
68
81
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
69
82
|
exports.INFERENCE_CONTRACT_VERSION = void 0;
|
|
@@ -74,12 +87,19 @@ exports.INFERENCE_CONTRACT_VERSION = void 0;
|
|
|
74
87
|
*
|
|
75
88
|
* MAJOR is bumped when any individual shape's `schemaVersion` increments (at
|
|
76
89
|
* least one message is now read differently by the two sides); MINOR when a
|
|
77
|
-
* shape or an optional field is added
|
|
78
|
-
*
|
|
90
|
+
* shape or an optional field is added, when a CLOSED ENUM gains a member, or
|
|
91
|
+
* when a refinement changes which bytes parse; PATCH for documentation-only
|
|
92
|
+
* changes that leave every parsed byte identical.
|
|
93
|
+
*
|
|
94
|
+
* The last two are MINOR rather than PATCH because both produce the same
|
|
95
|
+
* failure: a producer on the newer set emits something the older set refuses,
|
|
96
|
+
* with no `schemaVersion` difference to explain it. A new enum member and a
|
|
97
|
+
* loosened refinement are exactly what the handshake exists to surface — a
|
|
98
|
+
* skew the per-message version cannot express.
|
|
79
99
|
*
|
|
80
100
|
* This constant is deliberately NOT embedded in the request envelope. Pinning a
|
|
81
101
|
* request to the version of the whole set would make an unrelated additive
|
|
82
102
|
* change to, say, the catalogue reject every in-flight inference request; the
|
|
83
103
|
* per-shape `schemaVersion` is what a message is validated against.
|
|
84
104
|
*/
|
|
85
|
-
exports.INFERENCE_CONTRACT_VERSION = '1.
|
|
105
|
+
exports.INFERENCE_CONTRACT_VERSION = '1.2.0';
|