@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
@@ -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,57 @@ 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';
150
+ export {
151
+ // Account-scoped billing: who pays, what they hold, what bounds them, and
152
+ // how it reconciles against the payment processor. A grant and a purchase
153
+ // are never one number — there is deliberately no total.
154
+ BILLING_MODES, billingModeSchema, BILLING_PROFILE_STATUSES, billingProfileStatusSchema, autoRechargeSchema, billingProfileSchema, accountBillingStateSchema, BILLING_INVOICE_STATUSES, billingInvoiceStatusSchema, billingInvoiceSchema, EXTERNAL_PAYMENT_PROVIDERS, externalPaymentProviderSchema, EXTERNAL_PAYMENT_KINDS, externalPaymentKindSchema, externalPaymentSchema, AUTO_RECHARGE_STATUSES, autoRechargeStatusSchema, autoRechargeAttemptSchema, RECONCILIATION_DISCREPANCY_KINDS, reconciliationDiscrepancyKindSchema, RECONCILIATION_RUN_STATUSES, reconciliationRunStatusSchema, reconciliationDiscrepancySchema, reconciliationRunSchema, reconciliationReportSchema, } from './inference/accountBilling.js';
155
+ export {
156
+ // Product entitlements. Allowances are integer counts and money is an exact
157
+ // decimal, so a plan allowance cannot be added to a balance.
158
+ PRODUCT_PLAN_STATUSES, productPlanStatusSchema, LIVE_PRODUCT_PLAN_STATUSES, planAllowanceSchema, productPlanSchema, payAsYouGoEntitlementSchema, COST_CENTER_STATUSES, costCenterStatusSchema, costCenterSchema, costCenterSpendSchema, productEntitlementSchema, } from './inference/entitlement.js';
@@ -0,0 +1,331 @@
1
+ /**
2
+ * The account-scoped billing surface: who pays, what they hold, what bounds
3
+ * them, and how it reconciles against the payment processor.
4
+ *
5
+ * Every shape here is denominated in {@link exactDecimalSchema} — an exact
6
+ * decimal string — for the reason `money.ts` gives at length: a JS `number`
7
+ * cannot represent `0.1 + 0.2`, and a balance is the last place a rounding error
8
+ * should be allowed to accumulate silently.
9
+ *
10
+ * ## What is NOT here, and where it is instead
11
+ *
12
+ * The BALANCE and the BUDGETS both live at `/inference/reporting` (#972
13
+ * workstream 8), which owns the customer's view of what they hold and what
14
+ * bounds them, stamped `{source, consistency}` so a reader can always tell which
15
+ * kind of number they are looking at. This file declares who PAYS and on what
16
+ * TERMS, plus the records that sit behind those numbers — invoices, processor
17
+ * payments, auto-recharge attempts and reconciliation.
18
+ *
19
+ * The split is not cosmetic. A second balance shape here would be a second
20
+ * answer to one question, and the pair would disagree the day one of them stops
21
+ * accounting for an invoiced account's unused credit line — which is exactly the
22
+ * kind of drift a customer discovers before we do.
23
+ *
24
+ * ## Product entitlements are NOT here
25
+ *
26
+ * Alia plans, their monthly allowances and the API-credit product live in
27
+ * `entitlement.ts`, in a shape that cannot be added to a balance. #972 is
28
+ * explicit that confusing a product subscription with pay-as-you-go inference
29
+ * spend is the failure mode, so the two are separate types that share no field
30
+ * and no unit.
31
+ *
32
+ * ## Stripe appears only as a REFERENCE
33
+ *
34
+ * `externalRef` names a record in the processor's database; nothing in this file
35
+ * is derived from Stripe, and no shape here can carry a processor-computed
36
+ * balance. The epic's invariant is that Stripe is a payment and invoicing
37
+ * processor and not the authoritative usage ledger, so the reconciliation shapes
38
+ * below describe a COMPARISON between two independent records rather than an
39
+ * import of one into the other.
40
+ *
41
+ * Decided in: docs/adr/0014-account-billing-and-entitlements.md,
42
+ * docs/adr/0009-usage-reservation-and-settlement.md.
43
+ */
44
+ import { z } from 'zod';
45
+ import { oxyAccountIdSchema } from './identifiers.js';
46
+ import { currencyCodeSchema, exactDecimalSchema } from './money.js';
47
+ /* -------------------------------------------------------------------------- */
48
+ /* Billing profile */
49
+ /* -------------------------------------------------------------------------- */
50
+ /**
51
+ * How an account pays.
52
+ *
53
+ * `prepaid` spends money it topped up in advance. `invoiced` draws against a
54
+ * credit limit and is billed in arrears — the enterprise shape. Both settle
55
+ * through the same ledger; the difference is only which bucket a reservation
56
+ * draws from once the prepaid and granted balances are exhausted.
57
+ */
58
+ export const BILLING_MODES = ['prepaid', 'invoiced'];
59
+ export const billingModeSchema = z.enum(BILLING_MODES);
60
+ /** Whether the profile may currently spend at all. */
61
+ export const BILLING_PROFILE_STATUSES = ['active', 'suspended', 'closed'];
62
+ export const billingProfileStatusSchema = z.enum(BILLING_PROFILE_STATUSES);
63
+ /**
64
+ * Automatic top-up settings.
65
+ *
66
+ * An IMPLICATION rather than a biconditional, matching the column CHECK: the
67
+ * two amounts may be configured before the feature is switched on, but an
68
+ * `enabled` recharge with either missing is a setting that reads as "on" and can
69
+ * never fire — the shape a customer discovers only when their traffic stops.
70
+ */
71
+ export const autoRechargeSchema = z
72
+ .object({
73
+ enabled: z.boolean(),
74
+ /** Recharge when the spendable balance falls below this. */
75
+ threshold: exactDecimalSchema.optional(),
76
+ /** How much to add. */
77
+ amount: exactDecimalSchema.optional(),
78
+ })
79
+ .strict()
80
+ .refine((value) => !value.enabled || (value.threshold !== undefined && value.amount !== undefined), { message: 'an enabled auto-recharge must carry both a threshold and an amount' });
81
+ /**
82
+ * Which account pays for a workload, and on what terms.
83
+ *
84
+ * `accountId` is an Oxy account of ANY kind — personal, organization, project,
85
+ * bot or channel — because `Application.ownerAccountId` may be any of them. It
86
+ * is branded (`oxyAccountIdSchema`), so a delegated end-user id cannot be
87
+ * substituted for it anywhere in this contract.
88
+ */
89
+ export const billingProfileSchema = z
90
+ .object({
91
+ /** See `version.ts`: this shape is served to Console and to Alia. */
92
+ schemaVersion: z.literal(1),
93
+ accountId: oxyAccountIdSchema,
94
+ currency: currencyCodeSchema,
95
+ billingMode: billingModeSchema,
96
+ status: billingProfileStatusSchema,
97
+ /**
98
+ * How far an `invoiced` account may draw before a reservation is refused.
99
+ * Always `'0'` for a `prepaid` account, where it is not consulted at all.
100
+ */
101
+ creditLimit: exactDecimalSchema,
102
+ autoRecharge: autoRechargeSchema,
103
+ createdAt: z.string().datetime(),
104
+ updatedAt: z.string().datetime(),
105
+ })
106
+ .strict();
107
+ /* -------------------------------------------------------------------------- */
108
+ /* Billing state */
109
+ /* -------------------------------------------------------------------------- */
110
+ /**
111
+ * WHO pays for an account and on what TERMS. Deliberately not how much they
112
+ * have.
113
+ *
114
+ * The balance lives at `GET /inference/reporting/accounts/:accountId/balance`
115
+ * (#972 workstream 8), which reads `account_balances` and stamps every response
116
+ * with `{source: 'financial_ledger', consistency: 'authoritative'}`. Restating
117
+ * those amounts here would be a second answer to one question, and the failure
118
+ * mode of two balance endpoints is the pair disagreeing on the day one of them
119
+ * stops accounting for the credit line.
120
+ *
121
+ * What this shape adds, and what nothing else carries: `billingAccountId` and
122
+ * `inherited`. A project draws on the nearest ancestor that has a profile
123
+ * (ADR 0014), so a Console page has to be able to say "this project spends the
124
+ * organization's balance" — showing somebody else's money under a project's name
125
+ * with no indication whose it is would be worse than showing nothing.
126
+ */
127
+ export const accountBillingStateSchema = z
128
+ .object({
129
+ /** See `version.ts`: this shape is served to Console and to Alia. */
130
+ schemaVersion: z.literal(1),
131
+ accountId: oxyAccountIdSchema,
132
+ billingAccountId: oxyAccountIdSchema,
133
+ inherited: z.boolean(),
134
+ profile: billingProfileSchema,
135
+ })
136
+ .strict();
137
+ /*
138
+ * SPENDING LIMITS AND THEIR ALERTS ARE NOT DECLARED HERE.
139
+ *
140
+ * `/inference/reporting` owns the budget surface (#972 workstream 8) and
141
+ * declares its own shapes beside the router that serves them, stamped
142
+ * `{source, consistency}` so a reader can always tell which kind of number they
143
+ * are looking at. A second set here would be a second answer to what a budget
144
+ * is, on a table both would write — and the closed alert-threshold set already
145
+ * lives once more where it is ENFORCED, as a CHECK on
146
+ * `spending_limits.alert_threshold_bps`.
147
+ */
148
+ /* -------------------------------------------------------------------------- */
149
+ /* Invoices */
150
+ /* -------------------------------------------------------------------------- */
151
+ export const BILLING_INVOICE_STATUSES = ['draft', 'open', 'paid', 'void'];
152
+ export const billingInvoiceStatusSchema = z.enum(BILLING_INVOICE_STATUSES);
153
+ /**
154
+ * A period's charges, aggregated — the invoiced-enterprise settlement document.
155
+ *
156
+ * `subtotalAmount` is the EXACT sum of the receipts on the invoice, at full
157
+ * scale; `totalAmount` is the rounded figure actually charged. The difference is
158
+ * booked as an `invoice_rounding` ledger entry rather than discarded, because a
159
+ * discarded remainder is money that exists in one system and not the other.
160
+ *
161
+ * `minorUnitExponent` is carried rather than derived from the currency code:
162
+ * USD is 2, JPY is 0, BHD is 3, and that is not a property this platform's
163
+ * database knows. Storing what was used keeps the invoice reproducible.
164
+ */
165
+ export const billingInvoiceSchema = z
166
+ .object({
167
+ /** See `version.ts`: this shape is served to Console and to Alia. */
168
+ schemaVersion: z.literal(1),
169
+ id: z.string().min(1).max(64),
170
+ accountId: oxyAccountIdSchema,
171
+ currency: currencyCodeSchema,
172
+ periodStart: z.string().datetime(),
173
+ periodEnd: z.string().datetime(),
174
+ status: billingInvoiceStatusSchema,
175
+ subtotalAmount: exactDecimalSchema,
176
+ totalAmount: exactDecimalSchema,
177
+ minorUnitExponent: z.number().int().min(0).max(4),
178
+ /** The processor's own invoice id, for reconciliation. Never an authority. */
179
+ externalInvoiceRef: z.string().min(1).max(255).optional(),
180
+ issuedAt: z.string().datetime().optional(),
181
+ paidAt: z.string().datetime().optional(),
182
+ receiptCount: z.number().int().nonnegative().safe(),
183
+ })
184
+ .strict();
185
+ /* -------------------------------------------------------------------------- */
186
+ /* Payment processor boundary */
187
+ /* -------------------------------------------------------------------------- */
188
+ /** The processors this platform records payments from. */
189
+ export const EXTERNAL_PAYMENT_PROVIDERS = ['stripe'];
190
+ export const externalPaymentProviderSchema = z.enum(EXTERNAL_PAYMENT_PROVIDERS);
191
+ /**
192
+ * What kind of processor record `externalRef` names.
193
+ *
194
+ * Kept explicit because reconciliation compares like with like: a payment intent
195
+ * and the invoice it paid are two records of one movement of money, and counting
196
+ * both would double the external total.
197
+ */
198
+ export const EXTERNAL_PAYMENT_KINDS = ['payment_intent', 'invoice'];
199
+ export const externalPaymentKindSchema = z.enum(EXTERNAL_PAYMENT_KINDS);
200
+ /**
201
+ * A processor payment that funded an Oxy balance.
202
+ *
203
+ * This is the reconciliation ANCHOR: one row per charge that landed in the
204
+ * ledger, carrying both the processor's reference and the ledger entry it
205
+ * produced. Without it, matching a Stripe charge back to a ledger entry means
206
+ * parsing an idempotency key, and a parser is not a foreign key.
207
+ */
208
+ export const externalPaymentSchema = z
209
+ .object({
210
+ id: z.string().min(1).max(64),
211
+ accountId: oxyAccountIdSchema,
212
+ provider: externalPaymentProviderSchema,
213
+ externalKind: externalPaymentKindSchema,
214
+ externalRef: z.string().min(1).max(255),
215
+ amount: exactDecimalSchema,
216
+ currency: currencyCodeSchema,
217
+ ledgerEntryId: z.string().min(1).max(64),
218
+ occurredAt: z.string().datetime(),
219
+ createdAt: z.string().datetime(),
220
+ })
221
+ .strict();
222
+ /** Lifecycle of one automatic top-up. */
223
+ export const AUTO_RECHARGE_STATUSES = ['pending', 'succeeded', 'failed'];
224
+ export const autoRechargeStatusSchema = z.enum(AUTO_RECHARGE_STATUSES);
225
+ /**
226
+ * One attempt to top an account up automatically.
227
+ *
228
+ * Recorded BEFORE the processor is called and keyed on the account, currency and
229
+ * the window it fired in, so a sweep that runs twice — or two instances of it —
230
+ * charges a customer's card once. An off-session charge is the one operation on
231
+ * this surface where a duplicate is not merely a wrong number in a report.
232
+ */
233
+ export const autoRechargeAttemptSchema = z
234
+ .object({
235
+ /** See `version.ts`: this shape is served to Console and to Alia. */
236
+ schemaVersion: z.literal(1),
237
+ id: z.string().min(1).max(64),
238
+ accountId: oxyAccountIdSchema,
239
+ currency: currencyCodeSchema,
240
+ requestedAmount: exactDecimalSchema,
241
+ /** The spendable balance that triggered it — why this attempt exists. */
242
+ balanceAtTrigger: exactDecimalSchema,
243
+ status: autoRechargeStatusSchema,
244
+ externalRef: z.string().min(1).max(255).optional(),
245
+ /** The processor's own decline code. Never a free-form message. */
246
+ failureCode: z.string().min(1).max(64).optional(),
247
+ createdAt: z.string().datetime(),
248
+ updatedAt: z.string().datetime(),
249
+ })
250
+ .strict();
251
+ /* -------------------------------------------------------------------------- */
252
+ /* Reconciliation */
253
+ /* -------------------------------------------------------------------------- */
254
+ /**
255
+ * How an Oxy record and a processor record fail to agree.
256
+ *
257
+ * A closed set, and every member is a DIFFERENT operational problem:
258
+ *
259
+ * - `missing_in_ledger` — the processor took money Oxy never credited. The
260
+ * customer paid and has no balance. This is the one that costs a customer.
261
+ * - `missing_in_external` — Oxy credited a balance with no processor charge
262
+ * behind it. This is the one that costs Oxy.
263
+ * - `amount_mismatch` — both exist and disagree.
264
+ * - `account_unresolved` — a processor charge whose customer maps to no Oxy
265
+ * account. Money arrived and nobody owns it.
266
+ *
267
+ * Collapsing them into a single "discrepancy" count is what makes a
268
+ * reconciliation report a number nobody acts on.
269
+ */
270
+ export const RECONCILIATION_DISCREPANCY_KINDS = [
271
+ 'missing_in_ledger',
272
+ 'missing_in_external',
273
+ 'amount_mismatch',
274
+ 'account_unresolved',
275
+ ];
276
+ export const reconciliationDiscrepancyKindSchema = z.enum(RECONCILIATION_DISCREPANCY_KINDS);
277
+ export const RECONCILIATION_RUN_STATUSES = ['running', 'completed', 'failed'];
278
+ export const reconciliationRunStatusSchema = z.enum(RECONCILIATION_RUN_STATUSES);
279
+ export const reconciliationDiscrepancySchema = z
280
+ .object({
281
+ /** See `version.ts`: this shape is served to Console and to Alia. */
282
+ schemaVersion: z.literal(1),
283
+ id: z.string().min(1).max(64),
284
+ runId: z.string().min(1).max(64),
285
+ kind: reconciliationDiscrepancyKindSchema,
286
+ accountId: oxyAccountIdSchema.optional(),
287
+ externalRef: z.string().min(1).max(255).optional(),
288
+ ledgerEntryId: z.string().min(1).max(64).optional(),
289
+ /** What Oxy recorded. Absent for `missing_in_ledger`. */
290
+ ledgerAmount: exactDecimalSchema.optional(),
291
+ /** What the processor recorded. Absent for `missing_in_external`. */
292
+ externalAmount: exactDecimalSchema.optional(),
293
+ currency: currencyCodeSchema,
294
+ createdAt: z.string().datetime(),
295
+ })
296
+ .strict();
297
+ /**
298
+ * One reconciliation pass over a window.
299
+ *
300
+ * The totals are carried beside the discrepancy list on purpose: two totals that
301
+ * agree while the lists differ is a real and common state (a charge recorded
302
+ * against the wrong account), and a report that only published a difference
303
+ * would call it clean.
304
+ */
305
+ export const reconciliationRunSchema = z
306
+ .object({
307
+ /** See `version.ts`: this shape is served to Console and to Alia. */
308
+ schemaVersion: z.literal(1),
309
+ id: z.string().min(1).max(64),
310
+ provider: externalPaymentProviderSchema,
311
+ /** Absent for a platform-wide pass. */
312
+ accountId: oxyAccountIdSchema.optional(),
313
+ currency: currencyCodeSchema,
314
+ periodStart: z.string().datetime(),
315
+ periodEnd: z.string().datetime(),
316
+ status: reconciliationRunStatusSchema,
317
+ ledgerTotal: exactDecimalSchema,
318
+ externalTotal: exactDecimalSchema,
319
+ discrepancyCount: z.number().int().nonnegative().safe(),
320
+ startedAt: z.string().datetime(),
321
+ completedAt: z.string().datetime().optional(),
322
+ })
323
+ .strict();
324
+ export const reconciliationReportSchema = z
325
+ .object({
326
+ /** See `version.ts`: this shape is served to Console and to Alia. */
327
+ schemaVersion: z.literal(1),
328
+ run: reconciliationRunSchema,
329
+ discrepancies: z.array(reconciliationDiscrepancySchema),
330
+ })
331
+ .strict();
@@ -0,0 +1,103 @@
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 OXY EDGE, on admission and before
89
+ * authentication completes, and is always present (ADR 0007). Not by the data
90
+ * plane: a data plane that minted the correlation key could not correlate a
91
+ * request it never received, and the requests rejected at the edge are exactly
92
+ * the ones an operator most needs to find.
93
+ *
94
+ * `generationId` is present once a generation exists, which is why it is
95
+ * optional on a request and expected on a receipt. That one IS the data plane's,
96
+ * because only the data plane knows how many outputs a request produced.
97
+ */
98
+ export const inferenceAttributionSchema = z.object({
99
+ principal: authenticatedPrincipalSchema,
100
+ userId: delegatedUserIdSchema.optional(),
101
+ requestId: requestIdSchema,
102
+ generationId: generationIdSchema.optional(),
103
+ });