@oxy.so/contracts 1.0.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 (147) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +16 -0
  3. package/dist/cjs/.tsbuildinfo +1 -0
  4. package/dist/cjs/accountGraph.js +489 -0
  5. package/dist/cjs/agency.js +439 -0
  6. package/dist/cjs/browserHub.js +215 -0
  7. package/dist/cjs/civic.js +163 -0
  8. package/dist/cjs/commonsSignIn.js +59 -0
  9. package/dist/cjs/deviceBoot.js +50 -0
  10. package/dist/cjs/deviceDirectory.js +189 -0
  11. package/dist/cjs/devicePairing.js +138 -0
  12. package/dist/cjs/deviceSession.js +164 -0
  13. package/dist/cjs/emailAgentContext.js +32 -0
  14. package/dist/cjs/followGraph.js +28 -0
  15. package/dist/cjs/identity.js +258 -0
  16. package/dist/cjs/inboxPush.js +24 -0
  17. package/dist/cjs/index.js +618 -0
  18. package/dist/cjs/inference/accountBilling.js +334 -0
  19. package/dist/cjs/inference/aliaModelRelease.js +262 -0
  20. package/dist/cjs/inference/attribution.js +106 -0
  21. package/dist/cjs/inference/catalogue.js +487 -0
  22. package/dist/cjs/inference/entitlement.js +217 -0
  23. package/dist/cjs/inference/errors.js +309 -0
  24. package/dist/cjs/inference/identifiers.js +224 -0
  25. package/dist/cjs/inference/inbox.js +105 -0
  26. package/dist/cjs/inference/modelDocumentation.js +433 -0
  27. package/dist/cjs/inference/money.js +188 -0
  28. package/dist/cjs/inference/priceVersion.js +110 -0
  29. package/dist/cjs/inference/providerConnection.js +455 -0
  30. package/dist/cjs/inference/request.js +477 -0
  31. package/dist/cjs/inference/routingPolicy.js +318 -0
  32. package/dist/cjs/inference/streamEvents.js +258 -0
  33. package/dist/cjs/inference/usage.js +329 -0
  34. package/dist/cjs/inference/version.js +105 -0
  35. package/dist/cjs/keyRecovery.js +91 -0
  36. package/dist/cjs/keyRotation.js +75 -0
  37. package/dist/cjs/links.js +68 -0
  38. package/dist/cjs/moderationReputation.js +298 -0
  39. package/dist/cjs/oauth.js +66 -0
  40. package/dist/cjs/oxyRecordTypes.js +71 -0
  41. package/dist/cjs/protocol.js +53 -0
  42. package/dist/cjs/recommendations.js +168 -0
  43. package/dist/cjs/reputation.js +297 -0
  44. package/dist/cjs/sessionStatus.js +121 -0
  45. package/dist/cjs/transparency.js +89 -0
  46. package/dist/cjs/updates.js +252 -0
  47. package/dist/cjs/userInvalidation.js +89 -0
  48. package/dist/cjs/userResponse.js +245 -0
  49. package/dist/cjs/username.js +290 -0
  50. package/dist/cjs/webauthn.js +71 -0
  51. package/dist/esm/.tsbuildinfo +1 -0
  52. package/dist/esm/accountGraph.js +480 -0
  53. package/dist/esm/agency.js +436 -0
  54. package/dist/esm/browserHub.js +212 -0
  55. package/dist/esm/civic.js +160 -0
  56. package/dist/esm/commonsSignIn.js +56 -0
  57. package/dist/esm/deviceBoot.js +47 -0
  58. package/dist/esm/deviceDirectory.js +186 -0
  59. package/dist/esm/devicePairing.js +135 -0
  60. package/dist/esm/deviceSession.js +161 -0
  61. package/dist/esm/emailAgentContext.js +29 -0
  62. package/dist/esm/followGraph.js +27 -0
  63. package/dist/esm/identity.js +255 -0
  64. package/dist/esm/inboxPush.js +21 -0
  65. package/dist/esm/index.js +172 -0
  66. package/dist/esm/inference/accountBilling.js +331 -0
  67. package/dist/esm/inference/aliaModelRelease.js +259 -0
  68. package/dist/esm/inference/attribution.js +103 -0
  69. package/dist/esm/inference/catalogue.js +484 -0
  70. package/dist/esm/inference/entitlement.js +214 -0
  71. package/dist/esm/inference/errors.js +306 -0
  72. package/dist/esm/inference/identifiers.js +221 -0
  73. package/dist/esm/inference/inbox.js +102 -0
  74. package/dist/esm/inference/modelDocumentation.js +430 -0
  75. package/dist/esm/inference/money.js +185 -0
  76. package/dist/esm/inference/priceVersion.js +107 -0
  77. package/dist/esm/inference/providerConnection.js +452 -0
  78. package/dist/esm/inference/request.js +474 -0
  79. package/dist/esm/inference/routingPolicy.js +315 -0
  80. package/dist/esm/inference/streamEvents.js +255 -0
  81. package/dist/esm/inference/usage.js +326 -0
  82. package/dist/esm/inference/version.js +102 -0
  83. package/dist/esm/keyRecovery.js +88 -0
  84. package/dist/esm/keyRotation.js +72 -0
  85. package/dist/esm/links.js +65 -0
  86. package/dist/esm/moderationReputation.js +295 -0
  87. package/dist/esm/oauth.js +63 -0
  88. package/dist/esm/oxyRecordTypes.js +68 -0
  89. package/dist/esm/protocol.js +50 -0
  90. package/dist/esm/recommendations.js +165 -0
  91. package/dist/esm/reputation.js +293 -0
  92. package/dist/esm/sessionStatus.js +118 -0
  93. package/dist/esm/transparency.js +86 -0
  94. package/dist/esm/updates.js +249 -0
  95. package/dist/esm/userInvalidation.js +85 -0
  96. package/dist/esm/userResponse.js +240 -0
  97. package/dist/esm/username.js +283 -0
  98. package/dist/esm/webauthn.js +68 -0
  99. package/dist/types/.tsbuildinfo +1 -0
  100. package/dist/types/accountGraph.d.ts +378 -0
  101. package/dist/types/agency.d.ts +2162 -0
  102. package/dist/types/browserHub.d.ts +856 -0
  103. package/dist/types/civic.d.ts +338 -0
  104. package/dist/types/commonsSignIn.d.ts +58 -0
  105. package/dist/types/deviceBoot.d.ts +74 -0
  106. package/dist/types/deviceDirectory.d.ts +1317 -0
  107. package/dist/types/devicePairing.d.ts +130 -0
  108. package/dist/types/deviceSession.d.ts +411 -0
  109. package/dist/types/emailAgentContext.d.ts +248 -0
  110. package/dist/types/followGraph.d.ts +150 -0
  111. package/dist/types/identity.d.ts +402 -0
  112. package/dist/types/inboxPush.d.ts +30 -0
  113. package/dist/types/index.d.ts +100 -0
  114. package/dist/types/inference/accountBilling.d.ts +738 -0
  115. package/dist/types/inference/aliaModelRelease.d.ts +609 -0
  116. package/dist/types/inference/attribution.d.ts +176 -0
  117. package/dist/types/inference/catalogue.d.ts +1618 -0
  118. package/dist/types/inference/entitlement.d.ts +519 -0
  119. package/dist/types/inference/errors.d.ts +242 -0
  120. package/dist/types/inference/identifiers.d.ts +182 -0
  121. package/dist/types/inference/inbox.d.ts +374 -0
  122. package/dist/types/inference/modelDocumentation.d.ts +1603 -0
  123. package/dist/types/inference/money.d.ts +185 -0
  124. package/dist/types/inference/priceVersion.d.ts +182 -0
  125. package/dist/types/inference/providerConnection.d.ts +968 -0
  126. package/dist/types/inference/request.d.ts +2800 -0
  127. package/dist/types/inference/routingPolicy.d.ts +616 -0
  128. package/dist/types/inference/streamEvents.d.ts +950 -0
  129. package/dist/types/inference/usage.d.ts +1164 -0
  130. package/dist/types/inference/version.d.ts +102 -0
  131. package/dist/types/keyRecovery.d.ts +138 -0
  132. package/dist/types/keyRotation.d.ts +103 -0
  133. package/dist/types/links.d.ts +96 -0
  134. package/dist/types/moderationReputation.d.ts +487 -0
  135. package/dist/types/oauth.d.ts +86 -0
  136. package/dist/types/oxyRecordTypes.d.ts +62 -0
  137. package/dist/types/protocol.d.ts +86 -0
  138. package/dist/types/recommendations.d.ts +542 -0
  139. package/dist/types/reputation.d.ts +457 -0
  140. package/dist/types/sessionStatus.d.ts +231 -0
  141. package/dist/types/transparency.d.ts +392 -0
  142. package/dist/types/updates.d.ts +545 -0
  143. package/dist/types/userInvalidation.d.ts +94 -0
  144. package/dist/types/userResponse.d.ts +1706 -0
  145. package/dist/types/username.d.ts +265 -0
  146. package/dist/types/webauthn.d.ts +77 -0
  147. package/package.json +87 -0
@@ -0,0 +1,315 @@
1
+ /**
2
+ * Routing policy — the customer-facing routing configuration.
3
+ *
4
+ * Stored under an Oxy account or application (the control plane owns it),
5
+ * executed by the data plane, which owns execution. Every request records the
6
+ * exact `{routingPolicyId, policyVersion}` it was served under, so a route
7
+ * decision months old can be explained against the policy that was in force,
8
+ * not against the policy that exists now.
9
+ *
10
+ * Two rules shape the fallback controls:
11
+ *
12
+ * - **Same-model deployment failover is not cross-model fallback.** Moving
13
+ * between two deployments of the SAME revision is an availability decision
14
+ * and is on by default; serving a DIFFERENT model is a substitution the
15
+ * customer must have authorized by name.
16
+ * - **A request for a concrete model is never silently replaced.** Cross-model
17
+ * fallback is an explicit list of references, and a switch that uses it emits
18
+ * a customer-visible route-switch event.
19
+ *
20
+ * The controls are flat and independent, matching what Console renders — which
21
+ * means contradictory combinations are EXPRESSIBLE and must therefore be
22
+ * REJECTED, rather than being quietly resolved by whichever field the executor
23
+ * happens to read first. That rejection is `routingPolicySchema`'s refinement.
24
+ *
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.
33
+ */
34
+ import { z } from "zod";
35
+ import { deploymentIdSchema, inferenceProviderSlugSchema, inferenceRegionSchema, inferenceTimestampSchema, modelReferenceSchema, oxyAccountIdSchema, oxyApplicationIdSchema, routingProfileIdSchema, } from "./identifiers.js";
36
+ import { kaanaCredentialHandleSchema } from "./providerConnection.js";
37
+ import { exactDecimalSchema, currencyCodeSchema, unitPriceSchema, } from "./money.js";
38
+ /**
39
+ * What a policy resolves to when a caller names no model.
40
+ *
41
+ * A discriminated union rather than two optional fields, so "which one did the
42
+ * customer configure" is never a question about which field is non-null.
43
+ */
44
+ export const routingTargetSchema = z.discriminatedUnion("kind", [
45
+ z
46
+ .object({
47
+ kind: z.literal("model"),
48
+ modelReference: modelReferenceSchema,
49
+ })
50
+ .strict(),
51
+ z
52
+ .object({
53
+ kind: z.literal("routing_profile_id"),
54
+ routingProfileId: routingProfileIdSchema,
55
+ })
56
+ .strict(),
57
+ ]);
58
+ /**
59
+ * Which account or application a policy governs.
60
+ *
61
+ * Application-scoped policies are the common case; an account-scoped policy is
62
+ * the floor its applications inherit, and inheritance is resolved by the control
63
+ * plane before a policy reaches the data plane.
64
+ */
65
+ export const routingPolicyScopeSchema = z.discriminatedUnion("kind", [
66
+ z
67
+ .object({ kind: z.literal("account"), accountId: oxyAccountIdSchema })
68
+ .strict(),
69
+ z
70
+ .object({
71
+ kind: z.literal("application"),
72
+ accountId: oxyAccountIdSchema,
73
+ applicationId: oxyApplicationIdSchema,
74
+ })
75
+ .strict(),
76
+ ]);
77
+ /**
78
+ * The fallback controls, kept together so a reviewer sees all three at once.
79
+ *
80
+ * `authorizedCrossModel` is a list of model references the customer has
81
+ * explicitly permitted as substitutes — never a boolean, because "allow
82
+ * fallback" without naming the destination is exactly the silent substitution
83
+ * the invariant forbids.
84
+ */
85
+ export const routingFallbackPolicySchema = z
86
+ .object({
87
+ disabled: z.boolean(),
88
+ sameModelDeployment: z.boolean(),
89
+ authorizedCrossModel: z.array(modelReferenceSchema).default([]),
90
+ })
91
+ .strict();
92
+ /**
93
+ * A versioned routing policy.
94
+ *
95
+ * `policyVersion` is the CUSTOMER's revision of their own configuration and is
96
+ * unrelated to `schemaVersion`, which is the version of this wire shape. They
97
+ * are two different clocks: a customer edits their policy without any contract
98
+ * change, and a contract change does not renumber anybody's policy.
99
+ */
100
+ export const routingPolicySchema = z
101
+ .object({
102
+ /** See `version.ts`: exchanged with the data plane on its own. */
103
+ schemaVersion: z.literal(2),
104
+ routingPolicyId: z.string().min(1).max(128),
105
+ policyVersion: z.number().int().positive().safe(),
106
+ scope: routingPolicyScopeSchema,
107
+ /** Absent when every request must name its own model. */
108
+ defaultTarget: routingTargetSchema.optional(),
109
+ /** Empty means "no allowlist" — every provider qualifies unless denied. */
110
+ providerAllowlist: z.array(inferenceProviderSlugSchema).default([]),
111
+ providerDenylist: z.array(inferenceProviderSlugSchema).default([]),
112
+ /** Empty means "no residency constraint". */
113
+ allowedRegions: z.array(inferenceRegionSchema).default([]),
114
+ deniedRegions: z.array(inferenceRegionSchema).default([]),
115
+ requireZeroDataRetention: z.boolean(),
116
+ prohibitTrainingOnCustomerData: z.boolean(),
117
+ /** Ceilings on what a route may cost the customer, quoted like catalogue prices. */
118
+ maxPricePerUnit: z.array(unitPriceSchema).default([]),
119
+ maxPricePerRequest: z
120
+ .object({ amount: exactDecimalSchema, currency: currencyCodeSchema })
121
+ .strict()
122
+ .optional(),
123
+ /** What to optimise for among the routes that qualify. */
124
+ optimiseFor: z.enum(["price", "latency", "throughput", "balanced"]),
125
+ /** Serve only from Oxy's own hosting of open-weight models. */
126
+ oxyHostedOnly: z.boolean(),
127
+ /** License / usage-right constraints. Empty license list means unconstrained. */
128
+ allowedLicenseIds: z.array(z.string().min(1).max(128)).default([]),
129
+ requireCommercialUseRights: z.boolean(),
130
+ fallback: routingFallbackPolicySchema,
131
+ /** Whether the customer's own provider credentials may or must be used. */
132
+ byokPreference: z.enum(["disabled", "prefer", "require"]),
133
+ /** Enterprise reserved capacity rather than shared endpoints. */
134
+ dedicatedCapacity: z.enum(["disabled", "prefer", "require"]),
135
+ updatedAt: inferenceTimestampSchema,
136
+ })
137
+ .superRefine((policy, ctx) => {
138
+ // "Requires a denied provider": the allowlist is the requirement, so a
139
+ // provider named in both lists is a policy that can never resolve. This is
140
+ // also the shape an Oxy-hosted-only policy takes when it pins a provider it
141
+ // has itself denied — whether a provider is Oxy-hosted is a property of the
142
+ // catalogue entry, not of its slug, so the data plane resolves it, not this
143
+ // schema.
144
+ const denied = new Set(policy.providerDenylist);
145
+ for (const [index, provider] of policy.providerAllowlist.entries()) {
146
+ if (denied.has(provider)) {
147
+ ctx.addIssue({
148
+ code: z.ZodIssueCode.custom,
149
+ path: ["providerAllowlist", index],
150
+ message: `provider ${provider} is both required by the allowlist and denied`,
151
+ });
152
+ }
153
+ }
154
+ const deniedRegions = new Set(policy.deniedRegions);
155
+ for (const [index, region] of policy.allowedRegions.entries()) {
156
+ if (deniedRegions.has(region)) {
157
+ ctx.addIssue({
158
+ code: z.ZodIssueCode.custom,
159
+ path: ["allowedRegions", index],
160
+ message: `region ${region} is both allowed and denied`,
161
+ });
162
+ }
163
+ }
164
+ // Fallback disabled is an instruction to fail the request rather than serve
165
+ // it elsewhere. Combined with a fallback route it is not a strict policy but
166
+ // an ambiguous one, and the ambiguity resolves differently in each executor.
167
+ if (policy.fallback.disabled && policy.fallback.sameModelDeployment) {
168
+ ctx.addIssue({
169
+ code: z.ZodIssueCode.custom,
170
+ path: ["fallback", "sameModelDeployment"],
171
+ message: "fallback is disabled, so same-model deployment failover cannot be enabled",
172
+ });
173
+ }
174
+ if (policy.fallback.disabled &&
175
+ policy.fallback.authorizedCrossModel.length > 0) {
176
+ ctx.addIssue({
177
+ code: z.ZodIssueCode.custom,
178
+ path: ["fallback", "authorizedCrossModel"],
179
+ message: "fallback is disabled, so no cross-model fallback may be authorized",
180
+ });
181
+ }
182
+ // BYOK routes run on the customer's own upstream provider account, which is
183
+ // by definition not Oxy's hosting.
184
+ if (policy.oxyHostedOnly && policy.byokPreference === "require") {
185
+ ctx.addIssue({
186
+ code: z.ZodIssueCode.custom,
187
+ path: ["byokPreference"],
188
+ message: "an Oxy-hosted-only policy cannot also require a customer provider credential",
189
+ });
190
+ }
191
+ const ceilingUnits = policy.maxPricePerUnit.map((ceiling) => ceiling.unit);
192
+ if (new Set(ceilingUnits).size !== ceilingUnits.length) {
193
+ ctx.addIssue({
194
+ code: z.ZodIssueCode.custom,
195
+ path: ["maxPricePerUnit"],
196
+ message: "a unit may carry only one price ceiling",
197
+ });
198
+ }
199
+ if (policy.maxPricePerRequest !== undefined) {
200
+ for (const [index, ceiling] of policy.maxPricePerUnit.entries()) {
201
+ if (ceiling.currency !== policy.maxPricePerRequest.currency) {
202
+ ctx.addIssue({
203
+ code: z.ZodIssueCode.custom,
204
+ path: ["maxPricePerUnit", index, "currency"],
205
+ message: "every price ceiling in one policy must use the same currency",
206
+ });
207
+ }
208
+ }
209
+ }
210
+ });
211
+ /**
212
+ * The reference a request records: which policy, at which of the customer's own
213
+ * revisions. Embedded in the request envelope and in the settled receipt, so a
214
+ * charge can be explained against the exact configuration that produced it.
215
+ */
216
+ export const routingPolicyReferenceSchema = z
217
+ .object({
218
+ routingPolicyId: z.string().min(1).max(128),
219
+ policyVersion: z.number().int().positive().safe(),
220
+ })
221
+ .strict();
222
+ /* -------------------------------------------------------------------------- */
223
+ /* Pre-authorized routes */
224
+ /* -------------------------------------------------------------------------- */
225
+ /**
226
+ * What every authorized route carries, whichever kind it is.
227
+ *
228
+ * Exactly what EXECUTING a route needs, and nothing a policy could be
229
+ * re-derived from. There is no price, no data-retention flag, no licence id and
230
+ * no availability scope here: those are the values the control plane already
231
+ * evaluated to put this entry in the list, and repeating them would invite the
232
+ * data plane to evaluate them a second time — differently, in another language.
233
+ *
234
+ * `regions` is plural and matches `modelDeploymentSchema.regions`, because a
235
+ * deployment declares every ATTESTED region it MAY serve from and choosing among
236
+ * them is routing execution (ADR 0006). Oxy checked the whole set against the
237
+ * customer's residency controls as a SUBSET, so any non-empty region in this
238
+ * list is one the policy permits and the data plane's choice among them cannot
239
+ * escape it. An empty list means no location was attested and can only survive a
240
+ * request with no explicit regional control; it never means global. Collapsing
241
+ * the set to one invented region would make Oxy take a decision the boundary
242
+ * assigns elsewhere.
243
+ */
244
+ const authorizedRouteFields = {
245
+ /** Which concrete endpoint. Opaque to customers; the data plane's own key. */
246
+ deploymentId: deploymentIdSchema,
247
+ /** Always revision-pinned: the entry names the exact weights to serve. */
248
+ modelReference: modelReferenceSchema,
249
+ provider: inferenceProviderSlugSchema,
250
+ regions: z.array(inferenceRegionSchema),
251
+ /**
252
+ * Exact customer credential binding. Absence means a platform credential;
253
+ * presence names the immutable Kaana generation this route may use.
254
+ */
255
+ customerProviderCredential: z
256
+ .object({
257
+ credentialHandle: kaanaCredentialHandleSchema,
258
+ credentialRevision: z.number().int().positive().safe(),
259
+ ownerAccountId: oxyAccountIdSchema,
260
+ connectionId: z.string().min(1).max(128),
261
+ environment: z.enum(["development", "staging", "production"]),
262
+ })
263
+ .strict()
264
+ .optional(),
265
+ };
266
+ /**
267
+ * One route the control plane has already authorized for one request.
268
+ *
269
+ * **Authorization is an ENTRY, never a boolean.** The same stance
270
+ * `inference_routing_policy_fallbacks` takes in storage: being allowed to serve
271
+ * a route IS appearing here, and every entry names its destination. A flag
272
+ * saying "substitution allowed" without naming the destination is exactly the
273
+ * silent substitution the platform forbids, and it invents a "flag set, list
274
+ * empty" state somebody then has to decide what to do with.
275
+ *
276
+ * Discriminated on `substitution`, relative to the FIRST entry — the primary
277
+ * route Oxy resolved:
278
+ *
279
+ * - `same_model` serves the same model line as the primary. This is
280
+ * availability failover between deployments of one model.
281
+ * - `cross_model` serves a DIFFERENT model line, and is expressible only with
282
+ * `authorizedByPolicy: true` as a literal. So "a model was substituted
283
+ * without the customer authorizing it" is not a sentence this contract can
284
+ * say — the same construction `inferenceRouteSwitchDetailSchema` uses to make
285
+ * the resulting route-switch event unreportable.
286
+ */
287
+ export const authorizedRouteSchema = z
288
+ .discriminatedUnion("substitution", [
289
+ z
290
+ .object({
291
+ substitution: z.literal("same_model"),
292
+ ...authorizedRouteFields,
293
+ })
294
+ .strict(),
295
+ z
296
+ .object({
297
+ substitution: z.literal("cross_model"),
298
+ ...authorizedRouteFields,
299
+ /** Literal `true`: an unauthorized substitution cannot be expressed. */
300
+ authorizedByPolicy: z.literal(true),
301
+ })
302
+ .strict(),
303
+ ])
304
+ .superRefine((route, ctx) => {
305
+ // Same rule as `modelDeploymentSchema`: a route serves specific weights. An
306
+ // unpinned entry would leave the data plane choosing a revision, which is
307
+ // the one substitution the customer never authorized by naming a model.
308
+ if (!route.modelReference.includes("@")) {
309
+ ctx.addIssue({
310
+ code: z.ZodIssueCode.custom,
311
+ path: ["modelReference"],
312
+ message: "an authorized route must pin an immutable revision (<publisher>/<model>@<revision>)",
313
+ });
314
+ }
315
+ });
@@ -0,0 +1,255 @@
1
+ /**
2
+ * Normalized stream events — what the data plane emits and the Oxy edge
3
+ * forwards as SSE.
4
+ *
5
+ * One discriminated union, seven shapes, all carrying `requestId` and a
6
+ * monotonic `sequence`. `requestId` is on EVERY event rather than only the
7
+ * first because a proxy that re-frames or a client that reconnects would
8
+ * otherwise be holding events it cannot attribute; `sequence` is what makes a
9
+ * redelivered event detectable as a duplicate rather than as new output.
10
+ *
11
+ * The events are versioned INDIVIDUALLY (see `version.ts`): a stream is a long
12
+ * sequence of small messages from a producer that may be redeployed mid-stream,
13
+ * so the alternative — one version for the whole union — would force every
14
+ * event to move whenever any one of them changed.
15
+ *
16
+ * `route_switch` is the customer-visible receipt for an allowed re-route, and
17
+ * its shape carries the invariant: a switch to a DIFFERENT model can only be
18
+ * expressed with `authorizedByPolicy: true`, so an unauthorized substitution is
19
+ * not a thing the contract can say.
20
+ *
21
+ * Decided in: docs/adr/0010-public-api-compatibility.md, docs/adr/0008-catalogue-concept-separation.md.
22
+ */
23
+ import { z } from 'zod';
24
+ import { deploymentIdSchema, generationIdSchema, inferenceProviderSlugSchema, inferenceTimestampSchema, modelIdSchema, modelReferenceSchema, requestIdSchema, } from './identifiers.js';
25
+ import { inferenceErrorSchema } from './errors.js';
26
+ import { usageQuantitySchema, usageSourceSchema } from './money.js';
27
+ /**
28
+ * The first event of every stream: what was actually resolved.
29
+ *
30
+ * Names the revision-pinned model and the serving provider, so a customer who
31
+ * asked for `<publisher>/<model>` learns which revision answered without having
32
+ * to wait for the receipt. Carries no deployment health, no route id and no
33
+ * upstream cost.
34
+ */
35
+ export const inferenceStreamStartEventSchema = z.object({
36
+ /** See `version.ts`: each stream event is a whole message on the wire. */
37
+ schemaVersion: z.literal(1),
38
+ type: z.literal('start'),
39
+ requestId: requestIdSchema,
40
+ sequence: z.number().int().nonnegative().safe(),
41
+ generationId: generationIdSchema.optional(),
42
+ /** Always revision-pinned, even when the request named only the model. */
43
+ resolvedModelReference: modelReferenceSchema,
44
+ servingProvider: inferenceProviderSlugSchema,
45
+ startedAt: inferenceTimestampSchema,
46
+ });
47
+ /**
48
+ * A chunk of output.
49
+ *
50
+ * `channel` separates visible output from reasoning and refusals, because a
51
+ * client that renders reasoning as answer text is a product bug, not a display
52
+ * preference.
53
+ */
54
+ export const inferenceStreamDeltaEventSchema = z.object({
55
+ /** See `version.ts`: each stream event is a whole message on the wire. */
56
+ schemaVersion: z.literal(1),
57
+ type: z.literal('delta'),
58
+ requestId: requestIdSchema,
59
+ sequence: z.number().int().nonnegative().safe(),
60
+ /** Which output of a multi-output response this chunk belongs to. */
61
+ outputIndex: z.number().int().nonnegative().safe(),
62
+ channel: z.enum(['output_text', 'reasoning', 'refusal']),
63
+ text: z.string(),
64
+ });
65
+ /**
66
+ * A tool call being streamed.
67
+ *
68
+ * `argumentsDelta` accumulates; `complete` marks the call finished so a client
69
+ * knows when the accumulated JSON text is worth parsing.
70
+ */
71
+ export const inferenceStreamToolCallEventSchema = z.object({
72
+ /** See `version.ts`: each stream event is a whole message on the wire. */
73
+ schemaVersion: z.literal(1),
74
+ type: z.literal('tool_call'),
75
+ requestId: requestIdSchema,
76
+ sequence: z.number().int().nonnegative().safe(),
77
+ toolCallId: z.string().min(1).max(128),
78
+ /** Present on the first event of a call. */
79
+ name: z.string().min(1).max(128).optional(),
80
+ argumentsDelta: z.string().optional(),
81
+ complete: z.boolean(),
82
+ });
83
+ /**
84
+ * Metered units for the request so far.
85
+ *
86
+ * Units only — no money. What a customer is charged is the ledger's answer,
87
+ * derived from these units and a price version at settlement; a cost quoted by
88
+ * the data plane would be a second, unauthoritative answer to the same
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.
117
+ */
118
+ export const inferenceStreamUsageEventSchema = z.object({
119
+ /** See `version.ts`: each stream event is a whole message on the wire. */
120
+ schemaVersion: z.literal(2),
121
+ type: z.literal('usage'),
122
+ requestId: requestIdSchema,
123
+ sequence: z.number().int().nonnegative().safe(),
124
+ /** Exact deployment whose metering produced these partial units. */
125
+ deploymentId: deploymentIdSchema,
126
+ units: z.array(usageQuantitySchema).min(1),
127
+ usageSource: usageSourceSchema,
128
+ });
129
+ /**
130
+ * What kind of re-route happened.
131
+ *
132
+ * `deployment` is same-model failover: the same revision, served somewhere else.
133
+ * `model` is a substitution, and is expressible ONLY with
134
+ * `authorizedByPolicy: true` — the routing policy's `authorizedCrossModel` list
135
+ * is the only thing that can produce one, so "a concrete model was silently
136
+ * replaced" has no representation in this contract.
137
+ */
138
+ export const inferenceRouteSwitchDetailSchema = z.discriminatedUnion('scope', [
139
+ z
140
+ .object({
141
+ scope: z.literal('deployment'),
142
+ modelReference: modelReferenceSchema,
143
+ toProvider: inferenceProviderSlugSchema,
144
+ toDeploymentId: deploymentIdSchema.optional(),
145
+ })
146
+ .strict(),
147
+ z
148
+ .object({
149
+ scope: z.literal('model'),
150
+ /**
151
+ * The UNPINNED model line the customer asked for (`<publisher>/<model>`,
152
+ * never `@revision`). A request that pinned a revision asked for exactly
153
+ * those weights and is served or refused, never substituted — so for such
154
+ * a request there is no value that satisfies this field, and the event
155
+ * cannot be constructed at all.
156
+ */
157
+ requestedModelId: modelIdSchema,
158
+ fromModelReference: modelReferenceSchema,
159
+ toModelReference: modelReferenceSchema,
160
+ toProvider: inferenceProviderSlugSchema,
161
+ /** Literal `true`: an unauthorized cross-model switch cannot be reported. */
162
+ authorizedByPolicy: z.literal(true),
163
+ })
164
+ .strict(),
165
+ ]);
166
+ /** Why a route changed mid-request. */
167
+ export const inferenceRouteSwitchReasonSchema = z.enum([
168
+ 'deployment_unavailable',
169
+ 'provider_error',
170
+ 'provider_timeout',
171
+ 'provider_overloaded',
172
+ 'rate_limited',
173
+ 'capacity',
174
+ 'policy_preference',
175
+ ]);
176
+ /**
177
+ * The customer-visible notice that an allowed route switch occurred.
178
+ *
179
+ * Emitted in-stream rather than only recorded on the receipt, because a
180
+ * customer comparing two answers needs to know that the second one came from
181
+ * somewhere else while they are reading it.
182
+ */
183
+ export const inferenceStreamRouteSwitchEventSchema = z.object({
184
+ /** See `version.ts`: each stream event is a whole message on the wire. */
185
+ schemaVersion: z.literal(1),
186
+ type: z.literal('route_switch'),
187
+ requestId: requestIdSchema,
188
+ sequence: z.number().int().nonnegative().safe(),
189
+ reason: inferenceRouteSwitchReasonSchema,
190
+ detail: inferenceRouteSwitchDetailSchema,
191
+ occurredAt: inferenceTimestampSchema,
192
+ });
193
+ /**
194
+ * A terminal error. The stream ends here; no `done` follows, so a client that
195
+ * saw an error never also has to reconcile a success.
196
+ */
197
+ export const inferenceStreamErrorEventSchema = z.object({
198
+ /** See `version.ts`: each stream event is a whole message on the wire. */
199
+ schemaVersion: z.literal(1),
200
+ type: z.literal('error'),
201
+ requestId: requestIdSchema,
202
+ sequence: z.number().int().nonnegative().safe(),
203
+ /** Carries its own `schemaVersion`: the same body is returned non-streaming. */
204
+ error: inferenceErrorSchema,
205
+ });
206
+ /**
207
+ * Why generation stopped.
208
+ *
209
+ * `refusal` and `content_filter` are separate members because they are separate
210
+ * events: the MODEL declining to answer is a property of the answer, while a
211
+ * filter is an upstream system removing one. The delta channels already carry
212
+ * that distinction (`channel: 'refusal'` beside the filter's own error code),
213
+ * so collapsing it here would have made the terminal event less specific than
214
+ * the stream that produced it.
215
+ */
216
+ export const inferenceFinishReasonSchema = z.enum([
217
+ 'stop',
218
+ 'length',
219
+ 'tool_calls',
220
+ 'content_filter',
221
+ 'refusal',
222
+ 'cancelled',
223
+ ]);
224
+ /**
225
+ * The successful terminal event.
226
+ *
227
+ * `receiptId` is present once settlement has produced one, giving a customer a
228
+ * direct handle on the exact amount charged rather than a telemetry estimate.
229
+ */
230
+ export const inferenceStreamDoneEventSchema = z.object({
231
+ /** See `version.ts`: each stream event is a whole message on the wire. */
232
+ schemaVersion: z.literal(1),
233
+ type: z.literal('done'),
234
+ requestId: requestIdSchema,
235
+ sequence: z.number().int().nonnegative().safe(),
236
+ generationId: generationIdSchema.optional(),
237
+ finishReason: inferenceFinishReasonSchema,
238
+ receiptId: z.string().min(1).max(128).optional(),
239
+ completedAt: inferenceTimestampSchema,
240
+ });
241
+ /**
242
+ * Every event a normalized stream can carry.
243
+ *
244
+ * Discriminated on `type`, so a consumer that meets an unknown event fails at
245
+ * the parse instead of falling into a default branch that treats it as output.
246
+ */
247
+ export const inferenceStreamEventSchema = z.discriminatedUnion('type', [
248
+ inferenceStreamStartEventSchema,
249
+ inferenceStreamDeltaEventSchema,
250
+ inferenceStreamToolCallEventSchema,
251
+ inferenceStreamUsageEventSchema,
252
+ inferenceStreamRouteSwitchEventSchema,
253
+ inferenceStreamErrorEventSchema,
254
+ inferenceStreamDoneEventSchema,
255
+ ]);