@oxyhq/contracts 0.26.0 → 0.27.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 (46) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/accountGraph.js +4 -3
  3. package/dist/cjs/index.js +143 -1
  4. package/dist/cjs/inference/attribution.js +101 -0
  5. package/dist/cjs/inference/catalogue.js +482 -0
  6. package/dist/cjs/inference/errors.js +195 -0
  7. package/dist/cjs/inference/identifiers.js +189 -0
  8. package/dist/cjs/inference/money.js +145 -0
  9. package/dist/cjs/inference/priceVersion.js +110 -0
  10. package/dist/cjs/inference/providerConnection.js +142 -0
  11. package/dist/cjs/inference/request.js +288 -0
  12. package/dist/cjs/inference/routingPolicy.js +213 -0
  13. package/dist/cjs/inference/streamEvents.js +219 -0
  14. package/dist/cjs/inference/usage.js +291 -0
  15. package/dist/cjs/inference/version.js +57 -0
  16. package/dist/esm/.tsbuildinfo +1 -1
  17. package/dist/esm/accountGraph.js +4 -3
  18. package/dist/esm/index.js +45 -0
  19. package/dist/esm/inference/attribution.js +98 -0
  20. package/dist/esm/inference/catalogue.js +479 -0
  21. package/dist/esm/inference/errors.js +192 -0
  22. package/dist/esm/inference/identifiers.js +186 -0
  23. package/dist/esm/inference/money.js +142 -0
  24. package/dist/esm/inference/priceVersion.js +107 -0
  25. package/dist/esm/inference/providerConnection.js +139 -0
  26. package/dist/esm/inference/request.js +285 -0
  27. package/dist/esm/inference/routingPolicy.js +210 -0
  28. package/dist/esm/inference/streamEvents.js +216 -0
  29. package/dist/esm/inference/usage.js +288 -0
  30. package/dist/esm/inference/version.js +54 -0
  31. package/dist/types/.tsbuildinfo +1 -1
  32. package/dist/types/accountGraph.d.ts +6 -5
  33. package/dist/types/index.d.ts +23 -0
  34. package/dist/types/inference/attribution.d.ts +171 -0
  35. package/dist/types/inference/catalogue.d.ts +1612 -0
  36. package/dist/types/inference/errors.d.ts +193 -0
  37. package/dist/types/inference/identifiers.d.ts +149 -0
  38. package/dist/types/inference/money.d.ts +142 -0
  39. package/dist/types/inference/priceVersion.d.ts +182 -0
  40. package/dist/types/inference/providerConnection.d.ts +297 -0
  41. package/dist/types/inference/request.d.ts +2364 -0
  42. package/dist/types/inference/routingPolicy.d.ts +426 -0
  43. package/dist/types/inference/streamEvents.d.ts +906 -0
  44. package/dist/types/inference/usage.d.ts +1133 -0
  45. package/dist/types/inference/version.d.ts +54 -0
  46. package/package.json +1 -1
@@ -14,9 +14,10 @@ import { z } from 'zod';
14
14
  * `db/schema/users.ts` mirrors to keep the `users_kind_check` CHECK honest.
15
15
  *
16
16
  * Deriving the union from the array instead would cost nothing here and be paid
17
- * by consumers: `kind` travels into `@oxyhq/services` through
18
- * `SwitchableAccount`, where an indexed-access type is materially more
19
- * expensive to check than a literal union.
17
+ * by consumers: `kind` travels into `@oxyhq/services` on every device-directory
18
+ * context (`deviceContextSchema.kind` `DeviceContext` the switcher rows),
19
+ * and an indexed-access type is materially more expensive to check there than a
20
+ * literal union.
20
21
  */
21
22
  export const ACCOUNT_KINDS = [
22
23
  'personal',
package/dist/esm/index.js CHANGED
@@ -102,3 +102,48 @@ devicePairingStatusSchema, deviceTransferInitRequestSchema, deviceTransferInitRe
102
102
  export {
103
103
  // Schemas — transparency log (checkpoints + inclusion proofs)
104
104
  transparencyCheckpointSignatureSchema, transparencyAnchorSchema, transparencyCheckpointSchema, transparencyInclusionProofSchema, transparencyCheckpointListSchema, } from './transparency.js';
105
+ /* -------------------------------------------------------------------------- */
106
+ /* Inference (Oxy↔data-plane) — issue #972 */
107
+ /* -------------------------------------------------------------------------- */
108
+ export {
109
+ // The version of the contract SET; per-shape versions live in the data.
110
+ INFERENCE_CONTRACT_VERSION, } from './inference/version.js';
111
+ export {
112
+ // Principal identifiers. `oxyAccountIdSchema` and `delegatedUserIdSchema`
113
+ // are branded apart so a delegated end user can never become the payer.
114
+ oxyAccountIdSchema, delegatedUserIdSchema, oxyApplicationIdSchema, oxyCredentialIdSchema, requestIdSchema, generationIdSchema, idempotencyKeySchema, inferenceEnvironmentSchema,
115
+ // Wire primitives
116
+ inferenceTimestampSchema, inferenceDateSchema, inferenceHttpsUrlSchema,
117
+ // Catalogue references
118
+ publisherSlugSchema, modelSlugSchema, modelIdSchema, modelRevisionLabelSchema, modelReferenceSchema, routingProfileSlugSchema, inferenceProviderSlugSchema, deploymentIdSchema, inferenceRegionSchema, RESERVED_ALIA_PUBLISHER, } from './inference/identifiers.js';
119
+ export {
120
+ // Exact money and metered units — never floats, units never money.
121
+ currencyCodeSchema, INFERENCE_MONEY_SCALE, exactDecimalSchema, moneySchema, USAGE_UNITS, usageUnitSchema, USAGE_SOURCES, usageSourceSchema, usageQuantitySchema, unitPriceSchema, } from './inference/money.js';
122
+ export {
123
+ // Canonical attribution: who pays, which app, which credential, which user.
124
+ INFERENCE_SCOPES, inferenceScopeSchema, billingPrincipalSchema, authenticatedPrincipalSchema, inferenceAttributionSchema, } from './inference/attribution.js';
125
+ export {
126
+ // Closed error vocabulary + retryability + a leak-proof provider passthrough.
127
+ INFERENCE_ERROR_CODES, NON_RETRYABLE_INFERENCE_ERROR_CODES, inferenceErrorCodeSchema, upstreamErrorCategorySchema, safeErrorTextSchema, providerErrorPassthroughSchema, inferenceErrorSchema, } from './inference/errors.js';
128
+ export {
129
+ // Price versions and the snapshot a settled receipt keeps.
130
+ priceVersionStatusSchema, priceVersionSchema, priceSnapshotSchema, } from './inference/priceVersion.js';
131
+ export {
132
+ // The six distinct catalogue objects + the customer-safe projection.
133
+ inferenceModalitySchema, modelCapabilitiesSchema, modelLicenseSchema, modelProvenanceSchema, inferenceDataPolicySchema, availabilityScopeSchema, commercialPermissionSchema, modelDeprecationSchema, modelEvaluationResultSchema, modelSafetyMetadataSchema, modelPublisherSchema, catalogueModelSchema, modelRevisionSchema, inferenceProviderSchema, modelDeploymentSchema, routingProfileCandidateSchema, routingProfileSchema, cataloguePublisherSummarySchema, catalogueServingProviderSummarySchema, modelCatalogueEntrySchema, } from './inference/catalogue.js';
134
+ export {
135
+ // Routing policy: every control, plus the refinement that rejects a policy
136
+ // no route could ever satisfy.
137
+ routingTargetSchema, routingPolicyScopeSchema, routingFallbackPolicySchema, routingPolicySchema, routingPolicyReferenceSchema, } from './inference/routingPolicy.js';
138
+ export {
139
+ // The normalized Oxy→data-plane request envelope.
140
+ inferenceContentSourceSchema, inferenceContentPartSchema, inferenceToolCallSchema, inferenceMessageRoleSchema, inferenceMessageSchema, inferenceInputSchema, samplingParametersSchema, toolDefinitionSchema, toolChoiceSchema, responseFormatSchema, clientRequestMetadataSchema, inferenceRequestSchema, } from './inference/request.js';
141
+ export {
142
+ // Normalized SSE events.
143
+ inferenceStreamStartEventSchema, inferenceStreamDeltaEventSchema, inferenceStreamToolCallEventSchema, inferenceStreamUsageEventSchema, inferenceRouteSwitchDetailSchema, inferenceRouteSwitchReasonSchema, inferenceStreamRouteSwitchEventSchema, inferenceStreamErrorEventSchema, inferenceFinishReasonSchema, inferenceStreamDoneEventSchema, inferenceStreamEventSchema, } from './inference/streamEvents.js';
144
+ export {
145
+ // Reserve → settle → refund.
146
+ usageReservationRequestSchema, usageReservationStatusSchema, usageReservationSchema, inferenceRequestOutcomeSchema, normalizedUsageReportSchema, usageReceiptSchema, usageRefundSubjectSchema, usageRefundReasonSchema, usageRefundSchema, } from './inference/usage.js';
147
+ export {
148
+ // BYOK connection metadata that structurally cannot carry a secret.
149
+ providerConnectionScopeSchema, providerSecretReferenceSchema, providerConnectionValidationSchema, providerConnectionStatusSchema, providerConnectionSchema, } from './inference/providerConnection.js';
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Canonical attribution for an inference request.
3
+ *
4
+ * Every accepted request resolves to an Oxy account, an Oxy application, the
5
+ * Oxy credential that authenticated it, an optional delegated end user, and the
6
+ * ids that correlate it across the edge, the data plane and the ledger. The
7
+ * data plane may store these as immutable references; it never owns or mutates
8
+ * them, and it never mints a customer identity of its own.
9
+ *
10
+ * The rule this file encodes structurally, rather than restating in prose:
11
+ * **the delegated `userId` can never be the billing identity.** Two independent
12
+ * mechanisms enforce it, one at compile time and one at parse time, because a
13
+ * delegated identity being charged for somebody else's workload is the kind of
14
+ * mistake that produces a correct-looking invoice for the wrong customer:
15
+ *
16
+ * 1. `accountId` and `userId` carry DIFFERENT brands, so neither is assignable
17
+ * to the other in any consumer without a cast.
18
+ * 2. {@link billingPrincipalSchema} is `.strict()` and holds exactly one field,
19
+ * so a payload that smuggles `userId` into the billing block is rejected at
20
+ * the parse rather than stripped and forgotten.
21
+ *
22
+ * These shapes are EMBEDDED — they ride inside a request envelope, a receipt or
23
+ * a ledger record and inherit its `schemaVersion`. Versioning them separately
24
+ * would let one message claim two versions.
25
+ *
26
+ * Decided in: docs/adr/0007-canonical-request-attribution.md.
27
+ */
28
+ import { z } from 'zod';
29
+ import { delegatedUserIdSchema, generationIdSchema, inferenceEnvironmentSchema, oxyAccountIdSchema, oxyApplicationIdSchema, oxyCredentialIdSchema, requestIdSchema, } from './identifiers.js';
30
+ /**
31
+ * The inference capability scopes the data plane needs to know about.
32
+ *
33
+ * A credential may carry many other Oxy scopes; only these cross the boundary,
34
+ * because the data plane's authorization questions are exactly "may this caller
35
+ * invoke", "may it read the catalogue", "may it read usage", "may it read or
36
+ * write routing", "may it read or write provider connections". Everything else
37
+ * is the control plane's business and is not the data plane's to hold.
38
+ */
39
+ export const INFERENCE_SCOPES = [
40
+ 'inference:invoke',
41
+ 'inference:models:read',
42
+ 'inference:usage:read',
43
+ 'inference:routing:read',
44
+ 'inference:routing:write',
45
+ 'inference:providers:read',
46
+ 'inference:providers:write',
47
+ ];
48
+ export const inferenceScopeSchema = z.enum(INFERENCE_SCOPES);
49
+ /**
50
+ * The financially responsible principal, and the ONLY identity a charge may be
51
+ * booked against.
52
+ *
53
+ * It is its own type — not a field on a larger principal object — precisely so
54
+ * that a function taking "who pays" cannot be handed a user, a session, a
55
+ * device or an application. It cannot be constructed from a delegated user id:
56
+ * the brands differ, and this object accepts no other key.
57
+ */
58
+ export const billingPrincipalSchema = z
59
+ .object({
60
+ accountId: oxyAccountIdSchema,
61
+ })
62
+ .strict();
63
+ /**
64
+ * Who authenticated, as resolved by the Oxy edge before a request is forwarded.
65
+ *
66
+ * Mirrors what a verified Oxy service token carries (`appId`, `credentialId`,
67
+ * `ownerAccountId`, `environment`, effective scopes) so that the two
68
+ * authentication paths — a machine API key and a first-party service token —
69
+ * produce one shape downstream. The data plane authorizes against this
70
+ * envelope; it does not re-derive access from its own database, because it has
71
+ * no account graph to re-derive it from.
72
+ */
73
+ export const authenticatedPrincipalSchema = z.object({
74
+ billing: billingPrincipalSchema,
75
+ applicationId: oxyApplicationIdSchema,
76
+ credentialId: oxyCredentialIdSchema,
77
+ environment: inferenceEnvironmentSchema,
78
+ inferenceScopes: z.array(inferenceScopeSchema),
79
+ });
80
+ /**
81
+ * The attribution block carried by every request, receipt and ledger record.
82
+ *
83
+ * `userId` is the OPTIONAL delegated end user — Alia's `X-Oxy-User-Id`. It is
84
+ * attribution only: it never changes which account is charged, never grants
85
+ * access, and lives outside {@link billingPrincipalSchema} so that no code path
86
+ * can read it as the payer.
87
+ *
88
+ * `requestId` is generated by the data plane and always present; `generationId`
89
+ * is present
90
+ * once a generation exists, which is why it is optional on a request and
91
+ * expected on a receipt.
92
+ */
93
+ export const inferenceAttributionSchema = z.object({
94
+ principal: authenticatedPrincipalSchema,
95
+ userId: delegatedUserIdSchema.optional(),
96
+ requestId: requestIdSchema,
97
+ generationId: generationIdSchema.optional(),
98
+ });
@@ -0,0 +1,479 @@
1
+ /**
2
+ * The canonical model catalogue.
3
+ *
4
+ * Six things are kept DISTINCT here, because collapsing any pair of them is how
5
+ * a catalogue starts lying to customers:
6
+ *
7
+ * 1. **Publisher** — who released the model (`openai`, `meta`, `alia`).
8
+ * 2. **Model** — a long-lived product identity, `<publisher>/<model>`. Its
9
+ * behaviour changes over time as revisions ship.
10
+ * 3. **Model revision** — an IMMUTABLE point in that history,
11
+ * `<publisher>/<model>@<revision>`. This is what an evaluation result, a
12
+ * model card and an artifact digest actually describe.
13
+ * 4. **Inference provider** — who runs the weights (a third party, Oxy's own
14
+ * hosting, or the customer's own account under BYOK).
15
+ * 5. **Deployment/endpoint** — one concrete servable route: a revision, on a
16
+ * provider, in a region, under a data policy, with a commercial permission.
17
+ * 6. **Routing profile** — a named strategy for CHOOSING among routes
18
+ * (`auto`, `fast`, `quality`). A profile is not a model; it has no weights,
19
+ * no revision and no license, and it is never written in the shape of a
20
+ * model id.
21
+ *
22
+ * `modelCatalogueEntrySchema` is the customer-safe projection Oxy serves from
23
+ * `GET /v1/models`: it repeats the customer-facing fields rather than embedding
24
+ * the operational descriptors, so internal deployment ids, upstream wholesale
25
+ * costs and route health can never reach it by accident of nesting.
26
+ *
27
+ * Decided in: docs/adr/0008-catalogue-concept-separation.md.
28
+ */
29
+ import { z } from 'zod';
30
+ import { deploymentIdSchema, inferenceDateSchema, inferenceHttpsUrlSchema, inferenceProviderSlugSchema, inferenceRegionSchema, inferenceTimestampSchema, modelIdSchema, modelReferenceSchema, modelRevisionLabelSchema, modelSlugSchema, publisherSlugSchema, RESERVED_ALIA_PUBLISHER, routingProfileSlugSchema, } from './identifiers.js';
31
+ import { priceSnapshotSchema } from './priceVersion.js';
32
+ /* -------------------------------------------------------------------------- */
33
+ /* Shared catalogue vocabulary */
34
+ /* -------------------------------------------------------------------------- */
35
+ /** The modalities a model can consume or produce. */
36
+ export const inferenceModalitySchema = z.enum([
37
+ 'text',
38
+ 'image',
39
+ 'audio',
40
+ 'video',
41
+ 'embedding',
42
+ ]);
43
+ /**
44
+ * What a model can do, in the terms a caller has to decide against before
45
+ * sending a request: can it call tools, does it accept images, will it honour a
46
+ * JSON schema, how much context does it take, how much can it emit.
47
+ */
48
+ export const modelCapabilitiesSchema = z
49
+ .object({
50
+ inputModalities: z.array(inferenceModalitySchema).min(1),
51
+ outputModalities: z.array(inferenceModalitySchema).min(1),
52
+ tools: z.boolean(),
53
+ parallelToolCalls: z.boolean(),
54
+ structuredOutput: z.boolean(),
55
+ jsonMode: z.boolean(),
56
+ reasoning: z.boolean(),
57
+ streaming: z.boolean(),
58
+ promptCaching: z.boolean(),
59
+ maxContextTokens: z.number().int().positive().safe(),
60
+ maxOutputTokens: z.number().int().positive().safe(),
61
+ })
62
+ .strict();
63
+ /**
64
+ * License terms, including the two questions that decide whether Oxy may serve
65
+ * a model to third parties at all: is commercial use permitted, and must the
66
+ * base model be attributed.
67
+ */
68
+ export const modelLicenseSchema = z
69
+ .object({
70
+ /** SPDX identifier where one exists, otherwise the publisher's own name. */
71
+ licenseId: z.string().min(1).max(128),
72
+ displayName: z.string().min(1).max(200),
73
+ url: inferenceHttpsUrlSchema.optional(),
74
+ commercialUseAllowed: z.boolean(),
75
+ requiresAttribution: z.boolean(),
76
+ acceptableUsePolicyUrl: inferenceHttpsUrlSchema.optional(),
77
+ })
78
+ .strict();
79
+ /**
80
+ * How a model came to exist.
81
+ *
82
+ * `releaseKind` is what stops `alia/*` from becoming a re-badging namespace:
83
+ * a first-party namespace may only carry models Alia actually trained or
84
+ * derived, never a third-party route wearing an Oxy name.
85
+ */
86
+ export const modelProvenanceSchema = z
87
+ .object({
88
+ releaseKind: z.enum([
89
+ 'first_party_original',
90
+ 'first_party_derived',
91
+ 'open_weight',
92
+ 'third_party_hosted',
93
+ ]),
94
+ /** The model this one was derived/fine-tuned from, where there is one. */
95
+ baseModelId: modelIdSchema.optional(),
96
+ trainingOrganization: z.string().min(1).max(200).optional(),
97
+ })
98
+ .strict();
99
+ /**
100
+ * What happens to the customer's prompts and responses on a given route.
101
+ *
102
+ * Every field here is enforceable by a routing policy, which is the reason they
103
+ * are structured rather than a prose paragraph on a docs page.
104
+ */
105
+ export const inferenceDataPolicySchema = z
106
+ .object({
107
+ retainsPayloads: z.boolean(),
108
+ /** Zero when nothing is retained. */
109
+ retentionDays: z.number().int().nonnegative().max(3650),
110
+ trainsOnCustomerData: z.boolean(),
111
+ zeroDataRetentionAvailable: z.boolean(),
112
+ /** Named subprocessors a payload may pass through, for compliance review. */
113
+ subprocessors: z.array(z.string().min(1).max(200)).default([]),
114
+ policyUrl: inferenceHttpsUrlSchema.optional(),
115
+ })
116
+ .strict()
117
+ .superRefine((policy, ctx) => {
118
+ if (!policy.retainsPayloads && policy.retentionDays > 0) {
119
+ ctx.addIssue({
120
+ code: z.ZodIssueCode.custom,
121
+ path: ['retentionDays'],
122
+ message: 'a route that retains no payloads cannot have a retention window',
123
+ });
124
+ }
125
+ // Training requires having the data to train on. A route claiming both is
126
+ // reporting one of the two fields wrongly, and a routing policy that
127
+ // prohibits training would be enforced against a value that is not true.
128
+ if (!policy.retainsPayloads && policy.trainsOnCustomerData) {
129
+ ctx.addIssue({
130
+ code: z.ZodIssueCode.custom,
131
+ path: ['trainsOnCustomerData'],
132
+ message: 'a route that retains no payloads cannot train on customer data',
133
+ });
134
+ }
135
+ });
136
+ /**
137
+ * Who a route may be served to. Availability inside Alia never implies
138
+ * permission to resell the same provider/model publicly, which is why this is
139
+ * an explicit scope on the route rather than a boolean derived from "it works".
140
+ */
141
+ export const availabilityScopeSchema = z.enum([
142
+ 'internal_alia',
143
+ 'public_payg',
144
+ 'enterprise',
145
+ 'byok_only',
146
+ 'oxy_hosted',
147
+ ]);
148
+ /** The commercial basis on which Oxy may serve a route. */
149
+ export const commercialPermissionSchema = z.enum([
150
+ 'standard_application_use',
151
+ 'public_resale_approved',
152
+ 'wholesale_contract',
153
+ 'customer_byok',
154
+ 'open_weight_hosting',
155
+ ]);
156
+ /** Deprecation state, with the replacement a customer should migrate to. */
157
+ export const modelDeprecationSchema = z
158
+ .object({
159
+ status: z.enum(['active', 'deprecated', 'retired']),
160
+ replacementModelReference: modelReferenceSchema.optional(),
161
+ announcedAt: inferenceTimestampSchema.optional(),
162
+ sunsetAt: inferenceTimestampSchema.optional(),
163
+ })
164
+ .strict()
165
+ .superRefine((deprecation, ctx) => {
166
+ if (deprecation.status === 'active' && deprecation.sunsetAt !== undefined) {
167
+ ctx.addIssue({
168
+ code: z.ZodIssueCode.custom,
169
+ path: ['sunsetAt'],
170
+ message: 'an active model has no sunset date; announce the deprecation first',
171
+ });
172
+ }
173
+ });
174
+ /** One published evaluation result for a revision. */
175
+ export const modelEvaluationResultSchema = z
176
+ .object({
177
+ suite: z.string().min(1).max(128),
178
+ metric: z.string().min(1).max(128),
179
+ /** Kept as a string: an eval score is published text, not arithmetic. */
180
+ score: z.string().min(1).max(64),
181
+ evaluatedAt: inferenceTimestampSchema.optional(),
182
+ reportUrl: inferenceHttpsUrlSchema.optional(),
183
+ })
184
+ .strict();
185
+ /**
186
+ * Safety metadata a downstream developer needs, and the EU AI Act / GPAI
187
+ * documentation trail expects to find beside a served model.
188
+ */
189
+ export const modelSafetyMetadataSchema = z
190
+ .object({
191
+ safetyCardUrl: inferenceHttpsUrlSchema.optional(),
192
+ contentFilteringDefault: z.enum(['none', 'provider_default', 'strict']),
193
+ knownLimitations: z.array(z.string().min(1).max(500)).default([]),
194
+ /** Content-provenance marking the modality requires, where it requires one. */
195
+ provenanceMarking: z.enum(['none', 'visible_watermark', 'invisible_watermark', 'c2pa']),
196
+ })
197
+ .strict();
198
+ /* -------------------------------------------------------------------------- */
199
+ /* 1. Publisher */
200
+ /* -------------------------------------------------------------------------- */
201
+ /** Who released a model. Never a provider, and never a routing profile. */
202
+ export const modelPublisherSchema = z.object({
203
+ /** See `version.ts`: served on its own by the catalogue, so it is versioned. */
204
+ schemaVersion: z.literal(1),
205
+ publisherId: z.string().min(1).max(128),
206
+ slug: publisherSlugSchema,
207
+ displayName: z.string().min(1).max(200),
208
+ description: z.string().max(2000).optional(),
209
+ websiteUrl: inferenceHttpsUrlSchema.optional(),
210
+ });
211
+ /* -------------------------------------------------------------------------- */
212
+ /* 2. Model */
213
+ /* -------------------------------------------------------------------------- */
214
+ /**
215
+ * A model: the long-lived product identity `<publisher>/<model>`.
216
+ *
217
+ * Carries no provider, no region, no price and no availability — those belong
218
+ * to a DEPLOYMENT, because the same model is served on several routes whose
219
+ * answers to those questions differ.
220
+ */
221
+ export const catalogueModelSchema = z
222
+ .object({
223
+ /** See `version.ts`: served on its own by the catalogue, so it is versioned. */
224
+ schemaVersion: z.literal(1),
225
+ modelId: modelIdSchema,
226
+ publisher: publisherSlugSchema,
227
+ slug: modelSlugSchema,
228
+ displayName: z.string().min(1).max(200),
229
+ description: z.string().max(4000).optional(),
230
+ capabilities: modelCapabilitiesSchema,
231
+ license: modelLicenseSchema,
232
+ provenance: modelProvenanceSchema,
233
+ knowledgeCutoff: inferenceDateSchema.optional(),
234
+ releasedOn: inferenceDateSchema.optional(),
235
+ /** The revision served when a caller names the model without pinning one. */
236
+ currentRevision: modelRevisionLabelSchema,
237
+ deprecation: modelDeprecationSchema,
238
+ })
239
+ .superRefine((model, ctx) => {
240
+ if (model.modelId !== `${model.publisher}/${model.slug}`) {
241
+ ctx.addIssue({
242
+ code: z.ZodIssueCode.custom,
243
+ path: ['modelId'],
244
+ message: 'modelId must be exactly <publisher>/<model>',
245
+ });
246
+ }
247
+ // `alia/*` is reserved for models Alia actually owns or derived. A provider
248
+ // alias published under it would make an Oxy-branded model id mean nothing,
249
+ // and would put an Oxy name on somebody else's weights and license.
250
+ if (model.publisher === RESERVED_ALIA_PUBLISHER &&
251
+ model.provenance.releaseKind !== 'first_party_original' &&
252
+ model.provenance.releaseKind !== 'first_party_derived') {
253
+ ctx.addIssue({
254
+ code: z.ZodIssueCode.custom,
255
+ path: ['provenance', 'releaseKind'],
256
+ message: `the ${RESERVED_ALIA_PUBLISHER}/* namespace is reserved for first-party releases`,
257
+ });
258
+ }
259
+ });
260
+ /* -------------------------------------------------------------------------- */
261
+ /* 3. Model revision */
262
+ /* -------------------------------------------------------------------------- */
263
+ /**
264
+ * An immutable revision of a model.
265
+ *
266
+ * `reference` is the exact string a customer pins (`<publisher>/<model>@<revision>`)
267
+ * and is checked against its parts, so a record cannot claim a reference that
268
+ * resolves elsewhere. Evaluation and safety metadata hang here rather than on
269
+ * the model, because they describe specific weights.
270
+ */
271
+ export const modelRevisionSchema = z
272
+ .object({
273
+ /** See `version.ts`: served on its own by the catalogue, so it is versioned. */
274
+ schemaVersion: z.literal(1),
275
+ revisionId: z.string().min(1).max(128),
276
+ modelId: modelIdSchema,
277
+ revision: modelRevisionLabelSchema,
278
+ reference: modelReferenceSchema,
279
+ releasedAt: inferenceTimestampSchema,
280
+ retiredAt: inferenceTimestampSchema.optional(),
281
+ /** Digest of the served artifact, where Oxy hosts the weights itself. */
282
+ artifactDigest: z
283
+ .string()
284
+ .regex(/^sha256:[a-f0-9]{64}$/, 'artifact digest must be sha256:<64 lowercase hex>')
285
+ .optional(),
286
+ modelCardUrl: inferenceHttpsUrlSchema.optional(),
287
+ evaluations: z.array(modelEvaluationResultSchema).default([]),
288
+ safety: modelSafetyMetadataSchema.optional(),
289
+ })
290
+ .superRefine((revision, ctx) => {
291
+ if (revision.reference !== `${revision.modelId}@${revision.revision}`) {
292
+ ctx.addIssue({
293
+ code: z.ZodIssueCode.custom,
294
+ path: ['reference'],
295
+ message: 'reference must be exactly <modelId>@<revision>',
296
+ });
297
+ }
298
+ });
299
+ /* -------------------------------------------------------------------------- */
300
+ /* 4. Inference provider */
301
+ /* -------------------------------------------------------------------------- */
302
+ /** Who runs the weights for a route. */
303
+ export const inferenceProviderSchema = z.object({
304
+ /** See `version.ts`: served on its own by the catalogue, so it is versioned. */
305
+ schemaVersion: z.literal(1),
306
+ providerId: z.string().min(1).max(128),
307
+ slug: inferenceProviderSlugSchema,
308
+ displayName: z.string().min(1).max(200),
309
+ /**
310
+ * `customer_byok` means the customer's own upstream account is billed
311
+ * directly by the provider and Oxy charges only its platform fee.
312
+ */
313
+ kind: z.enum(['third_party', 'oxy_hosted', 'customer_byok']),
314
+ websiteUrl: inferenceHttpsUrlSchema.optional(),
315
+ statusPageUrl: inferenceHttpsUrlSchema.optional(),
316
+ regions: z.array(inferenceRegionSchema).default([]),
317
+ dataPolicy: inferenceDataPolicySchema,
318
+ });
319
+ /* -------------------------------------------------------------------------- */
320
+ /* 5. Deployment / endpoint */
321
+ /* -------------------------------------------------------------------------- */
322
+ /**
323
+ * One concrete servable route: a revision, on a provider, in some regions,
324
+ * under a data policy, with an availability scope and a commercial permission.
325
+ *
326
+ * This is the object a routing policy filters and a route switch names — never
327
+ * a model. Two deployments of the SAME revision are what same-model failover
328
+ * moves between; moving to a deployment of a different revision or model is a
329
+ * cross-model fallback and needs explicit authorization.
330
+ */
331
+ export const modelDeploymentSchema = z
332
+ .object({
333
+ /** See `version.ts`: exchanged with the data plane on its own. */
334
+ schemaVersion: z.literal(1),
335
+ deploymentId: deploymentIdSchema,
336
+ provider: inferenceProviderSlugSchema,
337
+ /** Always revision-pinned: a deployment serves specific weights. */
338
+ modelReference: modelReferenceSchema,
339
+ regions: z.array(inferenceRegionSchema).min(1),
340
+ dataPolicy: inferenceDataPolicySchema,
341
+ availabilityScope: availabilityScopeSchema,
342
+ commercialPermission: commercialPermissionSchema,
343
+ status: z.enum(['active', 'degraded', 'disabled', 'retired']),
344
+ /** Reserved capacity for one enterprise account rather than shared. */
345
+ dedicatedCapacity: z.boolean(),
346
+ /** The price version customers are charged under on this route. */
347
+ priceVersionId: z.string().min(1).max(128).optional(),
348
+ })
349
+ .superRefine((deployment, ctx) => {
350
+ if (!deployment.modelReference.includes('@')) {
351
+ ctx.addIssue({
352
+ code: z.ZodIssueCode.custom,
353
+ path: ['modelReference'],
354
+ message: 'a deployment must pin an immutable revision (<publisher>/<model>@<revision>)',
355
+ });
356
+ }
357
+ // A technically callable route is not automatically publicly resellable.
358
+ // Publishing one to pay-as-you-go customers requires a permission state that
359
+ // says somebody reviewed the right to resell it, not merely that it answers.
360
+ if (deployment.availabilityScope === 'public_payg' &&
361
+ deployment.commercialPermission !== 'public_resale_approved' &&
362
+ deployment.commercialPermission !== 'wholesale_contract' &&
363
+ deployment.commercialPermission !== 'open_weight_hosting') {
364
+ ctx.addIssue({
365
+ code: z.ZodIssueCode.custom,
366
+ path: ['commercialPermission'],
367
+ message: 'a public pay-as-you-go route requires an approved resale permission',
368
+ });
369
+ }
370
+ if (deployment.availabilityScope === 'byok_only' &&
371
+ deployment.commercialPermission !== 'customer_byok') {
372
+ ctx.addIssue({
373
+ code: z.ZodIssueCode.custom,
374
+ path: ['commercialPermission'],
375
+ message: 'a BYOK-only route is served under the customer’s own provider terms',
376
+ });
377
+ }
378
+ // Pricing is what Oxy charges. A BYOK route bills the customer upstream and
379
+ // carries only a platform fee, which is not a per-unit model price.
380
+ if (deployment.priceVersionId !== undefined && deployment.availabilityScope === 'byok_only') {
381
+ ctx.addIssue({
382
+ code: z.ZodIssueCode.custom,
383
+ path: ['priceVersionId'],
384
+ message: 'a BYOK-only route has no customer model price; the platform fee is separate',
385
+ });
386
+ }
387
+ });
388
+ /* -------------------------------------------------------------------------- */
389
+ /* 6. Routing profile */
390
+ /* -------------------------------------------------------------------------- */
391
+ /** One candidate a profile may resolve to, and how strongly it is preferred. */
392
+ export const routingProfileCandidateSchema = z
393
+ .object({
394
+ modelReference: modelReferenceSchema,
395
+ /** Lower sorts first. Ties are broken by the profile's optimisation target. */
396
+ priority: z.number().int().nonnegative().max(1000),
397
+ })
398
+ .strict();
399
+ /**
400
+ * A named strategy for choosing among routes — `auto`, `fast`, `quality`.
401
+ *
402
+ * Not a model: it has no publisher, no revision, no license and no weights, and
403
+ * its slug cannot be written as `<publisher>/<model>`. A request that names a
404
+ * profile is asking Oxy to choose; a request that names a model is not, which is
405
+ * the distinction the "never silently substituted" invariant rests on.
406
+ */
407
+ export const routingProfileSchema = z.object({
408
+ /** See `version.ts`: served on its own by the catalogue, so it is versioned. */
409
+ schemaVersion: z.literal(1),
410
+ routingProfileId: z.string().min(1).max(128),
411
+ slug: routingProfileSlugSchema,
412
+ displayName: z.string().min(1).max(200),
413
+ description: z.string().max(2000).optional(),
414
+ /** What the profile optimises for when several candidates qualify. */
415
+ optimiseFor: z.enum(['price', 'latency', 'throughput', 'quality', 'balanced']),
416
+ candidates: z.array(routingProfileCandidateSchema).min(1),
417
+ /** A product preset (Alia's "fast" toggle) rather than a customer's own profile. */
418
+ isProductPreset: z.boolean(),
419
+ });
420
+ /* -------------------------------------------------------------------------- */
421
+ /* Customer-safe catalogue projection */
422
+ /* -------------------------------------------------------------------------- */
423
+ /** The publisher fields a customer sees beside a model. */
424
+ export const cataloguePublisherSummarySchema = z
425
+ .object({
426
+ slug: publisherSlugSchema,
427
+ displayName: z.string().min(1).max(200),
428
+ websiteUrl: inferenceHttpsUrlSchema.optional(),
429
+ })
430
+ .strict();
431
+ /**
432
+ * A serving provider as a customer may see it: who ran the request and where.
433
+ *
434
+ * Deliberately has no deployment id, no route id, no health score and no
435
+ * upstream cost — the customer-safe half of the serving boundary, so that
436
+ * exposing attribution can never leak operational topology.
437
+ */
438
+ export const catalogueServingProviderSummarySchema = z
439
+ .object({
440
+ slug: inferenceProviderSlugSchema,
441
+ displayName: z.string().min(1).max(200),
442
+ regions: z.array(inferenceRegionSchema).default([]),
443
+ dataPolicy: inferenceDataPolicySchema,
444
+ })
445
+ .strict();
446
+ /**
447
+ * One entry of the customer-facing catalogue (`GET /v1/models`).
448
+ *
449
+ * A projection, not a container: it repeats the customer-facing fields instead
450
+ * of embedding the operational descriptors, so no internal identifier can reach
451
+ * a customer by being nested one level deeper than anybody looked.
452
+ */
453
+ export const modelCatalogueEntrySchema = z.object({
454
+ /** See `version.ts`: this is the public catalogue response shape. */
455
+ schemaVersion: z.literal(1),
456
+ modelId: modelIdSchema,
457
+ publisher: cataloguePublisherSummarySchema,
458
+ displayName: z.string().min(1).max(200),
459
+ description: z.string().max(4000).optional(),
460
+ currentRevision: modelRevisionLabelSchema,
461
+ /** Revisions a customer may pin today, newest first. */
462
+ availableRevisions: z.array(modelRevisionLabelSchema).min(1),
463
+ capabilities: modelCapabilitiesSchema,
464
+ license: modelLicenseSchema,
465
+ provenance: modelProvenanceSchema,
466
+ knowledgeCutoff: inferenceDateSchema.optional(),
467
+ releasedOn: inferenceDateSchema.optional(),
468
+ regions: z.array(inferenceRegionSchema).default([]),
469
+ servingProviders: z.array(catalogueServingProviderSummarySchema).default([]),
470
+ dataPolicy: inferenceDataPolicySchema,
471
+ /** Absent for routes a customer cannot buy per-unit (BYOK-only, internal). */
472
+ pricing: priceSnapshotSchema.optional(),
473
+ availabilityScope: availabilityScopeSchema,
474
+ commercialPermission: commercialPermissionSchema,
475
+ deprecation: modelDeprecationSchema,
476
+ evaluations: z.array(modelEvaluationResultSchema).default([]),
477
+ safety: modelSafetyMetadataSchema.optional(),
478
+ modelCardUrl: inferenceHttpsUrlSchema.optional(),
479
+ });