@oxyhq/contracts 0.29.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.
Files changed (40) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/identity.js +33 -1
  3. package/dist/cjs/index.js +20 -7
  4. package/dist/cjs/inference/aliaModelRelease.js +250 -0
  5. package/dist/cjs/inference/catalogue.js +1 -4
  6. package/dist/cjs/inference/identifiers.js +23 -3
  7. package/dist/cjs/inference/request.js +188 -5
  8. package/dist/cjs/inference/routingPolicy.js +81 -2
  9. package/dist/cjs/inference/streamEvents.js +27 -0
  10. package/dist/cjs/inference/usage.js +32 -1
  11. package/dist/cjs/inference/version.js +15 -2
  12. package/dist/esm/.tsbuildinfo +1 -1
  13. package/dist/esm/identity.js +32 -0
  14. package/dist/esm/index.js +8 -3
  15. package/dist/esm/inference/aliaModelRelease.js +247 -0
  16. package/dist/esm/inference/catalogue.js +2 -5
  17. package/dist/esm/inference/identifiers.js +22 -2
  18. package/dist/esm/inference/request.js +189 -6
  19. package/dist/esm/inference/routingPolicy.js +81 -2
  20. package/dist/esm/inference/streamEvents.js +27 -0
  21. package/dist/esm/inference/usage.js +32 -1
  22. package/dist/esm/inference/version.js +15 -2
  23. package/dist/types/.tsbuildinfo +1 -1
  24. package/dist/types/identity.d.ts +59 -1
  25. package/dist/types/index.d.ts +7 -5
  26. package/dist/types/inference/accountBilling.d.ts +26 -26
  27. package/dist/types/inference/aliaModelRelease.d.ts +597 -0
  28. package/dist/types/inference/catalogue.d.ts +6 -6
  29. package/dist/types/inference/entitlement.d.ts +4 -4
  30. package/dist/types/inference/identifiers.d.ts +20 -2
  31. package/dist/types/inference/money.d.ts +4 -4
  32. package/dist/types/inference/priceVersion.d.ts +14 -14
  33. package/dist/types/inference/providerConnection.d.ts +4 -4
  34. package/dist/types/inference/request.d.ts +291 -5
  35. package/dist/types/inference/routingPolicy.d.ts +109 -13
  36. package/dist/types/inference/streamEvents.d.ts +53 -26
  37. package/dist/types/inference/usage.d.ts +103 -79
  38. package/dist/types/inference/version.d.ts +15 -2
  39. package/dist/types/keyRecovery.d.ts +4 -4
  40. package/package.json +1 -1
@@ -22,10 +22,17 @@
22
22
  * REJECTED, rather than being quietly resolved by whichever field the executor
23
23
  * happens to read first. That rejection is `routingPolicySchema`'s refinement.
24
24
  *
25
- * Decided in: docs/adr/0008-catalogue-concept-separation.md, issue #972 workstream 6.
25
+ * **The policy itself never crosses to the data plane.** What crosses is
26
+ * {@link authorizedRouteSchema} — the candidate routes that SURVIVED these
27
+ * controls, in preference order — plus {@link routingPolicyReferenceSchema} as
28
+ * provenance for the receipt. The data plane holds no control value and needs
29
+ * none: it fails over by taking the next entry. See ADR 0017.
30
+ *
31
+ * Decided in: docs/adr/0008-catalogue-concept-separation.md,
32
+ * docs/adr/0017-authorized-routes-in-the-envelope.md, issue #972 workstream 6.
26
33
  */
27
34
  import { z } from 'zod';
28
- import { inferenceProviderSlugSchema, inferenceRegionSchema, inferenceTimestampSchema, modelReferenceSchema, oxyAccountIdSchema, oxyApplicationIdSchema, routingProfileSlugSchema, } from './identifiers.js';
35
+ import { deploymentIdSchema, inferenceProviderSlugSchema, inferenceRegionSchema, inferenceTimestampSchema, modelReferenceSchema, oxyAccountIdSchema, oxyApplicationIdSchema, routingProfileSlugSchema, } from './identifiers.js';
29
36
  import { exactDecimalSchema, currencyCodeSchema, unitPriceSchema } from './money.js';
30
37
  /**
31
38
  * What a policy resolves to when a caller names no model.
@@ -208,3 +215,75 @@ export const routingPolicyReferenceSchema = z
208
215
  policyVersion: z.number().int().positive().safe(),
209
216
  })
210
217
  .strict();
218
+ /* -------------------------------------------------------------------------- */
219
+ /* Pre-authorized routes */
220
+ /* -------------------------------------------------------------------------- */
221
+ /**
222
+ * What every authorized route carries, whichever kind it is.
223
+ *
224
+ * Exactly what EXECUTING a route needs, and nothing a policy could be
225
+ * re-derived from. There is no price, no data-retention flag, no licence id and
226
+ * no availability scope here: those are the values the control plane already
227
+ * evaluated to put this entry in the list, and repeating them would invite the
228
+ * data plane to evaluate them a second time — differently, in another language.
229
+ *
230
+ * `regions` is plural and matches `modelDeploymentSchema.regions`, because a
231
+ * deployment declares every region it MAY serve from and choosing among them is
232
+ * routing execution (ADR 0006). Oxy checked the whole set against the customer's
233
+ * residency controls as a SUBSET, so any region in this list is one the policy
234
+ * permits and the data plane's choice among them cannot escape it. Collapsing it
235
+ * to one region would make Oxy take a decision the boundary assigns elsewhere.
236
+ */
237
+ const authorizedRouteFields = {
238
+ /** Which concrete endpoint. Opaque to customers; the data plane's own key. */
239
+ deploymentId: deploymentIdSchema,
240
+ /** Always revision-pinned: the entry names the exact weights to serve. */
241
+ modelReference: modelReferenceSchema,
242
+ provider: inferenceProviderSlugSchema,
243
+ regions: z.array(inferenceRegionSchema).min(1),
244
+ };
245
+ /**
246
+ * One route the control plane has already authorized for one request.
247
+ *
248
+ * **Authorization is an ENTRY, never a boolean.** The same stance
249
+ * `inference_routing_policy_fallbacks` takes in storage: being allowed to serve
250
+ * a route IS appearing here, and every entry names its destination. A flag
251
+ * saying "substitution allowed" without naming the destination is exactly the
252
+ * silent substitution the platform forbids, and it invents a "flag set, list
253
+ * empty" state somebody then has to decide what to do with.
254
+ *
255
+ * Discriminated on `substitution`, relative to the FIRST entry — the primary
256
+ * route Oxy resolved:
257
+ *
258
+ * - `same_model` serves the same model line as the primary. This is
259
+ * availability failover between deployments of one model.
260
+ * - `cross_model` serves a DIFFERENT model line, and is expressible only with
261
+ * `authorizedByPolicy: true` as a literal. So "a model was substituted
262
+ * without the customer authorizing it" is not a sentence this contract can
263
+ * say — the same construction `inferenceRouteSwitchDetailSchema` uses to make
264
+ * the resulting route-switch event unreportable.
265
+ */
266
+ export const authorizedRouteSchema = z
267
+ .discriminatedUnion('substitution', [
268
+ z.object({ substitution: z.literal('same_model'), ...authorizedRouteFields }).strict(),
269
+ z
270
+ .object({
271
+ substitution: z.literal('cross_model'),
272
+ ...authorizedRouteFields,
273
+ /** Literal `true`: an unauthorized substitution cannot be expressed. */
274
+ authorizedByPolicy: z.literal(true),
275
+ })
276
+ .strict(),
277
+ ])
278
+ .superRefine((route, ctx) => {
279
+ // Same rule as `modelDeploymentSchema`: a route serves specific weights. An
280
+ // unpinned entry would leave the data plane choosing a revision, which is
281
+ // the one substitution the customer never authorized by naming a model.
282
+ if (!route.modelReference.includes('@')) {
283
+ ctx.addIssue({
284
+ code: z.ZodIssueCode.custom,
285
+ path: ['modelReference'],
286
+ message: 'an authorized route must pin an immutable revision (<publisher>/<model>@<revision>)',
287
+ });
288
+ }
289
+ });
@@ -87,6 +87,33 @@ export const inferenceStreamToolCallEventSchema = z.object({
87
87
  * derived from these units and a price version at settlement; a cost quoted by
88
88
  * the data plane would be a second, unauthoritative answer to the same
89
89
  * question.
90
+ *
91
+ * ## This event is MEASUREMENT EVIDENCE, never a settleable record
92
+ *
93
+ * `normalizedUsageReportSchema` is the only shape a settlement is written from,
94
+ * and this event is deliberately NOT a subset of it that could be widened into
95
+ * one. It carries units and a source; a report additionally carries the
96
+ * attribution block, the outcome, the resolved route, the route-switch count and
97
+ * the two timestamps. Every one of those is knowable only by one END of the
98
+ * request rather than by the frame: the outcome only by the edge, which is the
99
+ * only party that knows whether the CLIENT cancelled, and the route record only
100
+ * by the data plane.
101
+ *
102
+ * So the event is not widened, and that is a decision rather than an omission.
103
+ * Adding the route record and the attribution block here would repeat both on
104
+ * EVERY usage frame of every stream, and each repetition is one more place two
105
+ * frames of one stream could disagree about which route served it — a second
106
+ * source of truth per frame, for fields no consumer of a progress signal reads.
107
+ *
108
+ * **What the edge may do with it, and what it may not.** The units are exact and
109
+ * may be settled; the record around them may not be inferred. When no terminal
110
+ * report arrives — the ordinary case for a client disconnect, since the report
111
+ * frame can no longer be delivered to a connection that is gone — the edge
112
+ * settles the units from the last such event and takes the OUTCOME from itself,
113
+ * never from the event. Its outcome is then `cancelled`, `partial` or `failed`;
114
+ * it is never `completed`, because nothing here can witness that the customer
115
+ * received the whole answer. Zero units marked `estimated` is the arm for no
116
+ * evidence at ALL, not for a disconnect that reported some.
90
117
  */
91
118
  export const inferenceStreamUsageEventSchema = z.object({
92
119
  /** See `version.ts`: each stream event is a whole message on the wire. */
@@ -118,7 +118,13 @@ export const inferenceRequestOutcomeSchema = z.enum([
118
118
  'failed',
119
119
  ]);
120
120
  /**
121
- * The data plane's technical account of one request.
121
+ * The data plane's technical account of one request, and the ONLY shape a
122
+ * settlement is written from.
123
+ *
124
+ * `inferenceStreamUsageEventSchema` is not a narrower version of this one that
125
+ * could be widened into it — see that event's own comment. Its units are exact
126
+ * and settleable; the record around them is not inferable, so an edge settling
127
+ * from a stream event supplies the outcome itself and never promotes the event.
122
128
  *
123
129
  * No money and no price: the data plane measures units and names the route it
124
130
  * used, and the control plane decides what that costs. Keeping the two apart is
@@ -134,6 +140,24 @@ export const inferenceRequestOutcomeSchema = z.enum([
134
140
  * nested `prompt_tokens`/`completion_tokens` verbatim charges the cached and
135
141
  * reasoning tokens twice, so subtracting the children out is part of what
136
142
  * "normalized" means in this shape's name.
143
+ *
144
+ * **A `completed` report carries at least one unit, and the other outcomes need
145
+ * not.** `completed` is the one outcome that asserts the customer received the
146
+ * whole answer, so "delivered in full, consumed nothing measurable" is a
147
+ * contradiction — and it is one that BILLS NOTHING: settlement prices every
148
+ * reported unit and sums, so an empty list is a free request produced by a
149
+ * provider that simply omitted its usage block. The refinement makes that shape
150
+ * unparseable, and the policy behind it is refuse-and-release: the report is
151
+ * rejected and the hold is released, never estimated and charged.
152
+ *
153
+ * The three other outcomes legitimately carry none, which is why the rule is
154
+ * conditional rather than a `.min(1)` on the field. In the reference data plane
155
+ * `failed` is DERIVED from having no units — `outcomeFor` in
156
+ * `internal/relay/executor.go` returns `partial` when units exist and `failed`
157
+ * when they do not — and `cancelled` is reported for a client that stopped
158
+ * before anything was measured. An unconditional minimum would refuse those
159
+ * reports, and a refused report is a request that ran, cost money upstream and
160
+ * can never be settled or refunded.
137
161
  */
138
162
  export const normalizedUsageReportSchema = z
139
163
  .object({
@@ -171,6 +195,13 @@ export const normalizedUsageReportSchema = z
171
195
  message: 'each unit is reported once, as a total',
172
196
  });
173
197
  }
198
+ if (report.outcome === 'completed' && report.units.length === 0) {
199
+ ctx.addIssue({
200
+ code: z.ZodIssueCode.custom,
201
+ path: ['units'],
202
+ message: 'a completed request consumed something; report at least one unit',
203
+ });
204
+ }
174
205
  });
175
206
  /* -------------------------------------------------------------------------- */
176
207
  /* 3. Receipt (settlement) */
@@ -62,7 +62,20 @@
62
62
  * escapes, and the billing and entitlement records, where one is a second
63
63
  * number beside an exact amount.
64
64
  *
65
- * Decided in: docs/adr/0006-oxy-relay-boundary.md, docs/adr/0010-public-api-compatibility.md.
65
+ * A SIGNED document is strict at its top level for a third reason, and there it
66
+ * is forced rather than chosen. `aliaModelReleaseManifestSchema` carries
67
+ * signatures over its own canonical bytes, so a field stripped at this parse is a
68
+ * field missing from the bytes a verifier re-canonicalizes: a tolerant parse
69
+ * would report an invalid SIGNATURE where the truth is that this build does not
70
+ * understand the DOCUMENT. Its producer can run ahead of this package — Alia's
71
+ * release tooling is deployed independently — so the usual argument applies here
72
+ * and is outweighed, because the refusal costs an operator one retry after Oxy
73
+ * takes the newer contract, while the tolerant parse costs a misdiagnosis of a
74
+ * cryptographic failure.
75
+ *
76
+ * Decided in: docs/adr/0006-oxy-relay-boundary.md,
77
+ * docs/adr/0010-public-api-compatibility.md,
78
+ * docs/adr/0017-authorized-routes-in-the-envelope.md.
66
79
  */
67
80
  /**
68
81
  * Version of the contract SET as a whole — the value the control plane and the
@@ -86,4 +99,4 @@
86
99
  * change to, say, the catalogue reject every in-flight inference request; the
87
100
  * per-shape `schemaVersion` is what a message is validated against.
88
101
  */
89
- export const INFERENCE_CONTRACT_VERSION = '1.1.0';
102
+ export const INFERENCE_CONTRACT_VERSION = '1.2.0';