@providerkit/core 0.1.0 → 0.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/README.md +35 -158
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +8 -0
- package/dist/context.js.map +1 -1
- package/dist/errors.d.ts +36 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +75 -2
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/key-pool.d.ts +60 -0
- package/dist/key-pool.d.ts.map +1 -0
- package/dist/key-pool.js +235 -0
- package/dist/key-pool.js.map +1 -0
- package/dist/providers/anthropic.d.ts +10 -6
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +30 -14
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/gemini.d.ts +40 -0
- package/dist/providers/gemini.d.ts.map +1 -0
- package/dist/providers/gemini.js +303 -0
- package/dist/providers/gemini.js.map +1 -0
- package/dist/providers/openai.d.ts +38 -1
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +122 -16
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/responses.d.ts +38 -0
- package/dist/providers/responses.d.ts.map +1 -0
- package/dist/providers/responses.js +341 -0
- package/dist/providers/responses.js.map +1 -0
- package/dist/rate-limit.d.ts +29 -0
- package/dist/rate-limit.d.ts.map +1 -0
- package/dist/rate-limit.js +194 -0
- package/dist/rate-limit.js.map +1 -0
- package/dist/transport.d.ts +18 -5
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +61 -35
- package/dist/transport.js.map +1 -1
- package/dist/types.d.ts +13 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +7 -2
- package/dist/types.js.map +1 -1
- package/dist/zod.d.ts +8 -1
- package/dist/zod.d.ts.map +1 -1
- package/dist/zod.js +9 -2
- package/dist/zod.js.map +1 -1
- package/package.json +2 -2
- package/src/context.ts +7 -0
- package/src/errors.ts +82 -2
- package/src/index.ts +4 -0
- package/src/key-pool.ts +272 -0
- package/src/providers/anthropic.ts +41 -20
- package/src/providers/gemini.ts +386 -0
- package/src/providers/openai.ts +153 -16
- package/src/providers/responses.ts +455 -0
- package/src/rate-limit.ts +217 -0
- package/src/transport.ts +61 -35
- package/src/types.ts +19 -2
- package/src/zod.ts +12 -2
package/src/key-pool.ts
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
// A rotating pool of API keys for one provider.
|
|
2
|
+
//
|
|
3
|
+
// Rate limits are scoped per project or per account, so several keys multiply
|
|
4
|
+
// throughput. Free-tier keys go first, round-robin, and the paid key last —
|
|
5
|
+
// free quota burns before money. A key that answers 429 is benched for the
|
|
6
|
+
// delay the provider named (Retry-After, or Gemini's RetryInfo; else a minute,
|
|
7
|
+
// and an hour when the body says the balance or the daily window is gone), a
|
|
8
|
+
// denied key for 12 h (durable, but key-specific — one dead key must not take
|
|
9
|
+
// the pool down with it), an overload only briefly, because a sibling project's
|
|
10
|
+
// key routes to a different backend.
|
|
11
|
+
//
|
|
12
|
+
// With ONE key there is nothing to rotate to, so NOTHING is evicted and the
|
|
13
|
+
// error propagates untouched: a transient 503 must not switch a single-key
|
|
14
|
+
// deployment off for a minute, where the caller's own retry is the whole
|
|
15
|
+
// recovery there is.
|
|
16
|
+
import { isTransient, ProviderError } from "./errors.ts";
|
|
17
|
+
import type { ErrorKind } from "./errors.ts";
|
|
18
|
+
import type {
|
|
19
|
+
ChatMessage,
|
|
20
|
+
Provider,
|
|
21
|
+
ProviderChunk,
|
|
22
|
+
StreamOptions,
|
|
23
|
+
ToolDefinition,
|
|
24
|
+
} from "./types.ts";
|
|
25
|
+
|
|
26
|
+
/** A per-minute throttle: the window it names is the next one. */
|
|
27
|
+
const RATE_COOLDOWN_MS = 60_000;
|
|
28
|
+
/** A balance or a daily window — minutes will not bring it back. */
|
|
29
|
+
const QUOTA_COOLDOWN_MS = 60 * 60_000;
|
|
30
|
+
const AUTH_COOLDOWN_MS = 12 * 60 * 60_000;
|
|
31
|
+
/** The vendor's bad time, not the key's — benched only long enough for the
|
|
32
|
+
* next call to land somewhere else. */
|
|
33
|
+
const TRANSIENT_COOLDOWN_MS = 60_000;
|
|
34
|
+
const MIN_EVICTION_MS = 1_000;
|
|
35
|
+
const MAX_EVICTION_MS = 12 * 60 * 60_000;
|
|
36
|
+
|
|
37
|
+
export type KeyTier = "free" | "paid";
|
|
38
|
+
|
|
39
|
+
interface PoolKey {
|
|
40
|
+
apiKey: string;
|
|
41
|
+
tier: KeyTier;
|
|
42
|
+
/** Epoch ms until which the key is out; 0 = available. */
|
|
43
|
+
evictedUntil: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface KeyPoolOptions {
|
|
47
|
+
/** Free-tier keys, walked round-robin before the paid one. */
|
|
48
|
+
keys: readonly string[];
|
|
49
|
+
paidKey?: string | null;
|
|
50
|
+
/** Fires whenever a key is benched — the pool's only report, in place of a
|
|
51
|
+
* logger a zero-dependency package has no business owning. */
|
|
52
|
+
onEvict?: (info: { tier: KeyTier; kind: ErrorKind; forMs: number }) => void;
|
|
53
|
+
/** Injected in tests, so an expiry can be exercised without waiting out a
|
|
54
|
+
* 12-hour cooldown. */
|
|
55
|
+
now?: () => number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** No key can serve right now. `retryAtMs` = when the soonest one is back. */
|
|
59
|
+
export class NoAvailableKeyError extends Error {
|
|
60
|
+
readonly retryAtMs: number;
|
|
61
|
+
|
|
62
|
+
constructor(label: string, retryAtMs: number) {
|
|
63
|
+
super(`No ${label} API key available (all rate-limited or denied)`);
|
|
64
|
+
this.name = "NoAvailableKeyError";
|
|
65
|
+
this.retryAtMs = retryAtMs;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export class KeyPool {
|
|
70
|
+
private readonly label: string;
|
|
71
|
+
private readonly free: PoolKey[];
|
|
72
|
+
private readonly paid: PoolKey | null;
|
|
73
|
+
private readonly onEvict: KeyPoolOptions["onEvict"];
|
|
74
|
+
private readonly now: () => number;
|
|
75
|
+
private cursor = 0;
|
|
76
|
+
|
|
77
|
+
constructor(label: string, opts: KeyPoolOptions) {
|
|
78
|
+
this.label = label;
|
|
79
|
+
// A blank slot in the caller's config is not a key — an absent env var
|
|
80
|
+
// reads as "" and would otherwise be dialled once per rotation.
|
|
81
|
+
this.free = opts.keys
|
|
82
|
+
.filter(Boolean)
|
|
83
|
+
.map((apiKey): PoolKey => ({ apiKey, tier: "free", evictedUntil: 0 }));
|
|
84
|
+
this.paid = opts.paidKey ? { apiKey: opts.paidKey, tier: "paid", evictedUntil: 0 } : null;
|
|
85
|
+
this.onEvict = opts.onEvict;
|
|
86
|
+
this.now = opts.now ?? Date.now;
|
|
87
|
+
if (this.free.length === 0 && !this.paid) {
|
|
88
|
+
throw new Error(`KeyPool(${this.label}) needs at least one key`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
get size(): number {
|
|
93
|
+
return this.free.length + (this.paid ? 1 : 0);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Run `fn` with the next available key, rotating on key-specific and
|
|
98
|
+
* transient failures. Everything else (a bad request, a content block) throws
|
|
99
|
+
* straight through — it would fail identically on every key, and spending the
|
|
100
|
+
* pool on it only turns one bad request into an outage.
|
|
101
|
+
*/
|
|
102
|
+
async with<T>(fn: (apiKey: string, tier: KeyTier) => Promise<T>): Promise<T> {
|
|
103
|
+
const candidates = this.candidates(this.now());
|
|
104
|
+
if (candidates.length === 0) throw new NoAvailableKeyError(this.label, this.nextAvailableAt());
|
|
105
|
+
|
|
106
|
+
let last: unknown;
|
|
107
|
+
for (const key of candidates) {
|
|
108
|
+
try {
|
|
109
|
+
return await fn(key.apiKey, key.tier);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
last = err;
|
|
112
|
+
// The single-key rule: with nothing to rotate to, benching the only key
|
|
113
|
+
// answers NoAvailableKeyError to every call for the next minute — for a
|
|
114
|
+
// failure the caller's own retry would have absorbed.
|
|
115
|
+
const cooldown = this.size > 1 ? cooldownFor(err) : null;
|
|
116
|
+
if (cooldown === null) throw err;
|
|
117
|
+
key.evictedUntil = this.now() + cooldown.forMs;
|
|
118
|
+
this.onEvict?.({ tier: key.tier, kind: cooldown.kind, forMs: cooldown.forMs });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Every candidate was evicted during this call.
|
|
123
|
+
if (this.candidates(this.now()).length === 0)
|
|
124
|
+
throw new NoAvailableKeyError(this.label, this.nextAvailableAt());
|
|
125
|
+
throw last;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Free keys round-robin from a moving cursor, then the paid key. */
|
|
129
|
+
private candidates(now: number): PoolKey[] {
|
|
130
|
+
const out: PoolKey[] = [];
|
|
131
|
+
const n = this.free.length;
|
|
132
|
+
for (let i = 0; i < n; i++) {
|
|
133
|
+
const key = this.free[(this.cursor + i) % n]!;
|
|
134
|
+
if (key.evictedUntil <= now) out.push(key);
|
|
135
|
+
}
|
|
136
|
+
if (n > 0) this.cursor = (this.cursor + 1) % n;
|
|
137
|
+
if (this.paid && this.paid.evictedUntil <= now) out.push(this.paid);
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private nextAvailableAt(): number {
|
|
142
|
+
const all = this.paid ? [...this.free, this.paid] : this.free;
|
|
143
|
+
return Math.min(...all.map((key) => key.evictedUntil));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* How long to bench a key for this failure, or null when the failure is not the
|
|
149
|
+
* key's fault — nothing is gained by rotating, and benching would spend the
|
|
150
|
+
* pool on a request that fails the same way everywhere.
|
|
151
|
+
*/
|
|
152
|
+
function cooldownFor(err: unknown): { kind: ErrorKind; forMs: number } | null {
|
|
153
|
+
if (!(err instanceof ProviderError)) return null;
|
|
154
|
+
const clamp = (ms: number) => Math.min(Math.max(ms, MIN_EVICTION_MS), MAX_EVICTION_MS);
|
|
155
|
+
switch (err.kind) {
|
|
156
|
+
// The three kinds that are about THIS key.
|
|
157
|
+
case "rate":
|
|
158
|
+
return { kind: err.kind, forMs: clamp(err.retryAfterMs ?? RATE_COOLDOWN_MS) };
|
|
159
|
+
case "quota":
|
|
160
|
+
return { kind: err.kind, forMs: clamp(err.retryAfterMs ?? QUOTA_COOLDOWN_MS) };
|
|
161
|
+
case "auth":
|
|
162
|
+
// 12 h, not forever: a key is also refused while a billing account is
|
|
163
|
+
// reinstated, and a pool that drops keys permanently ends up empty.
|
|
164
|
+
return { kind: err.kind, forMs: AUTH_COOLDOWN_MS };
|
|
165
|
+
default:
|
|
166
|
+
// Not the key's fault, but a sibling key is a different project on a
|
|
167
|
+
// different backend, so one more call is the cheapest way to find out —
|
|
168
|
+
// which is what an overload or a stalled request is worth. `network` is
|
|
169
|
+
// excluded deliberately: the socket died on our side and dies identically
|
|
170
|
+
// on every key, so benching for it would answer NoAvailableKeyError to
|
|
171
|
+
// what is really "no internet".
|
|
172
|
+
return isTransient(err.kind) && err.kind !== "network"
|
|
173
|
+
? { kind: err.kind, forMs: TRANSIENT_COOLDOWN_MS }
|
|
174
|
+
: null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Give any provider a rotating pool of keys.
|
|
180
|
+
*
|
|
181
|
+
* The subtlety is the seam's shape. `createStream` is an async generator, so
|
|
182
|
+
* CALLING it performs no I/O: the POST — and the 429 that should rotate the key
|
|
183
|
+
* — happens on the first `next()`, long after a `pool.with` wrapped around the
|
|
184
|
+
* call itself would have returned, with nothing left to rotate. So the first
|
|
185
|
+
* chunk is pulled INSIDE the pool and only the rest of the stream is consumed
|
|
186
|
+
* outside it.
|
|
187
|
+
*
|
|
188
|
+
* That line is also the honest one. Past the first chunk the answer is
|
|
189
|
+
* committed to one key, exactly as a retry is committed past its first chunk
|
|
190
|
+
* (retry.ts, rule 2): a mid-stream 429 evicts nothing and rotates nothing,
|
|
191
|
+
* because there is no way to resume a half-rendered answer on another key.
|
|
192
|
+
*/
|
|
193
|
+
export function withKeyPool(pool: KeyPool, factory: (apiKey: string) => Provider): Provider {
|
|
194
|
+
// The identity every key shares. Building an adapter performs no I/O — it
|
|
195
|
+
// closes over its config and nothing else — so a throwaway instance is the
|
|
196
|
+
// cheapest way to read `id` and `model` without holding a key outside the
|
|
197
|
+
// pool.
|
|
198
|
+
const identity = factory("");
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
id: identity.id,
|
|
202
|
+
model: identity.model,
|
|
203
|
+
|
|
204
|
+
async *createStream(
|
|
205
|
+
messages: ChatMessage[],
|
|
206
|
+
tools: ToolDefinition[],
|
|
207
|
+
opts: StreamOptions = {},
|
|
208
|
+
): AsyncIterable<ProviderChunk> {
|
|
209
|
+
const opened = await pool.with(async (apiKey) => {
|
|
210
|
+
// One AbortController per attempt, chained to the caller's own signal
|
|
211
|
+
// (retry.ts's rule). Finalizing a generator is not cancellation:
|
|
212
|
+
// `return()` unwinds the adapter down to `streamSse`'s finalizer, which
|
|
213
|
+
// only releases the reader's lock — the response body stays live and
|
|
214
|
+
// the request is never aborted. Without this controller an abandoned
|
|
215
|
+
// attempt keeps the provider generating the rest of the answer, holding
|
|
216
|
+
// a connection and a concurrency slot on the very key the pool is
|
|
217
|
+
// rotating away from.
|
|
218
|
+
const controller = new AbortController();
|
|
219
|
+
const onAbort = () => controller.abort(opts.signal?.reason);
|
|
220
|
+
if (opts.signal?.aborted) onAbort();
|
|
221
|
+
else opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
222
|
+
|
|
223
|
+
const stream = factory(apiKey).createStream(messages, tools, {
|
|
224
|
+
...opts,
|
|
225
|
+
signal: controller.signal,
|
|
226
|
+
});
|
|
227
|
+
const iterator = stream[Symbol.asyncIterator]();
|
|
228
|
+
const release = async () => {
|
|
229
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
230
|
+
await close(iterator);
|
|
231
|
+
controller.abort();
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
return { iterator, first: await iterator.next(), release };
|
|
236
|
+
} catch (err) {
|
|
237
|
+
// The request failed under the pool, which is about to try the next
|
|
238
|
+
// key: release this attempt before a second one opens against the
|
|
239
|
+
// same rate limit.
|
|
240
|
+
await release();
|
|
241
|
+
throw err;
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const { iterator, first, release } = opened;
|
|
246
|
+
try {
|
|
247
|
+
if (first.done) return;
|
|
248
|
+
yield first.value;
|
|
249
|
+
for (;;) {
|
|
250
|
+
const next = await iterator.next();
|
|
251
|
+
if (next.done) return;
|
|
252
|
+
yield next.value;
|
|
253
|
+
}
|
|
254
|
+
} finally {
|
|
255
|
+
// A consumer that breaks out of its loop never reaches the end of ours,
|
|
256
|
+
// and an attempt left unfinalized and un-aborted streams for the whole
|
|
257
|
+
// rest of the answer into a body nobody reads.
|
|
258
|
+
await release();
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Finalize an abandoned stream. `return()` can reject on its own (an aborted
|
|
265
|
+
* body), and that must never replace the failure being handled. */
|
|
266
|
+
async function close(iterator: AsyncIterator<ProviderChunk>): Promise<void> {
|
|
267
|
+
try {
|
|
268
|
+
await iterator.return?.();
|
|
269
|
+
} catch {
|
|
270
|
+
// The request is being abandoned either way.
|
|
271
|
+
}
|
|
272
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Anthropic-shape adapter — SSE from POST /v1/messages.
|
|
2
|
-
import {
|
|
2
|
+
import { streamError } from "../errors.ts";
|
|
3
|
+
import { parseToolArgs } from "../tool-args.ts";
|
|
3
4
|
import { streamSse, apiUrl } from "../transport.ts";
|
|
4
5
|
import type {
|
|
5
6
|
ChatMessage,
|
|
@@ -15,13 +16,22 @@ import type {
|
|
|
15
16
|
export interface AnthropicConfig {
|
|
16
17
|
apiKey: string;
|
|
17
18
|
model: string;
|
|
19
|
+
/** Any endpoint speaking the Anthropic Messages dialect — a proxy or gateway.
|
|
20
|
+
* Defaults to Anthropic itself. */
|
|
18
21
|
baseUrl?: string;
|
|
22
|
+
/** Names the provider in errors and logs. The subscription backend is the
|
|
23
|
+
* reason this is not hardcoded: a token failure there is a re-login, not a
|
|
24
|
+
* bad API key, and the two must not read the same in a ledger. */
|
|
25
|
+
id?: string;
|
|
19
26
|
/** Bound default; a per-call `effort` overrides it. */
|
|
20
27
|
effort?: Effort;
|
|
21
28
|
/** Anthropic requires an output ceiling on every request. */
|
|
22
29
|
maxTokens?: number;
|
|
23
30
|
version?: string;
|
|
24
31
|
fetchImpl?: typeof fetch;
|
|
32
|
+
/** Merged into every request. The subscription backend needs its own beta
|
|
33
|
+
* headers, and a gateway in front usually wants one of its own. */
|
|
34
|
+
headers?: Record<string, string>;
|
|
25
35
|
/** Send the key as a Bearer instead of `x-api-key` — what a subscription
|
|
26
36
|
* access token needs. */
|
|
27
37
|
bearer?: boolean;
|
|
@@ -77,8 +87,25 @@ function partsToAnthropic(content: string | ContentPart[]): unknown[] {
|
|
|
77
87
|
* turns carrying `tool_result` blocks — not as a role of their own. Consecutive
|
|
78
88
|
* tool results are merged into one user turn, which the API requires.
|
|
79
89
|
*/
|
|
90
|
+
/**
|
|
91
|
+
* The system prompt as ONE cached block.
|
|
92
|
+
*
|
|
93
|
+
* Anthropic's prompt caching is opt-in PER BLOCK — a plain string system prompt
|
|
94
|
+
* is never cached, however many times it is re-sent. An agent loop re-sends this
|
|
95
|
+
* every single turn, and it is the largest stable prefix in the request, so
|
|
96
|
+
* without the breakpoint the whole thing bills at the full input rate on every
|
|
97
|
+
* round instead of a tenth of it on all but the first.
|
|
98
|
+
*
|
|
99
|
+
* Unconditional. Below the model's minimum cacheable length the field is
|
|
100
|
+
* ignored rather than rejected, and above it the one-time 1.25× write is repaid
|
|
101
|
+
* by the second turn — which, in the loop this package sits under, always comes.
|
|
102
|
+
*/
|
|
103
|
+
function systemBlocks(text: string): unknown[] | undefined {
|
|
104
|
+
return text ? [{ type: "text", text, cache_control: { type: "ephemeral" } }] : undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
80
107
|
export function toAnthropicMessages(messages: readonly ChatMessage[]): {
|
|
81
|
-
system?:
|
|
108
|
+
system?: unknown[];
|
|
82
109
|
messages: unknown[];
|
|
83
110
|
} {
|
|
84
111
|
const system = messages
|
|
@@ -125,21 +152,14 @@ export function toAnthropicMessages(messages: readonly ChatMessage[]): {
|
|
|
125
152
|
type: "tool_use",
|
|
126
153
|
id: call.id,
|
|
127
154
|
name: call.name,
|
|
128
|
-
input:
|
|
155
|
+
input: parseToolArgs(call.arguments),
|
|
129
156
|
});
|
|
130
157
|
}
|
|
131
158
|
if (blocks.length > 0) pushBlocks("assistant", blocks);
|
|
132
159
|
}
|
|
133
160
|
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function safeParse(raw: string): unknown {
|
|
138
|
-
try {
|
|
139
|
-
return raw ? JSON.parse(raw) : {};
|
|
140
|
-
} catch {
|
|
141
|
-
return {};
|
|
142
|
-
}
|
|
161
|
+
const blocks = systemBlocks(system);
|
|
162
|
+
return { ...(blocks ? { system: blocks } : {}), messages: out };
|
|
143
163
|
}
|
|
144
164
|
|
|
145
165
|
interface AnthropicEvent {
|
|
@@ -167,9 +187,10 @@ interface AnthropicEvent {
|
|
|
167
187
|
|
|
168
188
|
export function createAnthropicProvider(config: AnthropicConfig): Provider {
|
|
169
189
|
const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
|
|
190
|
+
const id = config.id ?? "anthropic";
|
|
170
191
|
|
|
171
192
|
return {
|
|
172
|
-
id
|
|
193
|
+
id,
|
|
173
194
|
model: config.model,
|
|
174
195
|
|
|
175
196
|
async *createStream(
|
|
@@ -230,9 +251,10 @@ export function createAnthropicProvider(config: AnthropicConfig): Provider {
|
|
|
230
251
|
...(config.bearer
|
|
231
252
|
? { authorization: `Bearer ${config.apiKey}` }
|
|
232
253
|
: { "x-api-key": config.apiKey }),
|
|
254
|
+
...config.headers,
|
|
233
255
|
},
|
|
234
256
|
body: request,
|
|
235
|
-
provider:
|
|
257
|
+
provider: id,
|
|
236
258
|
...(opts.signal ? { signal: opts.signal } : {}),
|
|
237
259
|
...(config.fetchImpl ? { fetchImpl: config.fetchImpl } : {}),
|
|
238
260
|
})) {
|
|
@@ -244,13 +266,12 @@ export function createAnthropicProvider(config: AnthropicConfig): Provider {
|
|
|
244
266
|
}
|
|
245
267
|
|
|
246
268
|
switch (event.type) {
|
|
269
|
+
// A failure the backend reports after its headers went out. Classified
|
|
270
|
+
// rather than assumed transient: this shape carries `overloaded_error`
|
|
271
|
+
// most of the time, but a prompt found too long mid-stream arrives the
|
|
272
|
+
// same way, and retrying that one only fails it again more slowly.
|
|
247
273
|
case "error":
|
|
248
|
-
throw
|
|
249
|
-
"anthropic",
|
|
250
|
-
"overload",
|
|
251
|
-
event.error?.message ?? "anthropic stream error",
|
|
252
|
-
{ code: event.error?.type },
|
|
253
|
-
);
|
|
274
|
+
throw streamError(id, event.error);
|
|
254
275
|
|
|
255
276
|
case "message_start": {
|
|
256
277
|
const usage = event.message?.usage;
|