@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
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Inference errors and retryability.
3
+ *
4
+ * One closed set of codes, shared by the Oxy public edge, the data plane and
5
+ * every SDK.
6
+ * Closed because the alternative — a free-form `code` string — makes a client's
7
+ * error handling a guess about the producer's spelling, and makes "is this
8
+ * worth retrying" a decision every consumer re-derives from prose.
9
+ *
10
+ * Retryability is carried explicitly and is CONSTRAINED by the code: a code
11
+ * that can never succeed on a bare retry (an invalid request, a denied
12
+ * permission, an insufficient balance) cannot claim `retryable: true`. Without
13
+ * that constraint the field is advisory, and one producer setting it optimistically
14
+ * turns every client into a retry storm against a request that will never pass.
15
+ *
16
+ * The provider passthrough exists so a customer can see what the upstream said
17
+ * without Oxy having to interpret every provider's error vocabulary — but it is
18
+ * the single most likely place for an upstream credential to escape, because
19
+ * provider errors routinely echo the request that caused them. It is therefore
20
+ * a `.strict()` object of four fields with no room for headers or a request
21
+ * body, and its free text is refused if it looks like it contains a credential.
22
+ *
23
+ * Decided in: docs/adr/0010-public-api-compatibility.md.
24
+ */
25
+ import { z } from 'zod';
26
+ import { inferenceProviderSlugSchema, requestIdSchema } from './identifiers.js';
27
+ /**
28
+ * The closed set of inference error codes.
29
+ *
30
+ * Grouped by who must act: the caller (`invalid_request` … `idempotency_conflict`),
31
+ * the account owner (`insufficient_balance`, `spending_limit_exceeded`,
32
+ * `quota_exceeded`, `byok_credential_invalid`), routing/permission policy
33
+ * (`policy_violation`, `commercial_permission_denied`, `no_route_available`),
34
+ * and the platform or its upstreams (everything from `deployment_unavailable`).
35
+ */
36
+ export const INFERENCE_ERROR_CODES = [
37
+ 'invalid_request',
38
+ 'authentication_failed',
39
+ 'permission_denied',
40
+ 'insufficient_scope',
41
+ 'model_not_found',
42
+ 'unsupported_modality',
43
+ 'context_length_exceeded',
44
+ 'request_too_large',
45
+ 'output_limit_exceeded',
46
+ 'idempotency_conflict',
47
+ 'insufficient_balance',
48
+ 'spending_limit_exceeded',
49
+ 'quota_exceeded',
50
+ 'byok_credential_invalid',
51
+ 'policy_violation',
52
+ 'commercial_permission_denied',
53
+ 'no_route_available',
54
+ 'upstream_content_filtered',
55
+ 'cancelled',
56
+ 'rate_limited',
57
+ 'deployment_unavailable',
58
+ 'provider_error',
59
+ 'provider_timeout',
60
+ 'provider_overloaded',
61
+ 'service_unavailable',
62
+ 'internal_error',
63
+ ];
64
+ export const inferenceErrorCodeSchema = z.enum(INFERENCE_ERROR_CODES);
65
+ /**
66
+ * Codes for which an identical retried request cannot succeed.
67
+ *
68
+ * `rate_limited` and `quota_exceeded` sit on opposite sides of this line
69
+ * deliberately: a rate limit clears on its own within the window the response
70
+ * names, while a quota is an account-level ceiling that only a human raises.
71
+ * `cancelled` is here because the caller already withdrew the request; a client
72
+ * that retries it is contradicting its own cancellation.
73
+ */
74
+ export const NON_RETRYABLE_INFERENCE_ERROR_CODES = [
75
+ 'invalid_request',
76
+ 'authentication_failed',
77
+ 'permission_denied',
78
+ 'insufficient_scope',
79
+ 'model_not_found',
80
+ 'unsupported_modality',
81
+ 'context_length_exceeded',
82
+ 'request_too_large',
83
+ 'output_limit_exceeded',
84
+ 'idempotency_conflict',
85
+ 'insufficient_balance',
86
+ 'spending_limit_exceeded',
87
+ 'quota_exceeded',
88
+ 'byok_credential_invalid',
89
+ 'policy_violation',
90
+ 'commercial_permission_denied',
91
+ 'no_route_available',
92
+ 'upstream_content_filtered',
93
+ 'cancelled',
94
+ ];
95
+ const NON_RETRYABLE_CODE_SET = new Set(NON_RETRYABLE_INFERENCE_ERROR_CODES);
96
+ /**
97
+ * Text that looks like it carries a credential.
98
+ *
99
+ * A deliberately narrow set of literal markers — the shapes upstream providers
100
+ * actually echo — rather than an entropy heuristic, which would reject
101
+ * legitimate error text (a request id, a base64 image fragment) and teach
102
+ * producers to strip messages until they pass.
103
+ */
104
+ const CREDENTIAL_LIKE_TEXT = /(?:bearer\s+[a-z0-9._~+/=-]{8,}|authorization\s*[:=]|api[_-]?key\s*[:=]|\bsk-[a-z0-9_-]{8,}|\bsk_(?:live|test)_[a-z0-9]{8,})/i;
105
+ /**
106
+ * Free text that is safe to hand a customer: bounded, and refused outright if a
107
+ * credential marker appears in it. Applied to BOTH the Oxy message and the
108
+ * upstream one — a leak is no less a leak for having been written by a provider.
109
+ */
110
+ export const safeErrorTextSchema = z
111
+ .string()
112
+ .min(1)
113
+ .max(2000)
114
+ .refine((value) => !CREDENTIAL_LIKE_TEXT.test(value), 'error text must not contain credential-shaped material');
115
+ /**
116
+ * A coarse classification of an upstream failure (ADR 0010's `upstreamCategory`).
117
+ *
118
+ * Distinct from {@link providerErrorPassthroughSchema}, which carries the
119
+ * upstream's OWN code and text: this is Oxy's reading of what kind of failure it
120
+ * was, in a vocabulary that is the same across every provider, so a client can
121
+ * branch on it without knowing who served the request.
122
+ */
123
+ export const upstreamErrorCategorySchema = z.enum([
124
+ 'rate_limit',
125
+ 'quota',
126
+ 'timeout',
127
+ 'overloaded',
128
+ 'server_error',
129
+ 'content_filter',
130
+ 'invalid_request',
131
+ 'authentication',
132
+ 'unknown',
133
+ ]);
134
+ /**
135
+ * What the upstream provider said, reduced to the four fields a customer can
136
+ * act on.
137
+ *
138
+ * `.strict()` is the security control here, not a tidiness preference: it means
139
+ * a producer cannot widen this by attaching `requestHeaders`, `curl`, `body` or
140
+ * `raw` and have it silently pass. Adding a field is a contract change with a
141
+ * version bump and a review, which is the point.
142
+ */
143
+ export const providerErrorPassthroughSchema = z
144
+ .object({
145
+ provider: inferenceProviderSlugSchema,
146
+ /** The upstream HTTP status, when the upstream spoke HTTP. */
147
+ status: z.number().int().min(100).max(599).optional(),
148
+ /** The upstream's own error code, verbatim and uninterpreted. */
149
+ code: z.string().max(128).optional(),
150
+ /** The upstream's message, subject to the same credential refusal. */
151
+ message: safeErrorTextSchema.optional(),
152
+ })
153
+ .strict();
154
+ /**
155
+ * The error body every inference surface returns and every stream error event
156
+ * carries.
157
+ *
158
+ * `requestId` is always present — an error a customer cannot correlate with a
159
+ * log line is an error they have to reproduce to report.
160
+ */
161
+ export const inferenceErrorSchema = z
162
+ .object({
163
+ /** See `version.ts`: this shape appears alone on the wire, so it is versioned. */
164
+ schemaVersion: z.literal(1),
165
+ code: inferenceErrorCodeSchema,
166
+ message: safeErrorTextSchema,
167
+ retryable: z.boolean(),
168
+ requestId: requestIdSchema,
169
+ /** How long to wait before retrying. Only meaningful when `retryable`. */
170
+ retryAfterMs: z.number().int().nonnegative().safe().optional(),
171
+ /** The request field at fault, for `invalid_request`. */
172
+ param: z.string().max(128).optional(),
173
+ /** Present only when an upstream provider was reached and failed. */
174
+ upstreamCategory: upstreamErrorCategorySchema.optional(),
175
+ providerError: providerErrorPassthroughSchema.optional(),
176
+ })
177
+ .superRefine((error, ctx) => {
178
+ if (error.retryable && NON_RETRYABLE_CODE_SET.has(error.code)) {
179
+ ctx.addIssue({
180
+ code: z.ZodIssueCode.custom,
181
+ path: ['retryable'],
182
+ message: `${error.code} can never succeed on an identical retry, so it cannot be retryable`,
183
+ });
184
+ }
185
+ if (error.retryAfterMs !== undefined && !error.retryable) {
186
+ ctx.addIssue({
187
+ code: z.ZodIssueCode.custom,
188
+ path: ['retryAfterMs'],
189
+ message: 'retryAfterMs tells a client when to retry, so it requires retryable: true',
190
+ });
191
+ }
192
+ });
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Identifiers, references and wire primitives shared by every Oxy↔data-plane
3
+ * inference contract.
4
+ *
5
+ * Two kinds of identifier live here, and the difference matters:
6
+ *
7
+ * - **Principal identifiers** owned by Oxy (`accountId`, `applicationId`,
8
+ * `credentialId`, the optional delegated `userId`). The data plane may store
9
+ * them as immutable references; it never owns, mints or mutates them.
10
+ * - **Catalogue references** (`<publisher>/<model>`, `<publisher>/<model>@<revision>`,
11
+ * a routing-profile slug). These are the strings a customer types, so their
12
+ * grammar is part of the public contract, not an implementation detail.
13
+ *
14
+ * Platform-agnostic — zod only. Every regex here is plain ASCII: this package
15
+ * is imported by React Native apps running on Hermes, which rejects Unicode
16
+ * property escapes (`\p{…}`) at runtime.
17
+ *
18
+ * Decided in: docs/adr/0007-canonical-request-attribution.md, docs/adr/0008-catalogue-concept-separation.md.
19
+ */
20
+ import { z } from 'zod';
21
+ /* -------------------------------------------------------------------------- */
22
+ /* Principal identifiers */
23
+ /* -------------------------------------------------------------------------- */
24
+ /**
25
+ * An Oxy account id — the account that owns the workload and is financially
26
+ * responsible for it.
27
+ *
28
+ * Branded, and that brand is load-bearing rather than decorative: it is the
29
+ * type-level half of the rule that a delegated end-user identity can never
30
+ * become the billing identity (see {@link delegatedUserIdSchema} and
31
+ * `billingPrincipalSchema`). A plain `string` — and therefore any id read out
32
+ * of a header, a JWT claim or a request body — is not assignable to it; the
33
+ * only way to obtain one is to parse a value through this schema.
34
+ */
35
+ export const oxyAccountIdSchema = z.string().min(1).max(64).brand();
36
+ /**
37
+ * A delegated end-user identity (Alia's `X-Oxy-User-Id`), branded with a
38
+ * DIFFERENT brand from {@link oxyAccountIdSchema} so the two cannot be
39
+ * substituted for one another in either direction, in any consumer, without a
40
+ * cast that review would catch.
41
+ *
42
+ * It exists for attribution and product-side personalisation only. It is never
43
+ * a billing principal, never an access-control principal, and its presence
44
+ * never changes which account is charged.
45
+ */
46
+ export const delegatedUserIdSchema = z.string().min(1).max(64).brand();
47
+ /**
48
+ * An Oxy `Application._id`. Not branded: no invariant in this contract turns on
49
+ * confusing it with another id, and brand inflation costs every producer a
50
+ * parse call for no safety. The two ids above are branded because ADR 0007's
51
+ * rule is exactly that they must not be interchangeable.
52
+ */
53
+ export const oxyApplicationIdSchema = z.string().min(1).max(64);
54
+ /** An Oxy `ApplicationCredential._id` — the credential used for this request. */
55
+ export const oxyCredentialIdSchema = z.string().min(1).max(64);
56
+ /**
57
+ * A request id generated by the data plane. Correlates the Oxy edge, the data
58
+ * plane, the financial ledger and the customer-visible receipt, so it appears on
59
+ * every stream event and every ledger record.
60
+ */
61
+ export const requestIdSchema = z.string().min(1).max(128);
62
+ /**
63
+ * A generation id generated by the data plane, present when a request produced
64
+ * a generation that can be looked up later (`GET /v1/generations/:id`).
65
+ */
66
+ export const generationIdSchema = z.string().min(1).max(128);
67
+ /**
68
+ * A caller-supplied idempotency key. Every reserve/settle/refund call is keyed
69
+ * on one so a retry, a redelivered event or a duplicated webhook can never
70
+ * charge twice.
71
+ */
72
+ export const idempotencyKeySchema = z.string().min(1).max(255);
73
+ /** Credential environments. A credential is issued into exactly one of them. */
74
+ export const inferenceEnvironmentSchema = z.enum(['development', 'staging', 'production']);
75
+ /* -------------------------------------------------------------------------- */
76
+ /* Wire primitives */
77
+ /* -------------------------------------------------------------------------- */
78
+ /**
79
+ * An instant, as an ISO 8601 string in UTC (`2026-08-15T09:41:00.000Z`).
80
+ *
81
+ * Validated rather than left free-form — unlike the session contracts, which
82
+ * carry legacy expiry strings no consumer interprets, every instant here is
83
+ * read: a reservation expires, a price version starts applying, a receipt is
84
+ * settled. One canonical spelling (UTC, `Z`) so two records that describe the
85
+ * same moment compare and sort as equal, which a mix of offsets would not.
86
+ */
87
+ export const inferenceTimestampSchema = z.string().datetime();
88
+ /**
89
+ * A calendar DATE with no instant attached (`2026-05-01`) — a knowledge cutoff
90
+ * or a release date, which are published as days and become wrong when a
91
+ * timezone is invented for them.
92
+ */
93
+ export const inferenceDateSchema = z
94
+ .string()
95
+ .regex(/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/, 'must be an ISO 8601 calendar date (YYYY-MM-DD)');
96
+ /** An absolute https URL, for model cards, licenses and provider documentation. */
97
+ export const inferenceHttpsUrlSchema = z
98
+ .string()
99
+ .max(2048)
100
+ .regex(/^https:\/\/[^\s]+$/, 'must be an absolute https URL');
101
+ /* -------------------------------------------------------------------------- */
102
+ /* Catalogue references */
103
+ /* -------------------------------------------------------------------------- */
104
+ /** One path segment of a canonical model id: a lowercase, URL-safe slug. */
105
+ const SLUG_PATTERN = '[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?';
106
+ /** A revision label. Case-preserving, because upstream revisions often are. */
107
+ const REVISION_PATTERN = '[a-zA-Z0-9](?:[a-zA-Z0-9._-]*[a-zA-Z0-9])?';
108
+ /** A publisher slug, e.g. `openai`, `anthropic`, `meta`, `alia`. */
109
+ export const publisherSlugSchema = z
110
+ .string()
111
+ .max(64)
112
+ .regex(new RegExp(`^${SLUG_PATTERN}$`), 'publisher must be a lowercase URL-safe slug');
113
+ /** A model slug within its publisher's namespace, e.g. `gpt-5`, `llama-3.1-70b`. */
114
+ export const modelSlugSchema = z
115
+ .string()
116
+ .max(64)
117
+ .regex(new RegExp(`^${SLUG_PATTERN}$`), 'model must be a lowercase URL-safe slug');
118
+ /**
119
+ * A canonical model id, `<publisher>/<model>`. This names a MODEL — a
120
+ * long-lived product identity whose behaviour changes as revisions ship. It
121
+ * does not name a revision, a deployment or a provider.
122
+ */
123
+ export const modelIdSchema = z
124
+ .string()
125
+ .max(129)
126
+ .regex(new RegExp(`^${SLUG_PATTERN}/${SLUG_PATTERN}$`), 'model id must be <publisher>/<model>');
127
+ /** An immutable revision label, unique within its model, e.g. `2026-05-01`. */
128
+ export const modelRevisionLabelSchema = z
129
+ .string()
130
+ .max(64)
131
+ .regex(new RegExp(`^${REVISION_PATTERN}$`), 'revision must be a URL-safe label');
132
+ /**
133
+ * A model reference as a customer writes it: `<publisher>/<model>` (the model's
134
+ * current revision, chosen by Oxy) or `<publisher>/<model>@<revision>` (an
135
+ * immutable revision the customer pinned).
136
+ *
137
+ * Both forms name a CONCRETE MODEL. Neither can name a routing profile — see
138
+ * {@link routingProfileSlugSchema} — which is what makes "a request for a
139
+ * concrete model is never silently replaced with a different model" a
140
+ * distinction the type system can carry rather than a convention.
141
+ */
142
+ export const modelReferenceSchema = z
143
+ .string()
144
+ .max(194)
145
+ .regex(new RegExp(`^${SLUG_PATTERN}/${SLUG_PATTERN}(?:@${REVISION_PATTERN})?$`), 'model reference must be <publisher>/<model> or <publisher>/<model>@<revision>');
146
+ /**
147
+ * A routing-profile slug, e.g. `auto`, `fast`, `quality`.
148
+ *
149
+ * Deliberately refuses a `/`, so a profile can never be written in the shape of
150
+ * a model id and no caller can be confused about whether they asked for a
151
+ * concrete model or for Oxy to choose one. Modes like `auto`/`fast`/`quality`
152
+ * are profiles or product presets; they are never model objects.
153
+ */
154
+ export const routingProfileSlugSchema = z
155
+ .string()
156
+ .max(64)
157
+ .regex(new RegExp(`^${SLUG_PATTERN}$`), 'routing profile must be a lowercase URL-safe slug');
158
+ /** An inference provider slug, e.g. `openai`, `bedrock`, `oxy-hosted`. */
159
+ export const inferenceProviderSlugSchema = z
160
+ .string()
161
+ .max(64)
162
+ .regex(new RegExp(`^${SLUG_PATTERN}$`), 'provider must be a lowercase URL-safe slug');
163
+ /**
164
+ * A deployment/endpoint id. Opaque to customers: which concrete endpoint served
165
+ * a request is the data plane's operational detail, and only the customer-safe
166
+ * subset of it is ever attributed back (see the catalogue's serving-boundary
167
+ * rules).
168
+ */
169
+ export const deploymentIdSchema = z.string().min(1).max(128);
170
+ /**
171
+ * A region identifier, e.g. `us-west-2`, `eu-central-1`. Free-form rather than
172
+ * a closed enum because the set is provider-defined and grows without any
173
+ * contract change; residency policies match on exact strings.
174
+ */
175
+ export const inferenceRegionSchema = z
176
+ .string()
177
+ .max(64)
178
+ .regex(new RegExp(`^${SLUG_PATTERN}$`), 'region must be a lowercase URL-safe slug');
179
+ /**
180
+ * The publisher namespace reserved for models Alia actually owns or derives.
181
+ *
182
+ * `alia/*` is never a re-badged third-party route and never a prompt preset —
183
+ * enforcing that is the job of `modelSchema`'s provenance refinement, which
184
+ * this constant exists to be checked against.
185
+ */
186
+ export const RESERVED_ALIA_PUBLISHER = 'alia';
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Money and usage units for the inference contracts.
3
+ *
4
+ * The non-negotiable invariant this file exists to make structural: **customer
5
+ * charges never use floating-point values as the financial source of truth.**
6
+ * A JS `number` cannot represent `0.1 + 0.2` exactly, and an inference ledger
7
+ * adds millions of small amounts, so a float total is wrong by construction
8
+ * rather than by accident.
9
+ *
10
+ * Every amount and every price is one representation: {@link exactDecimalSchema},
11
+ * an exact decimal STRING at the scale ADR 0009 declares. Amounts are not
12
+ * integer minor units, and that is the ADR's decision rather than an oversight:
13
+ * one token costs several orders of magnitude less than one cent, so rounding
14
+ * per request would make a customer's bill depend on how their client chunked
15
+ * its work. Rounding happens ONCE, at the invoice boundary, and is itself a
16
+ * ledger entry.
17
+ *
18
+ * A string is also what the driver hands back — `postgres.js` decodes `NUMERIC`
19
+ * as a string — so keeping it a string on the wire means accidental JS
20
+ * arithmetic fails loudly instead of silently losing precision. Money
21
+ * arithmetic happens in SQL or in a decimal type, never in a JS `number`.
22
+ *
23
+ * Units are carried separately from money in every shape: a receipt says both
24
+ * "204 output tokens" and "0.003060000000 USD", and neither is derived from the
25
+ * other at read time. That separation is what lets a price version change
26
+ * without rewriting settled history.
27
+ *
28
+ * Decided in: docs/adr/0009-usage-reservation-and-settlement.md.
29
+ */
30
+ import { z } from 'zod';
31
+ /* -------------------------------------------------------------------------- */
32
+ /* Money */
33
+ /* -------------------------------------------------------------------------- */
34
+ /** ISO 4217 alpha-3 currency code, e.g. `USD`. */
35
+ export const currencyCodeSchema = z
36
+ .string()
37
+ .regex(/^[A-Z]{3}$/, 'currency must be an ISO 4217 alpha-3 code');
38
+ /**
39
+ * The declared fractional scale of every amount and price in this contract, and
40
+ * of the `NUMERIC` columns the ledger stores them in.
41
+ *
42
+ * Twelve digits is sub-minor-unit precision by a wide margin: at $3 per million
43
+ * input tokens, one token costs `0.000003000000`, which this scale represents
44
+ * exactly. Amounts are compared and summed NUMERICALLY, never as text — `3.0`
45
+ * and `3.000000000000` are one amount written two ways.
46
+ */
47
+ export const INFERENCE_MONEY_SCALE = 12;
48
+ /**
49
+ * An exact non-negative decimal, carried as a STRING so no parse step can turn
50
+ * it into a float on the way past. Up to 18 integer digits and
51
+ * {@link INFERENCE_MONEY_SCALE} fractional digits.
52
+ *
53
+ * Non-negative: direction is carried by the SHAPE — a receipt debits, a refund
54
+ * credits — so a stray sign can never silently invert an entry.
55
+ *
56
+ * No exponent form: `1e-6` and `0.000001` are the same number, but only one of
57
+ * them survives a naive string comparison, a cache key or a log grep intact.
58
+ *
59
+ * Branded, so a bare `string` is not assignable and an amount cannot arrive
60
+ * from string concatenation that was never checked. Producers construct one
61
+ * with `exactDecimalSchema.parse(value)`.
62
+ */
63
+ export const exactDecimalSchema = z
64
+ .string()
65
+ .regex(/^(?:0|[1-9][0-9]{0,17})(?:\.[0-9]{1,12})?$/, 'must be an exact non-negative decimal string without an exponent')
66
+ .brand();
67
+ /**
68
+ * An amount of money: the exact decimal plus the currency it is in.
69
+ *
70
+ * `.strict()` so a payload carrying a convenience float beside the exact value
71
+ * (`{ amount: '18.06', amountFloat: 18.06 }`) is REJECTED rather than stripped.
72
+ * A stripped float is the more dangerous outcome: it disappears silently here
73
+ * and survives in the producer, where it is the value somebody eventually
74
+ * displays.
75
+ */
76
+ export const moneySchema = z
77
+ .object({
78
+ amount: exactDecimalSchema,
79
+ currency: currencyCodeSchema,
80
+ })
81
+ .strict();
82
+ /* -------------------------------------------------------------------------- */
83
+ /* Usage units */
84
+ /* -------------------------------------------------------------------------- */
85
+ /**
86
+ * The closed set of units inference is metered in.
87
+ *
88
+ * Time is carried in integer MILLISECONDS rather than seconds so that no unit
89
+ * quantity is ever fractional: a 12.5-second transcription is `12500`, exactly,
90
+ * and the "units are integers" rule holds for every modality instead of holding
91
+ * for tokens and being quietly broken by audio.
92
+ */
93
+ export const USAGE_UNITS = [
94
+ 'input_tokens',
95
+ 'cached_input_tokens',
96
+ 'output_tokens',
97
+ 'reasoning_tokens',
98
+ 'requests',
99
+ 'images',
100
+ 'audio_input_milliseconds',
101
+ 'audio_output_milliseconds',
102
+ 'video_milliseconds',
103
+ 'characters',
104
+ 'embeddings',
105
+ ];
106
+ export const usageUnitSchema = z.enum(USAGE_UNITS);
107
+ /**
108
+ * A metered quantity of ONE unit. Never money — a quantity carries no price and
109
+ * no currency, so a consumer cannot mistake a token count for an amount owed.
110
+ */
111
+ export const usageQuantitySchema = z
112
+ .object({
113
+ unit: usageUnitSchema,
114
+ quantity: z.number().int().nonnegative().safe(),
115
+ })
116
+ .strict();
117
+ /**
118
+ * Where a metered quantity came from.
119
+ *
120
+ * Kept explicit because the three are not interchangeable when a charge is
121
+ * disputed: `provider_reported` is the upstream's own count, `oxy_measured` is
122
+ * counted by the platform (streamed bytes, wall-clock milliseconds), and
123
+ * `estimated` is a reconstruction used when a provider returned no usage at
124
+ * all. An estimate that is indistinguishable from a reported number is an
125
+ * estimate nobody can later reconcile or refund against.
126
+ */
127
+ export const USAGE_SOURCES = ['provider_reported', 'oxy_measured', 'estimated'];
128
+ export const usageSourceSchema = z.enum(USAGE_SOURCES);
129
+ /**
130
+ * A price for one unit, as `amount` per `per` units — `per` because a price
131
+ * quoted per single token would need more fractional digits than it is worth
132
+ * ("$3.00 per 1000000 input_tokens" is how every provider quotes it, and how
133
+ * every customer reads it).
134
+ */
135
+ export const unitPriceSchema = z
136
+ .object({
137
+ unit: usageUnitSchema,
138
+ amount: exactDecimalSchema,
139
+ per: z.number().int().positive().safe(),
140
+ currency: currencyCodeSchema,
141
+ })
142
+ .strict();
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Price versions — the immutable snapshots customer pricing is quoted and
3
+ * settled against.
4
+ *
5
+ * A price is never edited in place. A change publishes a NEW version that
6
+ * supersedes the old one, and every settled receipt keeps the id of the version
7
+ * it was priced with. That is what makes an invoice reproducible a year later:
8
+ * the receipt does not say "3.00 per million tokens", it says "priced under
9
+ * `pv_2026_08`", and that version still exists, unchanged, with its own
10
+ * effective window.
11
+ *
12
+ * Prices are exact decimal strings (see `money.ts`), never floats, and they are
13
+ * quoted per unit. The amount a customer owes is computed from them and is
14
+ * carried in the same exact form, so no step of the calculation passes through
15
+ * a representation that cannot hold the value.
16
+ *
17
+ * Decided in: docs/adr/0009-usage-reservation-and-settlement.md.
18
+ */
19
+ import { z } from 'zod';
20
+ import { inferenceTimestampSchema, modelReferenceSchema, inferenceProviderSlugSchema, } from './identifiers.js';
21
+ import { currencyCodeSchema, unitPriceSchema } from './money.js';
22
+ /**
23
+ * Lifecycle of a price version.
24
+ *
25
+ * `draft` is quotable in Console previews but may never price a receipt;
26
+ * `active` is what live requests are priced with; `superseded` priced receipts
27
+ * in the past and still resolves for them forever.
28
+ */
29
+ export const priceVersionStatusSchema = z.enum(['draft', 'active', 'superseded']);
30
+ /**
31
+ * A published set of customer prices for one model reference on one provider.
32
+ *
33
+ * Scoped to a `(modelReference, provider)` pair rather than to a model alone
34
+ * because the same model costs different amounts on different providers, and a
35
+ * receipt has to be reproducible against the route that actually served it.
36
+ */
37
+ export const priceVersionSchema = z
38
+ .object({
39
+ /** See `version.ts`: served on its own by the catalogue, so it is versioned. */
40
+ schemaVersion: z.literal(1),
41
+ priceVersionId: z.string().min(1).max(128),
42
+ status: priceVersionStatusSchema,
43
+ modelReference: modelReferenceSchema,
44
+ provider: inferenceProviderSlugSchema,
45
+ currency: currencyCodeSchema,
46
+ unitPrices: z.array(unitPriceSchema).min(1),
47
+ effectiveFrom: inferenceTimestampSchema,
48
+ /** Absent while this version is the current one. */
49
+ effectiveUntil: inferenceTimestampSchema.optional(),
50
+ /** The version this one replaced, absent for the first version of a route. */
51
+ supersedesPriceVersionId: z.string().min(1).max(128).optional(),
52
+ createdAt: inferenceTimestampSchema,
53
+ })
54
+ .superRefine((priceVersion, ctx) => {
55
+ const units = priceVersion.unitPrices.map((price) => price.unit);
56
+ if (new Set(units).size !== units.length) {
57
+ ctx.addIssue({
58
+ code: z.ZodIssueCode.custom,
59
+ path: ['unitPrices'],
60
+ message: 'a unit may be priced only once per price version',
61
+ });
62
+ }
63
+ for (const [index, price] of priceVersion.unitPrices.entries()) {
64
+ if (price.currency !== priceVersion.currency) {
65
+ ctx.addIssue({
66
+ code: z.ZodIssueCode.custom,
67
+ path: ['unitPrices', index, 'currency'],
68
+ message: 'every unit price must be quoted in the price version currency',
69
+ });
70
+ }
71
+ }
72
+ // Compared as instants, not as strings: `…T00:00:00Z` and `…T00:00:00.000Z`
73
+ // are the same moment and sort differently as text.
74
+ if (priceVersion.effectiveUntil !== undefined &&
75
+ Date.parse(priceVersion.effectiveUntil) <= Date.parse(priceVersion.effectiveFrom)) {
76
+ ctx.addIssue({
77
+ code: z.ZodIssueCode.custom,
78
+ path: ['effectiveUntil'],
79
+ message: 'a price version must stop applying after it started applying',
80
+ });
81
+ }
82
+ // A superseded version priced requests during a window that has closed. Left
83
+ // open, it is indistinguishable from the current one when a receipt is
84
+ // re-priced years later — which is the one job this record exists to do.
85
+ if (priceVersion.status === 'superseded' && priceVersion.effectiveUntil === undefined) {
86
+ ctx.addIssue({
87
+ code: z.ZodIssueCode.custom,
88
+ path: ['effectiveUntil'],
89
+ message: 'a superseded price version must record when it stopped applying',
90
+ });
91
+ }
92
+ });
93
+ /**
94
+ * The price snapshot a settled receipt keeps.
95
+ *
96
+ * The unit prices are COPIED onto the receipt, not just referenced, so a receipt
97
+ * remains readable even if the price version record is later archived, and so
98
+ * that a mistake in the copy is visible as a disagreement with the version it
99
+ * names rather than silently invisible.
100
+ */
101
+ export const priceSnapshotSchema = z
102
+ .object({
103
+ priceVersionId: z.string().min(1).max(128),
104
+ currency: currencyCodeSchema,
105
+ unitPrices: z.array(unitPriceSchema).min(1),
106
+ })
107
+ .strict();