@oxyhq/contracts 0.27.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.
@@ -35,6 +35,12 @@ const identifiers_1 = require("./identifiers");
35
35
  * `quota_exceeded`, `byok_credential_invalid`), routing/permission policy
36
36
  * (`policy_violation`, `commercial_permission_denied`, `no_route_available`),
37
37
  * and the platform or its upstreams (everything from `deployment_unavailable`).
38
+ *
39
+ * The platform group is NOT uniformly retryable, and that is the point of
40
+ * `provider_credential_invalid` sitting in it: an upstream that refuses the
41
+ * PLATFORM's own credential fails every identical retry until an operator
42
+ * rotates a key, so classifying it as `provider_error` would send every client
43
+ * into a retry loop against a request that cannot succeed.
38
44
  */
39
45
  exports.INFERENCE_ERROR_CODES = [
40
46
  'invalid_request',
@@ -61,6 +67,7 @@ exports.INFERENCE_ERROR_CODES = [
61
67
  'provider_error',
62
68
  'provider_timeout',
63
69
  'provider_overloaded',
70
+ 'provider_credential_invalid',
64
71
  'service_unavailable',
65
72
  'internal_error',
66
73
  ];
@@ -73,6 +80,13 @@ exports.inferenceErrorCodeSchema = zod_1.z.enum(exports.INFERENCE_ERROR_CODES);
73
80
  * names, while a quota is an account-level ceiling that only a human raises.
74
81
  * `cancelled` is here because the caller already withdrew the request; a client
75
82
  * that retries it is contradicting its own cancellation.
83
+ *
84
+ * `byok_credential_invalid` and `provider_credential_invalid` are the same
85
+ * failure seen from the two sides of the BYOK boundary — the customer's own
86
+ * upstream credential and the platform's — and they are two codes rather than
87
+ * one because only the first names an action the customer can take. Both are
88
+ * non-retryable for the same reason: a credential an upstream has refused keeps
89
+ * being refused until somebody replaces it.
76
90
  */
77
91
  exports.NON_RETRYABLE_INFERENCE_ERROR_CODES = [
78
92
  'invalid_request',
@@ -94,6 +108,7 @@ exports.NON_RETRYABLE_INFERENCE_ERROR_CODES = [
94
108
  'no_route_available',
95
109
  'upstream_content_filtered',
96
110
  'cancelled',
111
+ 'provider_credential_invalid',
97
112
  ];
98
113
  const NON_RETRYABLE_CODE_SET = new Set(exports.NON_RETRYABLE_INFERENCE_ERROR_CODES);
99
114
  /**
@@ -57,9 +57,17 @@ exports.oxyApplicationIdSchema = zod_1.z.string().min(1).max(64);
57
57
  /** An Oxy `ApplicationCredential._id` — the credential used for this request. */
58
58
  exports.oxyCredentialIdSchema = zod_1.z.string().min(1).max(64);
59
59
  /**
60
- * A request id generated by the data plane. Correlates the Oxy edge, the data
61
- * plane, the financial ledger and the customer-visible receipt, so it appears on
62
- * every stream event and every ledger record.
60
+ * A request id allocated by the Oxy EDGE, at admission and BEFORE
61
+ * authentication, so that a request rejected for a bad credential is as
62
+ * traceable as one that was served (ADR 0007, and step 1 of ADR 0010's edge
63
+ * order). It is required on the inbound envelope, which is what makes the data
64
+ * plane a consumer of this id rather than its source: the data plane echoes it
65
+ * on every stream event, on the usage report and on anything it can be asked
66
+ * about later.
67
+ *
68
+ * Correlates the Oxy edge, the data plane, the financial ledger and the
69
+ * customer-visible receipt, so it appears on every stream event and every
70
+ * ledger record.
63
71
  */
64
72
  exports.requestIdSchema = zod_1.z.string().min(1).max(128);
65
73
  /**
@@ -88,6 +88,49 @@ exports.moneySchema = zod_1.z
88
88
  /**
89
89
  * The closed set of units inference is metered in.
90
90
  *
91
+ * **The units PARTITION a request: every unit counts material no other unit
92
+ * counts.** `cached_input_tokens` is not part of `input_tokens`, and
93
+ * `reasoning_tokens` is not part of `output_tokens` — they are siblings, not
94
+ * subsets. A request whose 10 000-token prompt was served 9 000 tokens from
95
+ * cache is reported as `input_tokens: 1000` beside `cached_input_tokens: 9000`,
96
+ * never as `input_tokens: 10000` beside it.
97
+ *
98
+ * That belongs to the definition rather than to a convention somewhere else,
99
+ * because settlement applies a price to EVERY reported unit and sums them
100
+ * (`inferenceLedger.service.ts`'s `computeCharge`). Under the partition rule
101
+ * that sum IS the request's cost, and a cached token can carry its own — lower
102
+ * — price. Under the nested reading the same sum charges the cached and
103
+ * reasoning tokens twice: once inside their parent and once on their own line.
104
+ * It fails silently, because every total still looks plausible and the receipt
105
+ * is still internally consistent, and on a reasoning model the reasoning tokens
106
+ * can dominate the completion, so the error is not marginal.
107
+ *
108
+ * **Every OpenAI-compatible provider reports the other way round**:
109
+ * `prompt_tokens` INCLUDES `prompt_tokens_details.cached_tokens`, and
110
+ * `completion_tokens` INCLUDES `completion_tokens_details.reasoning_tokens`.
111
+ * Normalising is the data plane's job and it is subtraction:
112
+ *
113
+ * ```text
114
+ * input_tokens = prompt_tokens - prompt_tokens_details.cached_tokens
115
+ * output_tokens = completion_tokens - completion_tokens_details.reasoning_tokens
116
+ * ```
117
+ *
118
+ * No refinement in this package can enforce it, and saying so is part of the
119
+ * rule: a nested report and a disjoint one are the same four non-negative
120
+ * integers, so no predicate over a single report can tell them apart. The two
121
+ * structural guards that DO exist — refining `cached <= input` and
122
+ * `reasoning <= output`, or deriving the parents instead of reporting them —
123
+ * both encode the nested reading, which is the one this rule rejects. What IS
124
+ * enforceable is the arithmetic that depends on the rule, and that is where the
125
+ * enforcement lives — `inferenceLedger.service.test.ts` prices a report in
126
+ * which cached and reasoning tokens are both non-zero and asserts the exact
127
+ * total, which the nested reading cannot produce.
128
+ *
129
+ * Where the public surface has to speak a nested dialect, the sum is put back
130
+ * at the boundary rather than the internal reading being bent to it
131
+ * (`routes/inferenceEdge.ts` renders `prompt_tokens` as
132
+ * `input_tokens + cached_input_tokens`).
133
+ *
91
134
  * Time is carried in integer MILLISECONDS rather than seconds so that no unit
92
135
  * quantity is ever fractional: a 12.5-second transcription is `12500`, exactly,
93
136
  * and the "units are integers" rule holds for every modality instead of holding
@@ -131,6 +131,12 @@ exports.inferenceRequestOutcomeSchema = zod_1.z.enum([
131
131
  * `usageSource` is load-bearing when a provider returns no usage at all: the
132
132
  * report still arrives, marked `estimated`, so settlement can apply the
133
133
  * estimation policy knowingly instead of treating a reconstruction as fact.
134
+ *
135
+ * `units` is a PARTITION of what the request consumed, not a set of totals with
136
+ * details hanging off them — see `USAGE_UNITS` in `money.ts`. Reporting a provider's
137
+ * nested `prompt_tokens`/`completion_tokens` verbatim charges the cached and
138
+ * reasoning tokens twice, so subtracting the children out is part of what
139
+ * "normalized" means in this shape's name.
134
140
  */
135
141
  exports.normalizedUsageReportSchema = zod_1.z
136
142
  .object({
@@ -35,6 +35,34 @@
35
35
  * field is additive and does not bump it, because a consumer on the previous
36
36
  * version parses the message correctly and simply does not read the new field.
37
37
  *
38
+ * ## Which shapes reject an unknown field
39
+ *
40
+ * That last rule is why the shapes EXCHANGED WITH THE DATA PLANE are not
41
+ * `.strict()` at their top level — the request envelope, the four usage records,
42
+ * the stream events, the error body, the catalogue descriptors, the price
43
+ * version. The split is a decision rather than an omission: `.strict()` and
44
+ * "adding an optional field is additive" cannot both hold on one shape, because
45
+ * a producer one minor version ahead would have its whole message REFUSED
46
+ * rather than its new field ignored. For a usage report that means a request
47
+ * already served upstream can never be settled and Oxy absorbs its cost, which
48
+ * is a worse failure than the one strictness would have caught.
49
+ *
50
+ * Their LEAVES are strict, and that is where the protection lives: a stripped
51
+ * field is the worse outcome exactly where it would be a leak or a second
52
+ * source of truth, because it disappears at this parse and survives in the
53
+ * producer, which is where somebody eventually reads it. So
54
+ * `clientRequestMetadataSchema` (no IP, ever), `moneySchema` (no convenience
55
+ * float beside the exact decimal), `providerErrorPassthroughSchema` (no
56
+ * upstream request or headers beside the message), `usageQuantitySchema` and
57
+ * `unitPriceSchema` all refuse an unknown field, while the envelope carrying
58
+ * them tolerates an additive one.
59
+ *
60
+ * A shape Oxy does NOT exchange with the data plane is strict at its top level
61
+ * too, since nothing there can run ahead of this package:
62
+ * `providerConnectionSchema`, where an unknown field is how a BYOK credential
63
+ * escapes, and the billing and entitlement records, where one is a second
64
+ * number beside an exact amount.
65
+ *
38
66
  * Decided in: docs/adr/0006-oxy-relay-boundary.md, docs/adr/0010-public-api-compatibility.md.
39
67
  */
40
68
  Object.defineProperty(exports, "__esModule", { value: true });