@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.
- package/LICENSE +202 -0
- package/NOTICE +16 -0
- package/dist/cjs/.tsbuildinfo +1 -0
- package/dist/cjs/accountGraph.js +489 -0
- package/dist/cjs/agency.js +439 -0
- package/dist/cjs/browserHub.js +215 -0
- package/dist/cjs/civic.js +163 -0
- package/dist/cjs/commonsSignIn.js +59 -0
- package/dist/cjs/deviceBoot.js +50 -0
- package/dist/cjs/deviceDirectory.js +189 -0
- package/dist/cjs/devicePairing.js +138 -0
- package/dist/cjs/deviceSession.js +164 -0
- package/dist/cjs/emailAgentContext.js +32 -0
- package/dist/cjs/followGraph.js +28 -0
- package/dist/cjs/identity.js +258 -0
- package/dist/cjs/inboxPush.js +24 -0
- package/dist/cjs/index.js +618 -0
- package/dist/cjs/inference/accountBilling.js +334 -0
- package/dist/cjs/inference/aliaModelRelease.js +262 -0
- package/dist/cjs/inference/attribution.js +106 -0
- package/dist/cjs/inference/catalogue.js +487 -0
- package/dist/cjs/inference/entitlement.js +217 -0
- package/dist/cjs/inference/errors.js +309 -0
- package/dist/cjs/inference/identifiers.js +224 -0
- package/dist/cjs/inference/inbox.js +105 -0
- package/dist/cjs/inference/modelDocumentation.js +433 -0
- package/dist/cjs/inference/money.js +188 -0
- package/dist/cjs/inference/priceVersion.js +110 -0
- package/dist/cjs/inference/providerConnection.js +455 -0
- package/dist/cjs/inference/request.js +477 -0
- package/dist/cjs/inference/routingPolicy.js +318 -0
- package/dist/cjs/inference/streamEvents.js +258 -0
- package/dist/cjs/inference/usage.js +329 -0
- package/dist/cjs/inference/version.js +105 -0
- package/dist/cjs/keyRecovery.js +91 -0
- package/dist/cjs/keyRotation.js +75 -0
- package/dist/cjs/links.js +68 -0
- package/dist/cjs/moderationReputation.js +298 -0
- package/dist/cjs/oauth.js +66 -0
- package/dist/cjs/oxyRecordTypes.js +71 -0
- package/dist/cjs/protocol.js +53 -0
- package/dist/cjs/recommendations.js +168 -0
- package/dist/cjs/reputation.js +297 -0
- package/dist/cjs/sessionStatus.js +121 -0
- package/dist/cjs/transparency.js +89 -0
- package/dist/cjs/updates.js +252 -0
- package/dist/cjs/userInvalidation.js +89 -0
- package/dist/cjs/userResponse.js +245 -0
- package/dist/cjs/username.js +290 -0
- package/dist/cjs/webauthn.js +71 -0
- package/dist/esm/.tsbuildinfo +1 -0
- package/dist/esm/accountGraph.js +480 -0
- package/dist/esm/agency.js +436 -0
- package/dist/esm/browserHub.js +212 -0
- package/dist/esm/civic.js +160 -0
- package/dist/esm/commonsSignIn.js +56 -0
- package/dist/esm/deviceBoot.js +47 -0
- package/dist/esm/deviceDirectory.js +186 -0
- package/dist/esm/devicePairing.js +135 -0
- package/dist/esm/deviceSession.js +161 -0
- package/dist/esm/emailAgentContext.js +29 -0
- package/dist/esm/followGraph.js +27 -0
- package/dist/esm/identity.js +255 -0
- package/dist/esm/inboxPush.js +21 -0
- package/dist/esm/index.js +172 -0
- package/dist/esm/inference/accountBilling.js +331 -0
- package/dist/esm/inference/aliaModelRelease.js +259 -0
- package/dist/esm/inference/attribution.js +103 -0
- package/dist/esm/inference/catalogue.js +484 -0
- package/dist/esm/inference/entitlement.js +214 -0
- package/dist/esm/inference/errors.js +306 -0
- package/dist/esm/inference/identifiers.js +221 -0
- package/dist/esm/inference/inbox.js +102 -0
- package/dist/esm/inference/modelDocumentation.js +430 -0
- package/dist/esm/inference/money.js +185 -0
- package/dist/esm/inference/priceVersion.js +107 -0
- package/dist/esm/inference/providerConnection.js +452 -0
- package/dist/esm/inference/request.js +474 -0
- package/dist/esm/inference/routingPolicy.js +315 -0
- package/dist/esm/inference/streamEvents.js +255 -0
- package/dist/esm/inference/usage.js +326 -0
- package/dist/esm/inference/version.js +102 -0
- package/dist/esm/keyRecovery.js +88 -0
- package/dist/esm/keyRotation.js +72 -0
- package/dist/esm/links.js +65 -0
- package/dist/esm/moderationReputation.js +295 -0
- package/dist/esm/oauth.js +63 -0
- package/dist/esm/oxyRecordTypes.js +68 -0
- package/dist/esm/protocol.js +50 -0
- package/dist/esm/recommendations.js +165 -0
- package/dist/esm/reputation.js +293 -0
- package/dist/esm/sessionStatus.js +118 -0
- package/dist/esm/transparency.js +86 -0
- package/dist/esm/updates.js +249 -0
- package/dist/esm/userInvalidation.js +85 -0
- package/dist/esm/userResponse.js +240 -0
- package/dist/esm/username.js +283 -0
- package/dist/esm/webauthn.js +68 -0
- package/dist/types/.tsbuildinfo +1 -0
- package/dist/types/accountGraph.d.ts +378 -0
- package/dist/types/agency.d.ts +2162 -0
- package/dist/types/browserHub.d.ts +856 -0
- package/dist/types/civic.d.ts +338 -0
- package/dist/types/commonsSignIn.d.ts +58 -0
- package/dist/types/deviceBoot.d.ts +74 -0
- package/dist/types/deviceDirectory.d.ts +1317 -0
- package/dist/types/devicePairing.d.ts +130 -0
- package/dist/types/deviceSession.d.ts +411 -0
- package/dist/types/emailAgentContext.d.ts +248 -0
- package/dist/types/followGraph.d.ts +150 -0
- package/dist/types/identity.d.ts +402 -0
- package/dist/types/inboxPush.d.ts +30 -0
- package/dist/types/index.d.ts +100 -0
- package/dist/types/inference/accountBilling.d.ts +738 -0
- package/dist/types/inference/aliaModelRelease.d.ts +609 -0
- package/dist/types/inference/attribution.d.ts +176 -0
- package/dist/types/inference/catalogue.d.ts +1618 -0
- package/dist/types/inference/entitlement.d.ts +519 -0
- package/dist/types/inference/errors.d.ts +242 -0
- package/dist/types/inference/identifiers.d.ts +182 -0
- package/dist/types/inference/inbox.d.ts +374 -0
- package/dist/types/inference/modelDocumentation.d.ts +1603 -0
- package/dist/types/inference/money.d.ts +185 -0
- package/dist/types/inference/priceVersion.d.ts +182 -0
- package/dist/types/inference/providerConnection.d.ts +968 -0
- package/dist/types/inference/request.d.ts +2800 -0
- package/dist/types/inference/routingPolicy.d.ts +616 -0
- package/dist/types/inference/streamEvents.d.ts +950 -0
- package/dist/types/inference/usage.d.ts +1164 -0
- package/dist/types/inference/version.d.ts +102 -0
- package/dist/types/keyRecovery.d.ts +138 -0
- package/dist/types/keyRotation.d.ts +103 -0
- package/dist/types/links.d.ts +96 -0
- package/dist/types/moderationReputation.d.ts +487 -0
- package/dist/types/oauth.d.ts +86 -0
- package/dist/types/oxyRecordTypes.d.ts +62 -0
- package/dist/types/protocol.d.ts +86 -0
- package/dist/types/recommendations.d.ts +542 -0
- package/dist/types/reputation.d.ts +457 -0
- package/dist/types/sessionStatus.d.ts +231 -0
- package/dist/types/transparency.d.ts +392 -0
- package/dist/types/updates.d.ts +545 -0
- package/dist/types/userInvalidation.d.ts +94 -0
- package/dist/types/userResponse.d.ts +1706 -0
- package/dist/types/username.d.ts +265 -0
- package/dist/types/webauthn.d.ts +77 -0
- 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
|
+
]);
|