@kaana.ai/contracts 0.0.0-stage → 4.3.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/LICENSE +673 -0
- package/NOTICE +21 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +29 -0
- package/dist/internal/attribution.d.ts +116 -0
- package/dist/internal/attribution.js +82 -0
- package/dist/internal/canonical.d.ts +31 -0
- package/dist/internal/canonical.js +57 -0
- package/dist/internal/catalogue.d.ts +1030 -0
- package/dist/internal/catalogue.js +349 -0
- package/dist/internal/credentialControl.d.ts +329 -0
- package/dist/internal/credentialControl.js +256 -0
- package/dist/internal/decisions.d.ts +245 -0
- package/dist/internal/decisions.js +69 -0
- package/dist/internal/economics.d.ts +27 -0
- package/dist/internal/economics.js +26 -0
- package/dist/internal/embeddings.d.ts +164 -0
- package/dist/internal/embeddings.js +59 -0
- package/dist/internal/errors.d.ts +185 -0
- package/dist/internal/errors.js +211 -0
- package/dist/internal/grant.d.ts +696 -0
- package/dist/internal/grant.js +437 -0
- package/dist/internal/identifiers.d.ts +63 -0
- package/dist/internal/identifiers.js +65 -0
- package/dist/internal/privateAutoExecution.d.ts +158 -0
- package/dist/internal/privateAutoExecution.js +188 -0
- package/dist/internal/realtime.d.ts +1005 -0
- package/dist/internal/realtime.js +187 -0
- package/dist/internal/request.d.ts +1079 -0
- package/dist/internal/request.js +404 -0
- package/dist/internal/routing.d.ts +90 -0
- package/dist/internal/routing.js +139 -0
- package/dist/internal/scopedExecution.d.ts +74 -0
- package/dist/internal/scopedExecution.js +61 -0
- package/dist/internal/settlement.d.ts +205 -0
- package/dist/internal/settlement.js +71 -0
- package/dist/internal/streamEvents.d.ts +422 -0
- package/dist/internal/streamEvents.js +171 -0
- package/dist/internal/usage.d.ts +127 -0
- package/dist/internal/usage.js +109 -0
- package/dist/internal.d.ts +30 -0
- package/dist/internal.js +30 -0
- package/dist/public/audio.d.ts +347 -0
- package/dist/public/audio.js +236 -0
- package/dist/public/audioFormats.d.ts +64 -0
- package/dist/public/audioFormats.js +60 -0
- package/dist/public/catalogue.d.ts +914 -0
- package/dist/public/catalogue.js +516 -0
- package/dist/public/chatCompletions.d.ts +381 -0
- package/dist/public/chatCompletions.js +321 -0
- package/dist/public/decisions.d.ts +182 -0
- package/dist/public/decisions.js +257 -0
- package/dist/public/embeddings.d.ts +34 -0
- package/dist/public/embeddings.js +66 -0
- package/dist/public/errors.d.ts +195 -0
- package/dist/public/errors.js +274 -0
- package/dist/public/generations.d.ts +330 -0
- package/dist/public/generations.js +121 -0
- package/dist/public/identifiers.d.ts +139 -0
- package/dist/public/identifiers.js +160 -0
- package/dist/public/images.d.ts +36 -0
- package/dist/public/images.js +36 -0
- package/dist/public/messages.d.ts +297 -0
- package/dist/public/messages.js +251 -0
- package/dist/public/money.d.ts +308 -0
- package/dist/public/money.js +237 -0
- package/dist/public/policy.d.ts +19 -0
- package/dist/public/policy.js +20 -0
- package/dist/public/realtime.d.ts +2006 -0
- package/dist/public/realtime.js +797 -0
- package/dist/public/responses.d.ts +253 -0
- package/dist/public/responses.js +95 -0
- package/dist/public/streamEvents.d.ts +372 -0
- package/dist/public/streamEvents.js +157 -0
- package/dist/public/version.d.ts +177 -0
- package/dist/public/version.js +177 -0
- package/openapi.json +6967 -0
- package/package.json +44 -4
- package/README.md +0 -3
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Errors of the public API.
|
|
3
|
+
*
|
|
4
|
+
* Moved from `@oxy.so/contracts` 4.12.0 (`src/inference/errors.ts`) by Kaana ADR 0002.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
/**
|
|
8
|
+
* Free text that is safe to hand a customer: bounded, and refused if it still
|
|
9
|
+
* looks like it carries a credential. Applied to BOTH the Oxy message and the
|
|
10
|
+
* upstream one — a leak is no less a leak for having been written by a provider.
|
|
11
|
+
*
|
|
12
|
+
* ## This is a last-resort REFUSAL, not protection
|
|
13
|
+
*
|
|
14
|
+
* A pattern over the OUTPUT cannot be the control that keeps a credential out of
|
|
15
|
+
* an error, and a producer that treats it as one has the hole #1027 reported.
|
|
16
|
+
* The only reliable control is redacting the KNOWN SECRET VALUE at the point
|
|
17
|
+
* where the producer still holds the bytes it sent — which is an adapter's job
|
|
18
|
+
* and is available to nobody else. This refinement exists to catch what that
|
|
19
|
+
* control missed, and nothing here is a licence to skip it.
|
|
20
|
+
*
|
|
21
|
+
* Two rules follow, and they are the whole reason this text is longer than the
|
|
22
|
+
* pattern it describes:
|
|
23
|
+
*
|
|
24
|
+
* - **Never redact by replacing the span this pattern matched.** The span is
|
|
25
|
+
* the MARKER; the secret is what follows it. OxyHQ/Kaana#3 measured the
|
|
26
|
+
* result: `{x-api-key: <key>}` is refused, `{x-[redacted] <key>}` was
|
|
27
|
+
* accepted, and both carry the key. Redaction made the leak worse by
|
|
28
|
+
* converting "this string is dangerous" into "this string is fine".
|
|
29
|
+
* - **This package deliberately ships no redaction helper.** One keyed on these
|
|
30
|
+
* patterns would rebuild the same defect one layer up, and one that took the
|
|
31
|
+
* secret as an argument would only restate what the producer already has.
|
|
32
|
+
*
|
|
33
|
+
* What it still cannot see, stated so nobody relies on it: a credential with no
|
|
34
|
+
* marker, no issued-token prefix and no placeholder beside it is bytes that look
|
|
35
|
+
* like a request id, and refusing those means refusing request ids.
|
|
36
|
+
*/
|
|
37
|
+
export declare const safeErrorTextSchema: z.ZodString;
|
|
38
|
+
/**
|
|
39
|
+
* A coarse classification of an upstream failure (ADR 0010's `upstreamCategory`).
|
|
40
|
+
*
|
|
41
|
+
* Distinct from the upstream's OWN code and text, which only the signed
|
|
42
|
+
* Oxy↔Kaana lane carries (`providerErrorPassthroughSchema` in `/internal`): this
|
|
43
|
+
* is the platform's reading of what kind of failure it
|
|
44
|
+
* was, in a vocabulary that is the same across every provider, so a client can
|
|
45
|
+
* branch on it without knowing who served the request.
|
|
46
|
+
*/
|
|
47
|
+
export declare const upstreamErrorCategorySchema: z.ZodEnum<{
|
|
48
|
+
rate_limit: "rate_limit";
|
|
49
|
+
quota: "quota";
|
|
50
|
+
timeout: "timeout";
|
|
51
|
+
overloaded: "overloaded";
|
|
52
|
+
server_error: "server_error";
|
|
53
|
+
content_filter: "content_filter";
|
|
54
|
+
invalid_request: "invalid_request";
|
|
55
|
+
authentication: "authentication";
|
|
56
|
+
unknown: "unknown";
|
|
57
|
+
}>;
|
|
58
|
+
export type UpstreamErrorCategory = z.infer<typeof upstreamErrorCategorySchema>;
|
|
59
|
+
/**
|
|
60
|
+
* The error codes a public client of `https://kaana.ai/v1` can receive.
|
|
61
|
+
*
|
|
62
|
+
* The internal vocabulary (`INFERENCE_ERROR_CODES` in `@kaana.ai/contracts/internal`)
|
|
63
|
+
* minus the three codes that describe Kaana's OWN relationship with a provider:
|
|
64
|
+
* `provider_credential_invalid`, `provider_billing_refused` and
|
|
65
|
+
* `deployment_unavailable`. On `/v1` Kaana retires that key or route and moves
|
|
66
|
+
* on; when nothing is left the client is told `service_unavailable` (retryable)
|
|
67
|
+
* or `no_route_available`, never which provider, key or deployment failed.
|
|
68
|
+
*
|
|
69
|
+
* `byok_credential_invalid` stays: it is the customer's own key, and only the
|
|
70
|
+
* customer can replace it. The provider-NEUTRAL upstream codes
|
|
71
|
+
* (`provider_error`, `provider_timeout`, `provider_overloaded`) stay too: they
|
|
72
|
+
* say what kind of failure happened, not who failed.
|
|
73
|
+
*/
|
|
74
|
+
export declare const PUBLIC_ERROR_CODES: readonly ["invalid_request", "authentication_failed", "permission_denied", "insufficient_scope", "model_not_found", "unsupported_modality", "context_length_exceeded", "request_too_large", "output_limit_exceeded", "idempotency_conflict", "insufficient_balance", "spending_limit_exceeded", "quota_exceeded", "byok_credential_invalid", "policy_violation", "commercial_permission_denied", "no_route_available", "upstream_content_filtered", "cancelled", "rate_limited", "provider_error", "provider_timeout", "provider_overloaded", "service_unavailable", "internal_error"];
|
|
75
|
+
export declare const publicErrorCodeSchema: z.ZodEnum<{
|
|
76
|
+
invalid_request: "invalid_request";
|
|
77
|
+
authentication_failed: "authentication_failed";
|
|
78
|
+
permission_denied: "permission_denied";
|
|
79
|
+
insufficient_scope: "insufficient_scope";
|
|
80
|
+
model_not_found: "model_not_found";
|
|
81
|
+
unsupported_modality: "unsupported_modality";
|
|
82
|
+
context_length_exceeded: "context_length_exceeded";
|
|
83
|
+
request_too_large: "request_too_large";
|
|
84
|
+
output_limit_exceeded: "output_limit_exceeded";
|
|
85
|
+
idempotency_conflict: "idempotency_conflict";
|
|
86
|
+
insufficient_balance: "insufficient_balance";
|
|
87
|
+
spending_limit_exceeded: "spending_limit_exceeded";
|
|
88
|
+
quota_exceeded: "quota_exceeded";
|
|
89
|
+
byok_credential_invalid: "byok_credential_invalid";
|
|
90
|
+
policy_violation: "policy_violation";
|
|
91
|
+
commercial_permission_denied: "commercial_permission_denied";
|
|
92
|
+
no_route_available: "no_route_available";
|
|
93
|
+
upstream_content_filtered: "upstream_content_filtered";
|
|
94
|
+
cancelled: "cancelled";
|
|
95
|
+
rate_limited: "rate_limited";
|
|
96
|
+
provider_error: "provider_error";
|
|
97
|
+
provider_timeout: "provider_timeout";
|
|
98
|
+
provider_overloaded: "provider_overloaded";
|
|
99
|
+
service_unavailable: "service_unavailable";
|
|
100
|
+
internal_error: "internal_error";
|
|
101
|
+
}>;
|
|
102
|
+
/** Public codes for which an identical retried request cannot succeed. */
|
|
103
|
+
export declare const NON_RETRYABLE_PUBLIC_ERROR_CODES: readonly ["invalid_request", "authentication_failed", "permission_denied", "insufficient_scope", "model_not_found", "unsupported_modality", "context_length_exceeded", "request_too_large", "output_limit_exceeded", "idempotency_conflict", "insufficient_balance", "spending_limit_exceeded", "quota_exceeded", "byok_credential_invalid", "policy_violation", "commercial_permission_denied", "no_route_available", "upstream_content_filtered", "cancelled"];
|
|
104
|
+
/**
|
|
105
|
+
* The HTTP status each public code is answered with when it is decided before
|
|
106
|
+
* the first byte. After the first byte every failure is the terminal `error`
|
|
107
|
+
* event of the stream, and the status that was already sent stands.
|
|
108
|
+
*
|
|
109
|
+
* `cancelled` is 499, nginx's spelling for a client that went away.
|
|
110
|
+
*/
|
|
111
|
+
export declare const PUBLIC_ERROR_HTTP_STATUS: {
|
|
112
|
+
readonly invalid_request: 400;
|
|
113
|
+
readonly authentication_failed: 401;
|
|
114
|
+
readonly permission_denied: 403;
|
|
115
|
+
readonly insufficient_scope: 403;
|
|
116
|
+
readonly model_not_found: 404;
|
|
117
|
+
readonly unsupported_modality: 400;
|
|
118
|
+
readonly context_length_exceeded: 400;
|
|
119
|
+
readonly request_too_large: 413;
|
|
120
|
+
readonly output_limit_exceeded: 400;
|
|
121
|
+
readonly idempotency_conflict: 409;
|
|
122
|
+
readonly insufficient_balance: 402;
|
|
123
|
+
readonly spending_limit_exceeded: 402;
|
|
124
|
+
readonly quota_exceeded: 429;
|
|
125
|
+
readonly byok_credential_invalid: 400;
|
|
126
|
+
readonly policy_violation: 403;
|
|
127
|
+
readonly commercial_permission_denied: 403;
|
|
128
|
+
readonly no_route_available: 503;
|
|
129
|
+
readonly upstream_content_filtered: 400;
|
|
130
|
+
readonly cancelled: 499;
|
|
131
|
+
readonly rate_limited: 429;
|
|
132
|
+
readonly provider_error: 502;
|
|
133
|
+
readonly provider_timeout: 504;
|
|
134
|
+
readonly provider_overloaded: 503;
|
|
135
|
+
readonly service_unavailable: 503;
|
|
136
|
+
readonly internal_error: 500;
|
|
137
|
+
};
|
|
138
|
+
/**
|
|
139
|
+
* The error body every `/v1` surface returns and every public stream's
|
|
140
|
+
* terminal `error` event carries.
|
|
141
|
+
*
|
|
142
|
+
* The internal error shape without its provider passthrough: a public error
|
|
143
|
+
* names what went wrong and whether to retry, never who served the request.
|
|
144
|
+
* Accounts, keys and balances are managed in Oxy Console
|
|
145
|
+
* (`https://console.oxy.so`), which an `authentication_failed` or balance
|
|
146
|
+
* message links to.
|
|
147
|
+
*/
|
|
148
|
+
export declare const publicErrorSchema: z.ZodObject<{
|
|
149
|
+
schemaVersion: z.ZodLiteral<1>;
|
|
150
|
+
code: z.ZodEnum<{
|
|
151
|
+
invalid_request: "invalid_request";
|
|
152
|
+
authentication_failed: "authentication_failed";
|
|
153
|
+
permission_denied: "permission_denied";
|
|
154
|
+
insufficient_scope: "insufficient_scope";
|
|
155
|
+
model_not_found: "model_not_found";
|
|
156
|
+
unsupported_modality: "unsupported_modality";
|
|
157
|
+
context_length_exceeded: "context_length_exceeded";
|
|
158
|
+
request_too_large: "request_too_large";
|
|
159
|
+
output_limit_exceeded: "output_limit_exceeded";
|
|
160
|
+
idempotency_conflict: "idempotency_conflict";
|
|
161
|
+
insufficient_balance: "insufficient_balance";
|
|
162
|
+
spending_limit_exceeded: "spending_limit_exceeded";
|
|
163
|
+
quota_exceeded: "quota_exceeded";
|
|
164
|
+
byok_credential_invalid: "byok_credential_invalid";
|
|
165
|
+
policy_violation: "policy_violation";
|
|
166
|
+
commercial_permission_denied: "commercial_permission_denied";
|
|
167
|
+
no_route_available: "no_route_available";
|
|
168
|
+
upstream_content_filtered: "upstream_content_filtered";
|
|
169
|
+
cancelled: "cancelled";
|
|
170
|
+
rate_limited: "rate_limited";
|
|
171
|
+
provider_error: "provider_error";
|
|
172
|
+
provider_timeout: "provider_timeout";
|
|
173
|
+
provider_overloaded: "provider_overloaded";
|
|
174
|
+
service_unavailable: "service_unavailable";
|
|
175
|
+
internal_error: "internal_error";
|
|
176
|
+
}>;
|
|
177
|
+
message: z.ZodString;
|
|
178
|
+
retryable: z.ZodBoolean;
|
|
179
|
+
requestId: z.ZodString;
|
|
180
|
+
retryAfterMs: z.ZodOptional<z.ZodNumber>;
|
|
181
|
+
param: z.ZodOptional<z.ZodString>;
|
|
182
|
+
upstreamCategory: z.ZodOptional<z.ZodEnum<{
|
|
183
|
+
rate_limit: "rate_limit";
|
|
184
|
+
quota: "quota";
|
|
185
|
+
timeout: "timeout";
|
|
186
|
+
overloaded: "overloaded";
|
|
187
|
+
server_error: "server_error";
|
|
188
|
+
content_filter: "content_filter";
|
|
189
|
+
invalid_request: "invalid_request";
|
|
190
|
+
authentication: "authentication";
|
|
191
|
+
unknown: "unknown";
|
|
192
|
+
}>>;
|
|
193
|
+
}, z.core.$strict>;
|
|
194
|
+
export type PublicErrorCode = z.infer<typeof publicErrorCodeSchema>;
|
|
195
|
+
export type PublicError = z.infer<typeof publicErrorSchema>;
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Errors of the public API.
|
|
3
|
+
*
|
|
4
|
+
* Moved from `@oxy.so/contracts` 4.12.0 (`src/inference/errors.ts`) by Kaana ADR 0002.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { requestIdSchema } from './identifiers.js';
|
|
8
|
+
/**
|
|
9
|
+
* A run of characters long enough and opaque enough to BE a credential.
|
|
10
|
+
*
|
|
11
|
+
* The alphabet every bearer token, API key and base64/base64url secret is
|
|
12
|
+
* written in. The LENGTH floors below are what keep this from being an entropy
|
|
13
|
+
* heuristic: nothing here fires on a short word, so `authorization: none` and
|
|
14
|
+
* `api_key=***` read as what they are.
|
|
15
|
+
*/
|
|
16
|
+
const OPAQUE_ALPHABET = '[A-Za-z0-9][A-Za-z0-9._~+/=-]';
|
|
17
|
+
/**
|
|
18
|
+
* Words a producer substitutes FOR a credential.
|
|
19
|
+
*
|
|
20
|
+
* Excluded at the value position so a message whose secret has already been
|
|
21
|
+
* replaced is accepted. That acceptance is deliberate and is half the fix for
|
|
22
|
+
* issue #1027: the previous pattern refused `Authorization: [redacted]` — a
|
|
23
|
+
* correctly redacted string — which is precisely what pushed a producer into
|
|
24
|
+
* redacting the MARKER instead, and a marker-redacted string carries the secret
|
|
25
|
+
* and passes.
|
|
26
|
+
*/
|
|
27
|
+
const PLACEHOLDER_WORDS = 'redacted|removed|hidden|masked|scrubbed|elided|omitted|filtered|sanitized|sanitised|none|null|undefined|empty';
|
|
28
|
+
/** A value position whose contents are a placeholder rather than a secret. */
|
|
29
|
+
const NOT_A_PLACEHOLDER = `(?!(?:${PLACEHOLDER_WORDS})\\b)`;
|
|
30
|
+
/**
|
|
31
|
+
* Header and parameter names that carry a credential, as any provider spells
|
|
32
|
+
* them.
|
|
33
|
+
*
|
|
34
|
+
* The prefix group is the whole point of the rewrite: `authorization` and
|
|
35
|
+
* `api_key` were matched literally, so `x-api-key`, `anthropic-api-key`,
|
|
36
|
+
* `x-goog-api-key` and `proxy-authorization` — the spellings an upstream
|
|
37
|
+
* actually echoes — went unrecognised.
|
|
38
|
+
*/
|
|
39
|
+
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?)';
|
|
40
|
+
/** An auth scheme sitting between the marker and the value. */
|
|
41
|
+
const AUTH_SCHEME = '(?:(?:bearer|basic|token|apikey|digest)\\s+)?';
|
|
42
|
+
/**
|
|
43
|
+
* The four ways a credential is recognisable in free text.
|
|
44
|
+
*
|
|
45
|
+
* Each is checked independently, so removing one signal does not clear the
|
|
46
|
+
* string — which is the failure #1027 reported. All four are load-bearing:
|
|
47
|
+
* `inference.errors.test.ts` has a case that only one of them catches, and
|
|
48
|
+
* deleting any one entry turns a test red.
|
|
49
|
+
*/
|
|
50
|
+
const CREDENTIAL_PATTERNS = [
|
|
51
|
+
// 1. A credential-bearing name ASSIGNED a value that is long enough to be a
|
|
52
|
+
// credential. The value is anchored to the separator so a placeholder at
|
|
53
|
+
// that position ends the match rather than being skipped over.
|
|
54
|
+
new RegExp(`(?:^|[^a-z0-9])${CREDENTIAL_NAME}["']?\\s*[:=]\\s*["']?${AUTH_SCHEME}${NOT_A_PLACEHOLDER}${OPAQUE_ALPHABET}{7,}`, 'i'),
|
|
55
|
+
// 2. A bearer token with no marker in front of it, which is how an upstream
|
|
56
|
+
// quotes the header value alone.
|
|
57
|
+
new RegExp(`\\bbearer\\s+${NOT_A_PLACEHOLDER}${OPAQUE_ALPHABET}{7,}`, 'i'),
|
|
58
|
+
// 3. Token grammars that ARE credentials wherever they appear, marker or not.
|
|
59
|
+
// This is the layer that survives a producer stripping the marker, and it
|
|
60
|
+
// is a closed list of issued shapes rather than an entropy score, so a
|
|
61
|
+
// request id or a base64 image fragment is unaffected.
|
|
62
|
+
//
|
|
63
|
+
// Case-SENSITIVE on purpose: `AKIA`, `AIza` and `gh[pousr]_` are issued in
|
|
64
|
+
// exactly that case, and matching them case-insensitively would start
|
|
65
|
+
// firing on ordinary words.
|
|
66
|
+
/\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,})/,
|
|
67
|
+
// 4. A redaction placeholder standing NEXT TO a surviving opaque value — the
|
|
68
|
+
// exact residue of the span redaction in #1027 (`{x-[redacted] <key>}`).
|
|
69
|
+
// A correct redaction puts the placeholder WHERE the value was, so the two
|
|
70
|
+
// never appear side by side; a marker-span redaction leaves them adjacent.
|
|
71
|
+
// Both signals are required, which is what keeps an ordinary redacted
|
|
72
|
+
// message from being refused.
|
|
73
|
+
new RegExp(`(?:[[<({]\\s*(?:${PLACEHOLDER_WORDS})[^\\])}>]{0,16}[\\])}>]|\\*{3,})[^A-Za-z0-9]{0,4}${OPAQUE_ALPHABET}{11,}`, 'i'),
|
|
74
|
+
];
|
|
75
|
+
/**
|
|
76
|
+
* Free text that is safe to hand a customer: bounded, and refused if it still
|
|
77
|
+
* looks like it carries a credential. Applied to BOTH the Oxy message and the
|
|
78
|
+
* upstream one — a leak is no less a leak for having been written by a provider.
|
|
79
|
+
*
|
|
80
|
+
* ## This is a last-resort REFUSAL, not protection
|
|
81
|
+
*
|
|
82
|
+
* A pattern over the OUTPUT cannot be the control that keeps a credential out of
|
|
83
|
+
* an error, and a producer that treats it as one has the hole #1027 reported.
|
|
84
|
+
* The only reliable control is redacting the KNOWN SECRET VALUE at the point
|
|
85
|
+
* where the producer still holds the bytes it sent — which is an adapter's job
|
|
86
|
+
* and is available to nobody else. This refinement exists to catch what that
|
|
87
|
+
* control missed, and nothing here is a licence to skip it.
|
|
88
|
+
*
|
|
89
|
+
* Two rules follow, and they are the whole reason this text is longer than the
|
|
90
|
+
* pattern it describes:
|
|
91
|
+
*
|
|
92
|
+
* - **Never redact by replacing the span this pattern matched.** The span is
|
|
93
|
+
* the MARKER; the secret is what follows it. OxyHQ/Kaana#3 measured the
|
|
94
|
+
* result: `{x-api-key: <key>}` is refused, `{x-[redacted] <key>}` was
|
|
95
|
+
* accepted, and both carry the key. Redaction made the leak worse by
|
|
96
|
+
* converting "this string is dangerous" into "this string is fine".
|
|
97
|
+
* - **This package deliberately ships no redaction helper.** One keyed on these
|
|
98
|
+
* patterns would rebuild the same defect one layer up, and one that took the
|
|
99
|
+
* secret as an argument would only restate what the producer already has.
|
|
100
|
+
*
|
|
101
|
+
* What it still cannot see, stated so nobody relies on it: a credential with no
|
|
102
|
+
* marker, no issued-token prefix and no placeholder beside it is bytes that look
|
|
103
|
+
* like a request id, and refusing those means refusing request ids.
|
|
104
|
+
*/
|
|
105
|
+
export const safeErrorTextSchema = z
|
|
106
|
+
.string()
|
|
107
|
+
.min(1)
|
|
108
|
+
.max(2000)
|
|
109
|
+
.refine((value) => !CREDENTIAL_PATTERNS.some((pattern) => pattern.test(value)), 'error text must not contain credential-shaped material');
|
|
110
|
+
/**
|
|
111
|
+
* A coarse classification of an upstream failure (ADR 0010's `upstreamCategory`).
|
|
112
|
+
*
|
|
113
|
+
* Distinct from the upstream's OWN code and text, which only the signed
|
|
114
|
+
* Oxy↔Kaana lane carries (`providerErrorPassthroughSchema` in `/internal`): this
|
|
115
|
+
* is the platform's reading of what kind of failure it
|
|
116
|
+
* was, in a vocabulary that is the same across every provider, so a client can
|
|
117
|
+
* branch on it without knowing who served the request.
|
|
118
|
+
*/
|
|
119
|
+
export const upstreamErrorCategorySchema = z.enum([
|
|
120
|
+
'rate_limit',
|
|
121
|
+
'quota',
|
|
122
|
+
'timeout',
|
|
123
|
+
'overloaded',
|
|
124
|
+
'server_error',
|
|
125
|
+
'content_filter',
|
|
126
|
+
'invalid_request',
|
|
127
|
+
'authentication',
|
|
128
|
+
'unknown',
|
|
129
|
+
]);
|
|
130
|
+
/* -------------------------------------------------------------------------- */
|
|
131
|
+
/* The public vocabulary (Kaana ADR 0002, decision 10) */
|
|
132
|
+
/* -------------------------------------------------------------------------- */
|
|
133
|
+
/**
|
|
134
|
+
* The error codes a public client of `https://kaana.ai/v1` can receive.
|
|
135
|
+
*
|
|
136
|
+
* The internal vocabulary (`INFERENCE_ERROR_CODES` in `@kaana.ai/contracts/internal`)
|
|
137
|
+
* minus the three codes that describe Kaana's OWN relationship with a provider:
|
|
138
|
+
* `provider_credential_invalid`, `provider_billing_refused` and
|
|
139
|
+
* `deployment_unavailable`. On `/v1` Kaana retires that key or route and moves
|
|
140
|
+
* on; when nothing is left the client is told `service_unavailable` (retryable)
|
|
141
|
+
* or `no_route_available`, never which provider, key or deployment failed.
|
|
142
|
+
*
|
|
143
|
+
* `byok_credential_invalid` stays: it is the customer's own key, and only the
|
|
144
|
+
* customer can replace it. The provider-NEUTRAL upstream codes
|
|
145
|
+
* (`provider_error`, `provider_timeout`, `provider_overloaded`) stay too: they
|
|
146
|
+
* say what kind of failure happened, not who failed.
|
|
147
|
+
*/
|
|
148
|
+
export const PUBLIC_ERROR_CODES = [
|
|
149
|
+
'invalid_request',
|
|
150
|
+
'authentication_failed',
|
|
151
|
+
'permission_denied',
|
|
152
|
+
'insufficient_scope',
|
|
153
|
+
'model_not_found',
|
|
154
|
+
'unsupported_modality',
|
|
155
|
+
'context_length_exceeded',
|
|
156
|
+
'request_too_large',
|
|
157
|
+
'output_limit_exceeded',
|
|
158
|
+
'idempotency_conflict',
|
|
159
|
+
'insufficient_balance',
|
|
160
|
+
'spending_limit_exceeded',
|
|
161
|
+
'quota_exceeded',
|
|
162
|
+
'byok_credential_invalid',
|
|
163
|
+
'policy_violation',
|
|
164
|
+
'commercial_permission_denied',
|
|
165
|
+
'no_route_available',
|
|
166
|
+
'upstream_content_filtered',
|
|
167
|
+
'cancelled',
|
|
168
|
+
'rate_limited',
|
|
169
|
+
'provider_error',
|
|
170
|
+
'provider_timeout',
|
|
171
|
+
'provider_overloaded',
|
|
172
|
+
'service_unavailable',
|
|
173
|
+
'internal_error',
|
|
174
|
+
];
|
|
175
|
+
export const publicErrorCodeSchema = z.enum(PUBLIC_ERROR_CODES);
|
|
176
|
+
/** Public codes for which an identical retried request cannot succeed. */
|
|
177
|
+
export const NON_RETRYABLE_PUBLIC_ERROR_CODES = [
|
|
178
|
+
'invalid_request',
|
|
179
|
+
'authentication_failed',
|
|
180
|
+
'permission_denied',
|
|
181
|
+
'insufficient_scope',
|
|
182
|
+
'model_not_found',
|
|
183
|
+
'unsupported_modality',
|
|
184
|
+
'context_length_exceeded',
|
|
185
|
+
'request_too_large',
|
|
186
|
+
'output_limit_exceeded',
|
|
187
|
+
'idempotency_conflict',
|
|
188
|
+
'insufficient_balance',
|
|
189
|
+
'spending_limit_exceeded',
|
|
190
|
+
'quota_exceeded',
|
|
191
|
+
'byok_credential_invalid',
|
|
192
|
+
'policy_violation',
|
|
193
|
+
'commercial_permission_denied',
|
|
194
|
+
'no_route_available',
|
|
195
|
+
'upstream_content_filtered',
|
|
196
|
+
'cancelled',
|
|
197
|
+
];
|
|
198
|
+
const NON_RETRYABLE_PUBLIC_CODE_SET = new Set(NON_RETRYABLE_PUBLIC_ERROR_CODES);
|
|
199
|
+
/**
|
|
200
|
+
* The HTTP status each public code is answered with when it is decided before
|
|
201
|
+
* the first byte. After the first byte every failure is the terminal `error`
|
|
202
|
+
* event of the stream, and the status that was already sent stands.
|
|
203
|
+
*
|
|
204
|
+
* `cancelled` is 499, nginx's spelling for a client that went away.
|
|
205
|
+
*/
|
|
206
|
+
export const PUBLIC_ERROR_HTTP_STATUS = {
|
|
207
|
+
invalid_request: 400,
|
|
208
|
+
authentication_failed: 401,
|
|
209
|
+
permission_denied: 403,
|
|
210
|
+
insufficient_scope: 403,
|
|
211
|
+
model_not_found: 404,
|
|
212
|
+
unsupported_modality: 400,
|
|
213
|
+
context_length_exceeded: 400,
|
|
214
|
+
request_too_large: 413,
|
|
215
|
+
output_limit_exceeded: 400,
|
|
216
|
+
idempotency_conflict: 409,
|
|
217
|
+
insufficient_balance: 402,
|
|
218
|
+
spending_limit_exceeded: 402,
|
|
219
|
+
quota_exceeded: 429,
|
|
220
|
+
byok_credential_invalid: 400,
|
|
221
|
+
policy_violation: 403,
|
|
222
|
+
commercial_permission_denied: 403,
|
|
223
|
+
no_route_available: 503,
|
|
224
|
+
upstream_content_filtered: 400,
|
|
225
|
+
cancelled: 499,
|
|
226
|
+
rate_limited: 429,
|
|
227
|
+
provider_error: 502,
|
|
228
|
+
provider_timeout: 504,
|
|
229
|
+
provider_overloaded: 503,
|
|
230
|
+
service_unavailable: 503,
|
|
231
|
+
internal_error: 500,
|
|
232
|
+
};
|
|
233
|
+
/**
|
|
234
|
+
* The error body every `/v1` surface returns and every public stream's
|
|
235
|
+
* terminal `error` event carries.
|
|
236
|
+
*
|
|
237
|
+
* The internal error shape without its provider passthrough: a public error
|
|
238
|
+
* names what went wrong and whether to retry, never who served the request.
|
|
239
|
+
* Accounts, keys and balances are managed in Oxy Console
|
|
240
|
+
* (`https://console.oxy.so`), which an `authentication_failed` or balance
|
|
241
|
+
* message links to.
|
|
242
|
+
*/
|
|
243
|
+
export const publicErrorSchema = z
|
|
244
|
+
.object({
|
|
245
|
+
/** A whole message on the wire, so it is versioned. */
|
|
246
|
+
schemaVersion: z.literal(1),
|
|
247
|
+
code: publicErrorCodeSchema,
|
|
248
|
+
message: safeErrorTextSchema,
|
|
249
|
+
retryable: z.boolean(),
|
|
250
|
+
requestId: requestIdSchema,
|
|
251
|
+
/** How long to wait before retrying. Only meaningful when `retryable`. */
|
|
252
|
+
retryAfterMs: z.number().int().nonnegative().safe().optional(),
|
|
253
|
+
/** The request field at fault, for `invalid_request`. */
|
|
254
|
+
param: z.string().max(128).optional(),
|
|
255
|
+
/** The kind of upstream failure, when an upstream was reached. Provider-neutral. */
|
|
256
|
+
upstreamCategory: upstreamErrorCategorySchema.optional(),
|
|
257
|
+
})
|
|
258
|
+
.strict()
|
|
259
|
+
.superRefine((error, ctx) => {
|
|
260
|
+
if (error.retryable && NON_RETRYABLE_PUBLIC_CODE_SET.has(error.code)) {
|
|
261
|
+
ctx.addIssue({
|
|
262
|
+
code: 'custom',
|
|
263
|
+
path: ['retryable'],
|
|
264
|
+
message: `${error.code} can never succeed on an identical retry, so it cannot be retryable`,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
if (error.retryAfterMs !== undefined && !error.retryable) {
|
|
268
|
+
ctx.addIssue({
|
|
269
|
+
code: 'custom',
|
|
270
|
+
path: ['retryAfterMs'],
|
|
271
|
+
message: 'retryAfterMs tells a client when to retry, so it requires retryable: true',
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
});
|