@oxyhq/contracts 0.26.0 → 0.28.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 (52) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/accountGraph.js +4 -3
  3. package/dist/cjs/index.js +186 -1
  4. package/dist/cjs/inference/accountBilling.js +334 -0
  5. package/dist/cjs/inference/attribution.js +106 -0
  6. package/dist/cjs/inference/catalogue.js +482 -0
  7. package/dist/cjs/inference/entitlement.js +217 -0
  8. package/dist/cjs/inference/errors.js +210 -0
  9. package/dist/cjs/inference/identifiers.js +197 -0
  10. package/dist/cjs/inference/money.js +188 -0
  11. package/dist/cjs/inference/priceVersion.js +110 -0
  12. package/dist/cjs/inference/providerConnection.js +142 -0
  13. package/dist/cjs/inference/request.js +288 -0
  14. package/dist/cjs/inference/routingPolicy.js +213 -0
  15. package/dist/cjs/inference/streamEvents.js +219 -0
  16. package/dist/cjs/inference/usage.js +297 -0
  17. package/dist/cjs/inference/version.js +85 -0
  18. package/dist/esm/.tsbuildinfo +1 -1
  19. package/dist/esm/accountGraph.js +4 -3
  20. package/dist/esm/index.js +54 -0
  21. package/dist/esm/inference/accountBilling.js +331 -0
  22. package/dist/esm/inference/attribution.js +103 -0
  23. package/dist/esm/inference/catalogue.js +479 -0
  24. package/dist/esm/inference/entitlement.js +214 -0
  25. package/dist/esm/inference/errors.js +207 -0
  26. package/dist/esm/inference/identifiers.js +194 -0
  27. package/dist/esm/inference/money.js +185 -0
  28. package/dist/esm/inference/priceVersion.js +107 -0
  29. package/dist/esm/inference/providerConnection.js +139 -0
  30. package/dist/esm/inference/request.js +285 -0
  31. package/dist/esm/inference/routingPolicy.js +210 -0
  32. package/dist/esm/inference/streamEvents.js +216 -0
  33. package/dist/esm/inference/usage.js +294 -0
  34. package/dist/esm/inference/version.js +82 -0
  35. package/dist/types/.tsbuildinfo +1 -1
  36. package/dist/types/accountGraph.d.ts +6 -5
  37. package/dist/types/index.d.ts +27 -0
  38. package/dist/types/inference/accountBilling.d.ts +738 -0
  39. package/dist/types/inference/attribution.d.ts +176 -0
  40. package/dist/types/inference/catalogue.d.ts +1612 -0
  41. package/dist/types/inference/entitlement.d.ts +519 -0
  42. package/dist/types/inference/errors.d.ts +206 -0
  43. package/dist/types/inference/identifiers.d.ts +157 -0
  44. package/dist/types/inference/money.d.ts +185 -0
  45. package/dist/types/inference/priceVersion.d.ts +182 -0
  46. package/dist/types/inference/providerConnection.d.ts +297 -0
  47. package/dist/types/inference/request.d.ts +2364 -0
  48. package/dist/types/inference/routingPolicy.d.ts +426 -0
  49. package/dist/types/inference/streamEvents.d.ts +906 -0
  50. package/dist/types/inference/usage.d.ts +1139 -0
  51. package/dist/types/inference/version.d.ts +82 -0
  52. package/package.json +1 -1
@@ -0,0 +1,288 @@
1
+ "use strict";
2
+ /**
3
+ * The normalized inference request — the canonical internal envelope Oxy's
4
+ * public edge forwards to the data plane.
5
+ *
6
+ * The public surface speaks several dialects (`/v1/responses`,
7
+ * `/v1/chat/completions`, embeddings, images, audio). Exactly one of them is
8
+ * normalized here, at the edge, so that routing, metering, policy enforcement
9
+ * and settlement are written once against one shape instead of once per dialect.
10
+ * `client.apiFormat` records which dialect the customer used, because the
11
+ * response has to be rendered back in it.
12
+ *
13
+ * What the envelope carries that a provider request does not: the resolved
14
+ * attribution block (who pays, which application, which credential, which
15
+ * delegated user), the exact routing policy reference and the customer's
16
+ * idempotency key. Those are the fields that make a request billable and
17
+ * explainable, and they are resolved BEFORE the request enters the data plane.
18
+ *
19
+ * Decided in: docs/adr/0010-public-api-compatibility.md.
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.inferenceRequestSchema = exports.clientRequestMetadataSchema = exports.responseFormatSchema = exports.toolChoiceSchema = exports.toolDefinitionSchema = exports.samplingParametersSchema = exports.inferenceInputSchema = exports.inferenceMessageSchema = exports.inferenceMessageRoleSchema = exports.inferenceToolCallSchema = exports.inferenceContentPartSchema = exports.inferenceContentSourceSchema = void 0;
23
+ const zod_1 = require("zod");
24
+ const attribution_1 = require("./attribution");
25
+ const catalogue_1 = require("./catalogue");
26
+ const identifiers_1 = require("./identifiers");
27
+ const routingPolicy_1 = require("./routingPolicy");
28
+ /* -------------------------------------------------------------------------- */
29
+ /* Input */
30
+ /* -------------------------------------------------------------------------- */
31
+ /**
32
+ * Where binary or remote content comes from.
33
+ *
34
+ * `url` is fetched by the data plane; `inline` carries base64 the customer sent
35
+ * with the request. Both are transient: neither is persisted by default, and
36
+ * neither appears in a receipt, a log line or a telemetry event.
37
+ */
38
+ exports.inferenceContentSourceSchema = zod_1.z.discriminatedUnion('kind', [
39
+ zod_1.z.object({ kind: zod_1.z.literal('url'), url: zod_1.z.string().min(1).max(4096) }).strict(),
40
+ zod_1.z
41
+ .object({
42
+ kind: zod_1.z.literal('inline'),
43
+ mediaType: zod_1.z.string().min(1).max(255),
44
+ data: zod_1.z.string().min(1),
45
+ })
46
+ .strict(),
47
+ ]);
48
+ /** One part of a message's content. A message is always a list of parts. */
49
+ exports.inferenceContentPartSchema = zod_1.z.discriminatedUnion('type', [
50
+ zod_1.z.object({ type: zod_1.z.literal('text'), text: zod_1.z.string() }).strict(),
51
+ zod_1.z
52
+ .object({
53
+ type: zod_1.z.literal('image'),
54
+ source: exports.inferenceContentSourceSchema,
55
+ /** Provider-independent hint; providers that ignore it are unaffected. */
56
+ detail: zod_1.z.enum(['auto', 'low', 'high']).optional(),
57
+ })
58
+ .strict(),
59
+ zod_1.z.object({ type: zod_1.z.literal('audio'), source: exports.inferenceContentSourceSchema }).strict(),
60
+ zod_1.z
61
+ .object({
62
+ type: zod_1.z.literal('file'),
63
+ source: exports.inferenceContentSourceSchema,
64
+ filename: zod_1.z.string().max(255).optional(),
65
+ })
66
+ .strict(),
67
+ ]);
68
+ /**
69
+ * A tool call an assistant made, in the normalized form.
70
+ *
71
+ * `arguments` is the JSON TEXT the model emitted, not a parsed object: models
72
+ * emit invalid JSON often enough that parsing it here would turn a recoverable
73
+ * model mistake into a rejected message.
74
+ */
75
+ exports.inferenceToolCallSchema = zod_1.z
76
+ .object({
77
+ id: zod_1.z.string().min(1).max(128),
78
+ name: zod_1.z.string().min(1).max(128),
79
+ arguments: zod_1.z.string(),
80
+ })
81
+ .strict();
82
+ exports.inferenceMessageRoleSchema = zod_1.z.enum([
83
+ 'system',
84
+ 'developer',
85
+ 'user',
86
+ 'assistant',
87
+ 'tool',
88
+ ]);
89
+ /**
90
+ * One normalized message.
91
+ *
92
+ * The role-specific fields are refined rather than modelled as a discriminated
93
+ * union so the shape stays the one a caller recognises from the OpenAI-style
94
+ * dialects; the refinement is what stops `toolCallId` from riding on a user
95
+ * message, where every provider would silently ignore it.
96
+ */
97
+ exports.inferenceMessageSchema = zod_1.z
98
+ .object({
99
+ role: exports.inferenceMessageRoleSchema,
100
+ content: zod_1.z.array(exports.inferenceContentPartSchema),
101
+ /** Participant name, where the dialect supports naming participants. */
102
+ name: zod_1.z.string().max(128).optional(),
103
+ /** The tool call this message answers. Required on, and only on, `tool`. */
104
+ toolCallId: zod_1.z.string().min(1).max(128).optional(),
105
+ /** Tool calls the assistant made. Only on `assistant`. */
106
+ toolCalls: zod_1.z.array(exports.inferenceToolCallSchema).optional(),
107
+ })
108
+ .strict()
109
+ .superRefine((message, ctx) => {
110
+ if (message.role === 'tool' && message.toolCallId === undefined) {
111
+ ctx.addIssue({
112
+ code: zod_1.z.ZodIssueCode.custom,
113
+ path: ['toolCallId'],
114
+ message: 'a tool message must name the tool call it answers',
115
+ });
116
+ }
117
+ if (message.role !== 'tool' && message.toolCallId !== undefined) {
118
+ ctx.addIssue({
119
+ code: zod_1.z.ZodIssueCode.custom,
120
+ path: ['toolCallId'],
121
+ message: 'only a tool message answers a tool call',
122
+ });
123
+ }
124
+ if (message.role !== 'assistant' && message.toolCalls !== undefined) {
125
+ ctx.addIssue({
126
+ code: zod_1.z.ZodIssueCode.custom,
127
+ path: ['toolCalls'],
128
+ message: 'only an assistant message makes tool calls',
129
+ });
130
+ }
131
+ });
132
+ /**
133
+ * The request's input.
134
+ *
135
+ * Three formats, because the modalities genuinely differ: a chat request is a
136
+ * conversation, an embedding request is a string or a batch of strings, and
137
+ * pretending the latter is a one-message conversation loses the batch boundary
138
+ * that both metering and provider translation depend on.
139
+ */
140
+ exports.inferenceInputSchema = zod_1.z.discriminatedUnion('format', [
141
+ zod_1.z
142
+ .object({
143
+ format: zod_1.z.literal('messages'),
144
+ messages: zod_1.z.array(exports.inferenceMessageSchema).min(1),
145
+ })
146
+ .strict(),
147
+ zod_1.z.object({ format: zod_1.z.literal('text'), text: zod_1.z.string() }).strict(),
148
+ zod_1.z
149
+ .object({
150
+ format: zod_1.z.literal('text_batch'),
151
+ texts: zod_1.z.array(zod_1.z.string()).min(1).max(2048),
152
+ })
153
+ .strict(),
154
+ ]);
155
+ /* -------------------------------------------------------------------------- */
156
+ /* Generation controls */
157
+ /* -------------------------------------------------------------------------- */
158
+ /** Sampling parameters, all optional: absent means the route's own default. */
159
+ exports.samplingParametersSchema = zod_1.z
160
+ .object({
161
+ temperature: zod_1.z.number().min(0).max(2).optional(),
162
+ topP: zod_1.z.number().min(0).max(1).optional(),
163
+ topK: zod_1.z.number().int().positive().safe().optional(),
164
+ frequencyPenalty: zod_1.z.number().min(-2).max(2).optional(),
165
+ presencePenalty: zod_1.z.number().min(-2).max(2).optional(),
166
+ /** A seed makes a request reproducible on providers that honour one. */
167
+ seed: zod_1.z.number().int().safe().optional(),
168
+ stopSequences: zod_1.z.array(zod_1.z.string().min(1).max(256)).max(8).optional(),
169
+ })
170
+ .strict();
171
+ /**
172
+ * A tool the model may call. `parameters` is a JSON Schema document, carried
173
+ * as an opaque object: validating the customer's JSON Schema against a meta
174
+ * schema here would reject documents providers accept.
175
+ */
176
+ exports.toolDefinitionSchema = zod_1.z
177
+ .object({
178
+ type: zod_1.z.literal('function'),
179
+ name: zod_1.z.string().min(1).max(128),
180
+ description: zod_1.z.string().max(2000).optional(),
181
+ parameters: zod_1.z.record(zod_1.z.unknown()),
182
+ /** Ask the provider to enforce the schema, where it supports enforcement. */
183
+ strict: zod_1.z.boolean().optional(),
184
+ })
185
+ .strict();
186
+ /** Whether, and which, tool the model must call. */
187
+ exports.toolChoiceSchema = zod_1.z.union([
188
+ zod_1.z.enum(['auto', 'none', 'required']),
189
+ zod_1.z.object({ type: zod_1.z.literal('function'), name: zod_1.z.string().min(1).max(128) }).strict(),
190
+ ]);
191
+ /** Structured-output request: free text, any JSON object, or a named schema. */
192
+ exports.responseFormatSchema = zod_1.z.discriminatedUnion('type', [
193
+ zod_1.z.object({ type: zod_1.z.literal('text') }).strict(),
194
+ zod_1.z.object({ type: zod_1.z.literal('json_object') }).strict(),
195
+ zod_1.z
196
+ .object({
197
+ type: zod_1.z.literal('json_schema'),
198
+ name: zod_1.z.string().min(1).max(128),
199
+ schema: zod_1.z.record(zod_1.z.unknown()),
200
+ strict: zod_1.z.boolean(),
201
+ })
202
+ .strict(),
203
+ ]);
204
+ /* -------------------------------------------------------------------------- */
205
+ /* Client metadata */
206
+ /* -------------------------------------------------------------------------- */
207
+ /**
208
+ * What the edge records about the CALL, as opposed to its content.
209
+ *
210
+ * `.strict()` is a privacy control, not tidiness. Oxy never persists a user IP
211
+ * — raw, hashed or geo-derived — and this object is the natural place somebody
212
+ * would add one "for security". Strict means a producer that attaches `ip`,
213
+ * `country`, `userAgent` or `forwardedFor` fails the parse instead of quietly
214
+ * shipping it into the data plane and the telemetry stream behind it.
215
+ *
216
+ * `labels` is customer-supplied cost-attribution metadata (a team name, a
217
+ * feature flag). It is echoed on the receipt, so it must never be used for
218
+ * anything the customer would not want to read back to themselves.
219
+ */
220
+ exports.clientRequestMetadataSchema = zod_1.z
221
+ .object({
222
+ /** The public dialect the customer called. The response is rendered in it. */
223
+ apiFormat: zod_1.z.enum([
224
+ 'responses',
225
+ 'chat_completions',
226
+ 'embeddings',
227
+ 'images_generations',
228
+ 'audio_transcriptions',
229
+ 'audio_speech',
230
+ 'rerank',
231
+ 'batches',
232
+ ]),
233
+ /** The public path, e.g. `/v1/responses`. */
234
+ endpoint: zod_1.z.string().min(1).max(256),
235
+ /** The customer's own correlation id, when they sent one. */
236
+ clientRequestId: zod_1.z.string().min(1).max(128).optional(),
237
+ receivedAt: identifiers_1.inferenceTimestampSchema,
238
+ labels: zod_1.z.record(zod_1.z.string().max(256)).optional(),
239
+ })
240
+ .strict();
241
+ /* -------------------------------------------------------------------------- */
242
+ /* The envelope */
243
+ /* -------------------------------------------------------------------------- */
244
+ /**
245
+ * The canonical internal request Oxy forwards to the data plane.
246
+ *
247
+ * `target` distinguishes the two questions a caller can ask — "serve THIS
248
+ * model" versus "choose one for me" — structurally. Everything downstream that
249
+ * must not silently substitute a model reads that discriminant rather than
250
+ * inferring intent from a string.
251
+ */
252
+ exports.inferenceRequestSchema = zod_1.z
253
+ .object({
254
+ /** See `version.ts`: this is the Oxy→data-plane request envelope. */
255
+ schemaVersion: zod_1.z.literal(1),
256
+ attribution: attribution_1.inferenceAttributionSchema,
257
+ target: routingPolicy_1.routingTargetSchema,
258
+ modality: catalogue_1.inferenceModalitySchema,
259
+ input: exports.inferenceInputSchema,
260
+ stream: zod_1.z.boolean(),
261
+ maxOutputTokens: zod_1.z.number().int().positive().safe().optional(),
262
+ sampling: exports.samplingParametersSchema,
263
+ tools: zod_1.z.array(exports.toolDefinitionSchema).default([]),
264
+ toolChoice: exports.toolChoiceSchema.optional(),
265
+ responseFormat: exports.responseFormatSchema.optional(),
266
+ client: exports.clientRequestMetadataSchema,
267
+ /** Present when the operation is safe to deduplicate on retry. */
268
+ idempotencyKey: identifiers_1.idempotencyKeySchema.optional(),
269
+ /** The exact policy revision this request is served under. */
270
+ routingPolicy: routingPolicy_1.routingPolicyReferenceSchema,
271
+ })
272
+ .superRefine((request, ctx) => {
273
+ if (request.toolChoice !== undefined && request.tools.length === 0) {
274
+ ctx.addIssue({
275
+ code: zod_1.z.ZodIssueCode.custom,
276
+ path: ['toolChoice'],
277
+ message: 'a tool choice requires at least one tool definition',
278
+ });
279
+ }
280
+ const toolNames = request.tools.map((tool) => tool.name);
281
+ if (new Set(toolNames).size !== toolNames.length) {
282
+ ctx.addIssue({
283
+ code: zod_1.z.ZodIssueCode.custom,
284
+ path: ['tools'],
285
+ message: 'tool names must be unique within one request',
286
+ });
287
+ }
288
+ });
@@ -0,0 +1,213 @@
1
+ "use strict";
2
+ /**
3
+ * Routing policy — the customer-facing routing configuration.
4
+ *
5
+ * Stored under an Oxy account or application (the control plane owns it),
6
+ * executed by the data plane, which owns execution. Every request records the
7
+ * exact `{routingPolicyId, policyVersion}` it was served under, so a route
8
+ * decision months old can be explained against the policy that was in force,
9
+ * not against the policy that exists now.
10
+ *
11
+ * Two rules shape the fallback controls:
12
+ *
13
+ * - **Same-model deployment failover is not cross-model fallback.** Moving
14
+ * between two deployments of the SAME revision is an availability decision
15
+ * and is on by default; serving a DIFFERENT model is a substitution the
16
+ * customer must have authorized by name.
17
+ * - **A request for a concrete model is never silently replaced.** Cross-model
18
+ * fallback is an explicit list of references, and a switch that uses it emits
19
+ * a customer-visible route-switch event.
20
+ *
21
+ * The controls are flat and independent, matching what Console renders — which
22
+ * means contradictory combinations are EXPRESSIBLE and must therefore be
23
+ * REJECTED, rather than being quietly resolved by whichever field the executor
24
+ * happens to read first. That rejection is `routingPolicySchema`'s refinement.
25
+ *
26
+ * Decided in: docs/adr/0008-catalogue-concept-separation.md, issue #972 workstream 6.
27
+ */
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.routingPolicyReferenceSchema = exports.routingPolicySchema = exports.routingFallbackPolicySchema = exports.routingPolicyScopeSchema = exports.routingTargetSchema = void 0;
30
+ const zod_1 = require("zod");
31
+ const identifiers_1 = require("./identifiers");
32
+ const money_1 = require("./money");
33
+ /**
34
+ * What a policy resolves to when a caller names no model.
35
+ *
36
+ * A discriminated union rather than two optional fields, so "which one did the
37
+ * customer configure" is never a question about which field is non-null.
38
+ */
39
+ exports.routingTargetSchema = zod_1.z.discriminatedUnion('kind', [
40
+ zod_1.z
41
+ .object({
42
+ kind: zod_1.z.literal('model'),
43
+ modelReference: identifiers_1.modelReferenceSchema,
44
+ })
45
+ .strict(),
46
+ zod_1.z
47
+ .object({
48
+ kind: zod_1.z.literal('routing_profile'),
49
+ routingProfile: identifiers_1.routingProfileSlugSchema,
50
+ })
51
+ .strict(),
52
+ ]);
53
+ /**
54
+ * Which account or application a policy governs.
55
+ *
56
+ * Application-scoped policies are the common case; an account-scoped policy is
57
+ * the floor its applications inherit, and inheritance is resolved by the control
58
+ * plane before a policy reaches the data plane.
59
+ */
60
+ exports.routingPolicyScopeSchema = zod_1.z.discriminatedUnion('kind', [
61
+ zod_1.z.object({ kind: zod_1.z.literal('account'), accountId: identifiers_1.oxyAccountIdSchema }).strict(),
62
+ zod_1.z
63
+ .object({
64
+ kind: zod_1.z.literal('application'),
65
+ accountId: identifiers_1.oxyAccountIdSchema,
66
+ applicationId: identifiers_1.oxyApplicationIdSchema,
67
+ })
68
+ .strict(),
69
+ ]);
70
+ /**
71
+ * The fallback controls, kept together so a reviewer sees all three at once.
72
+ *
73
+ * `authorizedCrossModel` is a list of model references the customer has
74
+ * explicitly permitted as substitutes — never a boolean, because "allow
75
+ * fallback" without naming the destination is exactly the silent substitution
76
+ * the invariant forbids.
77
+ */
78
+ exports.routingFallbackPolicySchema = zod_1.z
79
+ .object({
80
+ disabled: zod_1.z.boolean(),
81
+ sameModelDeployment: zod_1.z.boolean(),
82
+ authorizedCrossModel: zod_1.z.array(identifiers_1.modelReferenceSchema).default([]),
83
+ })
84
+ .strict();
85
+ /**
86
+ * A versioned routing policy.
87
+ *
88
+ * `policyVersion` is the CUSTOMER's revision of their own configuration and is
89
+ * unrelated to `schemaVersion`, which is the version of this wire shape. They
90
+ * are two different clocks: a customer edits their policy without any contract
91
+ * change, and a contract change does not renumber anybody's policy.
92
+ */
93
+ exports.routingPolicySchema = zod_1.z
94
+ .object({
95
+ /** See `version.ts`: exchanged with the data plane on its own. */
96
+ schemaVersion: zod_1.z.literal(1),
97
+ routingPolicyId: zod_1.z.string().min(1).max(128),
98
+ policyVersion: zod_1.z.number().int().positive().safe(),
99
+ scope: exports.routingPolicyScopeSchema,
100
+ /** Absent when every request must name its own model. */
101
+ defaultTarget: exports.routingTargetSchema.optional(),
102
+ /** Empty means "no allowlist" — every provider qualifies unless denied. */
103
+ providerAllowlist: zod_1.z.array(identifiers_1.inferenceProviderSlugSchema).default([]),
104
+ providerDenylist: zod_1.z.array(identifiers_1.inferenceProviderSlugSchema).default([]),
105
+ /** Empty means "no residency constraint". */
106
+ allowedRegions: zod_1.z.array(identifiers_1.inferenceRegionSchema).default([]),
107
+ deniedRegions: zod_1.z.array(identifiers_1.inferenceRegionSchema).default([]),
108
+ requireZeroDataRetention: zod_1.z.boolean(),
109
+ prohibitTrainingOnCustomerData: zod_1.z.boolean(),
110
+ /** Ceilings on what a route may cost the customer, quoted like catalogue prices. */
111
+ maxPricePerUnit: zod_1.z.array(money_1.unitPriceSchema).default([]),
112
+ maxPricePerRequest: zod_1.z
113
+ .object({ amount: money_1.exactDecimalSchema, currency: money_1.currencyCodeSchema })
114
+ .strict()
115
+ .optional(),
116
+ /** What to optimise for among the routes that qualify. */
117
+ optimiseFor: zod_1.z.enum(['price', 'latency', 'throughput', 'balanced']),
118
+ /** Serve only from Oxy's own hosting of open-weight models. */
119
+ oxyHostedOnly: zod_1.z.boolean(),
120
+ /** License / usage-right constraints. Empty license list means unconstrained. */
121
+ allowedLicenseIds: zod_1.z.array(zod_1.z.string().min(1).max(128)).default([]),
122
+ requireCommercialUseRights: zod_1.z.boolean(),
123
+ fallback: exports.routingFallbackPolicySchema,
124
+ /** Whether the customer's own provider credentials may or must be used. */
125
+ byokPreference: zod_1.z.enum(['disabled', 'prefer', 'require']),
126
+ /** Enterprise reserved capacity rather than shared endpoints. */
127
+ dedicatedCapacity: zod_1.z.enum(['disabled', 'prefer', 'require']),
128
+ updatedAt: identifiers_1.inferenceTimestampSchema,
129
+ })
130
+ .superRefine((policy, ctx) => {
131
+ // "Requires a denied provider": the allowlist is the requirement, so a
132
+ // provider named in both lists is a policy that can never resolve. This is
133
+ // also the shape an Oxy-hosted-only policy takes when it pins a provider it
134
+ // has itself denied — whether a provider is Oxy-hosted is a property of the
135
+ // catalogue entry, not of its slug, so the data plane resolves it, not this
136
+ // schema.
137
+ const denied = new Set(policy.providerDenylist);
138
+ for (const [index, provider] of policy.providerAllowlist.entries()) {
139
+ if (denied.has(provider)) {
140
+ ctx.addIssue({
141
+ code: zod_1.z.ZodIssueCode.custom,
142
+ path: ['providerAllowlist', index],
143
+ message: `provider ${provider} is both required by the allowlist and denied`,
144
+ });
145
+ }
146
+ }
147
+ const deniedRegions = new Set(policy.deniedRegions);
148
+ for (const [index, region] of policy.allowedRegions.entries()) {
149
+ if (deniedRegions.has(region)) {
150
+ ctx.addIssue({
151
+ code: zod_1.z.ZodIssueCode.custom,
152
+ path: ['allowedRegions', index],
153
+ message: `region ${region} is both allowed and denied`,
154
+ });
155
+ }
156
+ }
157
+ // Fallback disabled is an instruction to fail the request rather than serve
158
+ // it elsewhere. Combined with a fallback route it is not a strict policy but
159
+ // an ambiguous one, and the ambiguity resolves differently in each executor.
160
+ if (policy.fallback.disabled && policy.fallback.sameModelDeployment) {
161
+ ctx.addIssue({
162
+ code: zod_1.z.ZodIssueCode.custom,
163
+ path: ['fallback', 'sameModelDeployment'],
164
+ message: 'fallback is disabled, so same-model deployment failover cannot be enabled',
165
+ });
166
+ }
167
+ if (policy.fallback.disabled && policy.fallback.authorizedCrossModel.length > 0) {
168
+ ctx.addIssue({
169
+ code: zod_1.z.ZodIssueCode.custom,
170
+ path: ['fallback', 'authorizedCrossModel'],
171
+ message: 'fallback is disabled, so no cross-model fallback may be authorized',
172
+ });
173
+ }
174
+ // BYOK routes run on the customer's own upstream provider account, which is
175
+ // by definition not Oxy's hosting.
176
+ if (policy.oxyHostedOnly && policy.byokPreference === 'require') {
177
+ ctx.addIssue({
178
+ code: zod_1.z.ZodIssueCode.custom,
179
+ path: ['byokPreference'],
180
+ message: 'an Oxy-hosted-only policy cannot also require a customer provider credential',
181
+ });
182
+ }
183
+ const ceilingUnits = policy.maxPricePerUnit.map((ceiling) => ceiling.unit);
184
+ if (new Set(ceilingUnits).size !== ceilingUnits.length) {
185
+ ctx.addIssue({
186
+ code: zod_1.z.ZodIssueCode.custom,
187
+ path: ['maxPricePerUnit'],
188
+ message: 'a unit may carry only one price ceiling',
189
+ });
190
+ }
191
+ if (policy.maxPricePerRequest !== undefined) {
192
+ for (const [index, ceiling] of policy.maxPricePerUnit.entries()) {
193
+ if (ceiling.currency !== policy.maxPricePerRequest.currency) {
194
+ ctx.addIssue({
195
+ code: zod_1.z.ZodIssueCode.custom,
196
+ path: ['maxPricePerUnit', index, 'currency'],
197
+ message: 'every price ceiling in one policy must use the same currency',
198
+ });
199
+ }
200
+ }
201
+ }
202
+ });
203
+ /**
204
+ * The reference a request records: which policy, at which of the customer's own
205
+ * revisions. Embedded in the request envelope and in the settled receipt, so a
206
+ * charge can be explained against the exact configuration that produced it.
207
+ */
208
+ exports.routingPolicyReferenceSchema = zod_1.z
209
+ .object({
210
+ routingPolicyId: zod_1.z.string().min(1).max(128),
211
+ policyVersion: zod_1.z.number().int().positive().safe(),
212
+ })
213
+ .strict();