@oxyhq/contracts 0.28.0 → 0.29.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.
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/inference/errors.js +108 -9
- package/dist/cjs/inference/streamEvents.js +11 -1
- package/dist/cjs/inference/version.js +10 -3
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/inference/errors.js +108 -9
- package/dist/esm/inference/streamEvents.js +11 -1
- package/dist/esm/inference/version.js +10 -3
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/inference/errors.d.ts +46 -10
- package/dist/types/inference/streamEvents.d.ts +31 -22
- package/dist/types/inference/version.d.ts +10 -3
- package/package.json +1 -1
|
@@ -41,6 +41,11 @@ const identifiers_1 = require("./identifiers");
|
|
|
41
41
|
* PLATFORM's own credential fails every identical retry until an operator
|
|
42
42
|
* rotates a key, so classifying it as `provider_error` would send every client
|
|
43
43
|
* into a retry loop against a request that cannot succeed.
|
|
44
|
+
*
|
|
45
|
+
* `provider_billing_refused` is in that group for the same reason and was found
|
|
46
|
+
* the same way — an upstream declining to bill OXY (Anthropic answers 402) has
|
|
47
|
+
* to be distinguishable from the customer's own balance running out, or the
|
|
48
|
+
* error tells them to go and top up an account that is not the one at fault.
|
|
44
49
|
*/
|
|
45
50
|
exports.INFERENCE_ERROR_CODES = [
|
|
46
51
|
'invalid_request',
|
|
@@ -68,6 +73,7 @@ exports.INFERENCE_ERROR_CODES = [
|
|
|
68
73
|
'provider_timeout',
|
|
69
74
|
'provider_overloaded',
|
|
70
75
|
'provider_credential_invalid',
|
|
76
|
+
'provider_billing_refused',
|
|
71
77
|
'service_unavailable',
|
|
72
78
|
'internal_error',
|
|
73
79
|
];
|
|
@@ -87,6 +93,12 @@ exports.inferenceErrorCodeSchema = zod_1.z.enum(exports.INFERENCE_ERROR_CODES);
|
|
|
87
93
|
* one because only the first names an action the customer can take. Both are
|
|
88
94
|
* non-retryable for the same reason: a credential an upstream has refused keeps
|
|
89
95
|
* being refused until somebody replaces it.
|
|
96
|
+
*
|
|
97
|
+
* `quota_exceeded` and `provider_billing_refused` divide along the same line:
|
|
98
|
+
* both are money, but one is the CUSTOMER's ceiling and the other is Oxy's
|
|
99
|
+
* account with an upstream. Reporting the second as the first is retryability-
|
|
100
|
+
* correct and diagnostically wrong, which is the worst combination — it reads
|
|
101
|
+
* as actionable and the action does nothing.
|
|
90
102
|
*/
|
|
91
103
|
exports.NON_RETRYABLE_INFERENCE_ERROR_CODES = [
|
|
92
104
|
'invalid_request',
|
|
@@ -109,27 +121,114 @@ exports.NON_RETRYABLE_INFERENCE_ERROR_CODES = [
|
|
|
109
121
|
'upstream_content_filtered',
|
|
110
122
|
'cancelled',
|
|
111
123
|
'provider_credential_invalid',
|
|
124
|
+
'provider_billing_refused',
|
|
112
125
|
];
|
|
113
126
|
const NON_RETRYABLE_CODE_SET = new Set(exports.NON_RETRYABLE_INFERENCE_ERROR_CODES);
|
|
127
|
+
/* -------------------------------------------------------------------------- */
|
|
128
|
+
/* Credential-shaped text */
|
|
129
|
+
/* -------------------------------------------------------------------------- */
|
|
130
|
+
/**
|
|
131
|
+
* A run of characters long enough and opaque enough to BE a credential.
|
|
132
|
+
*
|
|
133
|
+
* The alphabet every bearer token, API key and base64/base64url secret is
|
|
134
|
+
* written in. The LENGTH floors below are what keep this from being an entropy
|
|
135
|
+
* heuristic: nothing here fires on a short word, so `authorization: none` and
|
|
136
|
+
* `api_key=***` read as what they are.
|
|
137
|
+
*/
|
|
138
|
+
const OPAQUE_ALPHABET = '[A-Za-z0-9][A-Za-z0-9._~+/=-]';
|
|
114
139
|
/**
|
|
115
|
-
*
|
|
140
|
+
* Words a producer substitutes FOR a credential.
|
|
116
141
|
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
142
|
+
* Excluded at the value position so a message whose secret has already been
|
|
143
|
+
* replaced is accepted. That acceptance is deliberate and is half the fix for
|
|
144
|
+
* issue #1027: the previous pattern refused `Authorization: [redacted]` — a
|
|
145
|
+
* correctly redacted string — which is precisely what pushed a producer into
|
|
146
|
+
* redacting the MARKER instead, and a marker-redacted string carries the secret
|
|
147
|
+
* and passes.
|
|
121
148
|
*/
|
|
122
|
-
const
|
|
149
|
+
const PLACEHOLDER_WORDS = 'redacted|removed|hidden|masked|scrubbed|elided|omitted|filtered|sanitized|sanitised|none|null|undefined|empty';
|
|
150
|
+
/** A value position whose contents are a placeholder rather than a secret. */
|
|
151
|
+
const NOT_A_PLACEHOLDER = `(?!(?:${PLACEHOLDER_WORDS})\\b)`;
|
|
123
152
|
/**
|
|
124
|
-
*
|
|
125
|
-
*
|
|
153
|
+
* Header and parameter names that carry a credential, as any provider spells
|
|
154
|
+
* them.
|
|
155
|
+
*
|
|
156
|
+
* The prefix group is the whole point of the rewrite: `authorization` and
|
|
157
|
+
* `api_key` were matched literally, so `x-api-key`, `anthropic-api-key`,
|
|
158
|
+
* `x-goog-api-key` and `proxy-authorization` — the spellings an upstream
|
|
159
|
+
* actually echoes — went unrecognised.
|
|
160
|
+
*/
|
|
161
|
+
const CREDENTIAL_NAME = '(?:[a-z0-9]{1,20}[-_]){0,3}(?:api[-_]?(?:key|token|secret)|authorization|auth[-_]?(?:token|key)?|access[-_]?token|id[-_]?token|refresh[-_]?token|bearer[-_]?token|secret[-_]?key|private[-_]?key|client[-_]?secret|session[-_]?(?:id|key|token)|passwords?|passwd|cookie|credentials?|tokens?|secrets?)';
|
|
162
|
+
/** An auth scheme sitting between the marker and the value. */
|
|
163
|
+
const AUTH_SCHEME = '(?:(?:bearer|basic|token|apikey|digest)\\s+)?';
|
|
164
|
+
/**
|
|
165
|
+
* The four ways a credential is recognisable in free text.
|
|
166
|
+
*
|
|
167
|
+
* Each is checked independently, so removing one signal does not clear the
|
|
168
|
+
* string — which is the failure #1027 reported. All four are load-bearing:
|
|
169
|
+
* `inference.errors.test.ts` has a case that only one of them catches, and
|
|
170
|
+
* deleting any one entry turns a test red.
|
|
171
|
+
*/
|
|
172
|
+
const CREDENTIAL_PATTERNS = [
|
|
173
|
+
// 1. A credential-bearing name ASSIGNED a value that is long enough to be a
|
|
174
|
+
// credential. The value is anchored to the separator so a placeholder at
|
|
175
|
+
// that position ends the match rather than being skipped over.
|
|
176
|
+
new RegExp(`(?:^|[^a-z0-9])${CREDENTIAL_NAME}["']?\\s*[:=]\\s*["']?${AUTH_SCHEME}${NOT_A_PLACEHOLDER}${OPAQUE_ALPHABET}{7,}`, 'i'),
|
|
177
|
+
// 2. A bearer token with no marker in front of it, which is how an upstream
|
|
178
|
+
// quotes the header value alone.
|
|
179
|
+
new RegExp(`\\bbearer\\s+${NOT_A_PLACEHOLDER}${OPAQUE_ALPHABET}{7,}`, 'i'),
|
|
180
|
+
// 3. Token grammars that ARE credentials wherever they appear, marker or not.
|
|
181
|
+
// This is the layer that survives a producer stripping the marker, and it
|
|
182
|
+
// is a closed list of issued shapes rather than an entropy score, so a
|
|
183
|
+
// request id or a base64 image fragment is unaffected.
|
|
184
|
+
//
|
|
185
|
+
// Case-SENSITIVE on purpose: `AKIA`, `AIza` and `gh[pousr]_` are issued in
|
|
186
|
+
// exactly that case, and matching them case-insensitively would start
|
|
187
|
+
// firing on ordinary words.
|
|
188
|
+
/\b(?:sk-[A-Za-z0-9_-]{8,}|[sprk]k_(?:live|test)_[A-Za-z0-9]{8,}|AKIA[0-9A-Z]{12,}|ASIA[0-9A-Z]{12,}|AIza[0-9A-Za-z_-]{20,}|gh[pousr]_[A-Za-z0-9]{16,}|github_pat_[A-Za-z0-9_]{20,}|xox[abeprs]-[A-Za-z0-9-]{10,}|glpat-[A-Za-z0-9_-]{16,}|npm_[A-Za-z0-9]{20,}|eyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{4,})/,
|
|
189
|
+
// 4. A redaction placeholder standing NEXT TO a surviving opaque value — the
|
|
190
|
+
// exact residue of the span redaction in #1027 (`{x-[redacted] <key>}`).
|
|
191
|
+
// A correct redaction puts the placeholder WHERE the value was, so the two
|
|
192
|
+
// never appear side by side; a marker-span redaction leaves them adjacent.
|
|
193
|
+
// Both signals are required, which is what keeps an ordinary redacted
|
|
194
|
+
// message from being refused.
|
|
195
|
+
new RegExp(`(?:[[<({]\\s*(?:${PLACEHOLDER_WORDS})[^\\])}>]{0,16}[\\])}>]|\\*{3,})[^A-Za-z0-9]{0,4}${OPAQUE_ALPHABET}{11,}`, 'i'),
|
|
196
|
+
];
|
|
197
|
+
/**
|
|
198
|
+
* Free text that is safe to hand a customer: bounded, and refused if it still
|
|
199
|
+
* looks like it carries a credential. Applied to BOTH the Oxy message and the
|
|
126
200
|
* upstream one — a leak is no less a leak for having been written by a provider.
|
|
201
|
+
*
|
|
202
|
+
* ## This is a last-resort REFUSAL, not protection
|
|
203
|
+
*
|
|
204
|
+
* A pattern over the OUTPUT cannot be the control that keeps a credential out of
|
|
205
|
+
* an error, and a producer that treats it as one has the hole #1027 reported.
|
|
206
|
+
* The only reliable control is redacting the KNOWN SECRET VALUE at the point
|
|
207
|
+
* where the producer still holds the bytes it sent — which is an adapter's job
|
|
208
|
+
* and is available to nobody else. This refinement exists to catch what that
|
|
209
|
+
* control missed, and nothing here is a licence to skip it.
|
|
210
|
+
*
|
|
211
|
+
* Two rules follow, and they are the whole reason this text is longer than the
|
|
212
|
+
* pattern it describes:
|
|
213
|
+
*
|
|
214
|
+
* - **Never redact by replacing the span this pattern matched.** The span is
|
|
215
|
+
* the MARKER; the secret is what follows it. OxyHQ/Relay#3 measured the
|
|
216
|
+
* result: `{x-api-key: <key>}` is refused, `{x-[redacted] <key>}` was
|
|
217
|
+
* accepted, and both carry the key. Redaction made the leak worse by
|
|
218
|
+
* converting "this string is dangerous" into "this string is fine".
|
|
219
|
+
* - **This package deliberately ships no redaction helper.** One keyed on these
|
|
220
|
+
* patterns would rebuild the same defect one layer up, and one that took the
|
|
221
|
+
* secret as an argument would only restate what the producer already has.
|
|
222
|
+
*
|
|
223
|
+
* What it still cannot see, stated so nobody relies on it: a credential with no
|
|
224
|
+
* marker, no issued-token prefix and no placeholder beside it is bytes that look
|
|
225
|
+
* like a request id, and refusing those means refusing request ids.
|
|
127
226
|
*/
|
|
128
227
|
exports.safeErrorTextSchema = zod_1.z
|
|
129
228
|
.string()
|
|
130
229
|
.min(1)
|
|
131
230
|
.max(2000)
|
|
132
|
-
.refine((value) => !
|
|
231
|
+
.refine((value) => !CREDENTIAL_PATTERNS.some((pattern) => pattern.test(value)), 'error text must not contain credential-shaped material');
|
|
133
232
|
/**
|
|
134
233
|
* A coarse classification of an upstream failure (ADR 0010's `upstreamCategory`).
|
|
135
234
|
*
|
|
@@ -177,12 +177,22 @@ exports.inferenceStreamErrorEventSchema = zod_1.z.object({
|
|
|
177
177
|
/** Carries its own `schemaVersion`: the same body is returned non-streaming. */
|
|
178
178
|
error: errors_1.inferenceErrorSchema,
|
|
179
179
|
});
|
|
180
|
-
/**
|
|
180
|
+
/**
|
|
181
|
+
* Why generation stopped.
|
|
182
|
+
*
|
|
183
|
+
* `refusal` and `content_filter` are separate members because they are separate
|
|
184
|
+
* events: the MODEL declining to answer is a property of the answer, while a
|
|
185
|
+
* filter is an upstream system removing one. The delta channels already carry
|
|
186
|
+
* that distinction (`channel: 'refusal'` beside the filter's own error code),
|
|
187
|
+
* so collapsing it here would have made the terminal event less specific than
|
|
188
|
+
* the stream that produced it.
|
|
189
|
+
*/
|
|
181
190
|
exports.inferenceFinishReasonSchema = zod_1.z.enum([
|
|
182
191
|
'stop',
|
|
183
192
|
'length',
|
|
184
193
|
'tool_calls',
|
|
185
194
|
'content_filter',
|
|
195
|
+
'refusal',
|
|
186
196
|
'cancelled',
|
|
187
197
|
]);
|
|
188
198
|
/**
|
|
@@ -74,12 +74,19 @@ exports.INFERENCE_CONTRACT_VERSION = void 0;
|
|
|
74
74
|
*
|
|
75
75
|
* MAJOR is bumped when any individual shape's `schemaVersion` increments (at
|
|
76
76
|
* least one message is now read differently by the two sides); MINOR when a
|
|
77
|
-
* shape or an optional field is added
|
|
78
|
-
*
|
|
77
|
+
* shape or an optional field is added, when a CLOSED ENUM gains a member, or
|
|
78
|
+
* when a refinement changes which bytes parse; PATCH for documentation-only
|
|
79
|
+
* changes that leave every parsed byte identical.
|
|
80
|
+
*
|
|
81
|
+
* The last two are MINOR rather than PATCH because both produce the same
|
|
82
|
+
* failure: a producer on the newer set emits something the older set refuses,
|
|
83
|
+
* with no `schemaVersion` difference to explain it. A new enum member and a
|
|
84
|
+
* loosened refinement are exactly what the handshake exists to surface — a
|
|
85
|
+
* skew the per-message version cannot express.
|
|
79
86
|
*
|
|
80
87
|
* This constant is deliberately NOT embedded in the request envelope. Pinning a
|
|
81
88
|
* request to the version of the whole set would make an unrelated additive
|
|
82
89
|
* change to, say, the catalogue reject every in-flight inference request; the
|
|
83
90
|
* per-shape `schemaVersion` is what a message is validated against.
|
|
84
91
|
*/
|
|
85
|
-
exports.INFERENCE_CONTRACT_VERSION = '1.
|
|
92
|
+
exports.INFERENCE_CONTRACT_VERSION = '1.1.0';
|