@providerkit/core 0.23.0 → 0.25.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 +43 -7
- package/dist/auth/device.d.ts +3 -2
- package/dist/auth/device.d.ts.map +1 -1
- package/dist/auth/device.js +5 -2
- package/dist/auth/device.js.map +1 -1
- package/dist/auth/flows.d.ts +5 -1
- package/dist/auth/flows.d.ts.map +1 -1
- package/dist/auth/flows.js +237 -0
- package/dist/auth/flows.js.map +1 -1
- package/dist/auth/oauth.d.ts +4 -2
- package/dist/auth/oauth.d.ts.map +1 -1
- package/dist/auth/oauth.js +4 -3
- package/dist/auth/oauth.js.map +1 -1
- package/dist/auth/with-token.d.ts +4 -1
- package/dist/auth/with-token.d.ts.map +1 -1
- package/dist/auth/with-token.js +9 -4
- package/dist/auth/with-token.js.map +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +3 -0
- package/dist/errors.js.map +1 -1
- package/dist/gate.d.ts +29 -0
- package/dist/gate.d.ts.map +1 -0
- package/dist/gate.js +96 -0
- package/dist/gate.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/key-pool.d.ts +11 -0
- package/dist/key-pool.d.ts.map +1 -1
- package/dist/key-pool.js +14 -0
- package/dist/key-pool.js.map +1 -1
- package/dist/presets.d.ts +53 -0
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +100 -0
- package/dist/presets.js.map +1 -1
- package/dist/providers/anthropic.d.ts +7 -0
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +3 -1
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/factory.d.ts +36 -0
- package/dist/providers/factory.d.ts.map +1 -1
- package/dist/providers/factory.js +208 -22
- package/dist/providers/factory.js.map +1 -1
- package/dist/providers/gemini.d.ts +17 -5
- package/dist/providers/gemini.d.ts.map +1 -1
- package/dist/providers/gemini.js +241 -119
- package/dist/providers/gemini.js.map +1 -1
- package/dist/providers/openai.d.ts +7 -0
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +7 -4
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/responses.d.ts +11 -0
- package/dist/providers/responses.d.ts.map +1 -1
- package/dist/providers/responses.js +3 -1
- package/dist/providers/responses.js.map +1 -1
- package/dist/registry.d.ts +105 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +291 -0
- package/dist/registry.js.map +1 -0
- package/dist/schema.d.ts +27 -9
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +197 -62
- package/dist/schema.js.map +1 -1
- package/dist/tools.d.ts +8 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +2 -2
- package/dist/tools.js.map +1 -1
- package/dist/turn.d.ts +55 -0
- package/dist/turn.d.ts.map +1 -0
- package/dist/turn.js +125 -0
- package/dist/turn.js.map +1 -0
- package/dist/types.d.ts +8 -22
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +0 -45
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/src/auth/device.ts +8 -4
- package/src/auth/flows.ts +288 -1
- package/src/auth/oauth.ts +8 -3
- package/src/auth/with-token.ts +9 -4
- package/src/errors.ts +3 -0
- package/src/gate.ts +152 -0
- package/src/index.ts +3 -0
- package/src/key-pool.ts +15 -0
- package/src/presets.ts +100 -0
- package/src/providers/anthropic.ts +10 -1
- package/src/providers/factory.ts +288 -34
- package/src/providers/gemini.ts +273 -124
- package/src/providers/openai.ts +15 -4
- package/src/providers/responses.ts +14 -1
- package/src/registry.ts +420 -0
- package/src/schema.ts +200 -72
- package/src/tools.ts +10 -2
- package/src/turn.ts +166 -0
- package/src/types.ts +8 -62
package/src/gate.ts
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// How many calls run at once on one connection.
|
|
2
|
+
//
|
|
3
|
+
// A flat-rate plan caps concurrent requests, and says so only by failing: six
|
|
4
|
+
// calls at once on one Z.ai Coding Plan key came back with some 429s (measured
|
|
5
|
+
// 2026-09-25, `capability.ts`). Behind a fallback chain the overflow is
|
|
6
|
+
// answered by the NEXT model, so the cap shows up as a quietly different
|
|
7
|
+
// answer, not as an error. The capability probe runs one call at a time for
|
|
8
|
+
// that reason, and one adopter runs its batches one item at a time on purpose.
|
|
9
|
+
// A gate holds the overflow in a queue instead, and lets each call in as a
|
|
10
|
+
// slot frees.
|
|
11
|
+
//
|
|
12
|
+
// The hard part is giving the slot back. A slot leaked by an abandoned stream
|
|
13
|
+
// is gone for the life of the process, with no error anywhere: the connection
|
|
14
|
+
// just serves one call fewer, until it serves none. So the slot is taken on the
|
|
15
|
+
// first read (a stream nobody reads holds nothing) and released exactly once,
|
|
16
|
+
// on whichever comes first: the stream ends, throws or is returned, or its
|
|
17
|
+
// signal aborts. That last one matters when the consumer holds a stream and
|
|
18
|
+
// stops reading it. Nothing else would ever free that slot.
|
|
19
|
+
//
|
|
20
|
+
// ponytail: three known ceilings.
|
|
21
|
+
// - It counts one process only. Two replicas each let `max` calls through. The
|
|
22
|
+
// upgrade path is a shared counter (one adopter already keeps one in Redis).
|
|
23
|
+
// - A full gate makes callers wait instead of moving on to the next candidate
|
|
24
|
+
// in a fallback chain. The upgrade is a bounded wait that fails as transient,
|
|
25
|
+
// so the pool tries the next one.
|
|
26
|
+
// - A fallback probe stuck waiting here has no first chunk, so once it goes
|
|
27
|
+
// stale (`probeStaleMs`) the pool can let one extra probe in beside it.
|
|
28
|
+
import type {
|
|
29
|
+
ChatMessage,
|
|
30
|
+
Provider,
|
|
31
|
+
ProviderChunk,
|
|
32
|
+
StreamOptions,
|
|
33
|
+
ToolDefinition,
|
|
34
|
+
} from "./types.ts";
|
|
35
|
+
|
|
36
|
+
export interface Gate {
|
|
37
|
+
/** How many calls may hold a slot at once. */
|
|
38
|
+
readonly max: number;
|
|
39
|
+
/** How many slots are held right now. */
|
|
40
|
+
readonly held: number;
|
|
41
|
+
/** How many calls are queued for a slot. */
|
|
42
|
+
readonly waiting: number;
|
|
43
|
+
/**
|
|
44
|
+
* Wait for a slot, first in, first out. Resolves to its release, which is
|
|
45
|
+
* safe to call more than once. The signal covers the whole life of the slot:
|
|
46
|
+
* an abort while waiting leaves the queue and rejects with `signal.reason`,
|
|
47
|
+
* and an abort while held releases it.
|
|
48
|
+
*/
|
|
49
|
+
acquire(signal?: AbortSignal): Promise<() => void>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A gate with `max` slots. Keep one alive per connection, so every call on
|
|
53
|
+
* it shares the count. */
|
|
54
|
+
export function createGate(max: number): Gate {
|
|
55
|
+
if (!Number.isInteger(max) || max < 1) {
|
|
56
|
+
throw new Error(`[providerkit] A gate needs a whole number of slots, 1 or more. Got ${max}.`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let held = 0;
|
|
60
|
+
const queue: Array<() => void> = [];
|
|
61
|
+
|
|
62
|
+
function acquire(signal?: AbortSignal): Promise<() => void> {
|
|
63
|
+
// Rejected before it queues: a caller already gone must not take a place
|
|
64
|
+
// in line, or a slot, even for a moment.
|
|
65
|
+
if (signal?.aborted) return Promise.reject(signal.reason);
|
|
66
|
+
|
|
67
|
+
return new Promise((resolve, reject) => {
|
|
68
|
+
let state: "waiting" | "held" | "released" = "waiting";
|
|
69
|
+
|
|
70
|
+
const release = () => {
|
|
71
|
+
if (state !== "held") return;
|
|
72
|
+
state = "released";
|
|
73
|
+
signal?.removeEventListener("abort", onAbort);
|
|
74
|
+
// Handed straight to the next waiter, so `held` never dips and a
|
|
75
|
+
// newcomer arriving now finds the gate full and queues behind them.
|
|
76
|
+
const next = queue.shift();
|
|
77
|
+
if (next) next();
|
|
78
|
+
else held--;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const grant = () => {
|
|
82
|
+
state = "held";
|
|
83
|
+
resolve(release);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
function onAbort() {
|
|
87
|
+
if (state === "held") {
|
|
88
|
+
release();
|
|
89
|
+
} else if (state === "waiting") {
|
|
90
|
+
state = "released";
|
|
91
|
+
queue.splice(queue.indexOf(grant), 1);
|
|
92
|
+
reject(signal?.reason);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (held < max) {
|
|
97
|
+
held++;
|
|
98
|
+
grant();
|
|
99
|
+
} else {
|
|
100
|
+
queue.push(grant);
|
|
101
|
+
}
|
|
102
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
max,
|
|
108
|
+
get held() {
|
|
109
|
+
return held;
|
|
110
|
+
},
|
|
111
|
+
get waiting() {
|
|
112
|
+
return queue.length;
|
|
113
|
+
},
|
|
114
|
+
acquire,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Let a provider run at most `gate.max` calls at once.
|
|
120
|
+
*
|
|
121
|
+
* Put it outside the watchdog. The watchdog arms on its first read, and this
|
|
122
|
+
* reads it only once a slot is held, so a call queued here never spends its
|
|
123
|
+
* deadlines waiting. Inside it, a call that stood in line for five minutes
|
|
124
|
+
* would time out as a silent provider before it ever sent a byte.
|
|
125
|
+
*/
|
|
126
|
+
export function withGate(provider: Provider, gate: Gate): Provider {
|
|
127
|
+
return {
|
|
128
|
+
...provider,
|
|
129
|
+
createStream(
|
|
130
|
+
messages: ChatMessage[],
|
|
131
|
+
tools: ToolDefinition[],
|
|
132
|
+
opts: StreamOptions = {},
|
|
133
|
+
): AsyncIterable<ProviderChunk> {
|
|
134
|
+
// A generator, so nothing runs (the wait included) until the first read.
|
|
135
|
+
async function* gated(): AsyncGenerator<ProviderChunk> {
|
|
136
|
+
const release = await gate.acquire(opts.signal);
|
|
137
|
+
try {
|
|
138
|
+
// The slot can be granted and released by the same abort before this
|
|
139
|
+
// line runs (two calls sharing one signal). Such a call is over, and
|
|
140
|
+
// must not reach the wire without a slot.
|
|
141
|
+
opts.signal?.throwIfAborted();
|
|
142
|
+
yield* provider.createStream(messages, tools, opts);
|
|
143
|
+
} finally {
|
|
144
|
+
// Covers the end, a throw and a consumer's `break`. An abort may
|
|
145
|
+
// have released it already, and a second call is a no-op.
|
|
146
|
+
release();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return gated();
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
export * from "./types.ts";
|
|
2
|
+
export * from "./turn.ts";
|
|
2
3
|
export * from "./errors.ts";
|
|
3
4
|
export * from "./retry.ts";
|
|
4
5
|
export * from "./fallback.ts";
|
|
5
6
|
export * from "./key-pool.ts";
|
|
6
7
|
export * from "./watchdog.ts";
|
|
8
|
+
export * from "./gate.ts";
|
|
7
9
|
export * from "./usage.ts";
|
|
8
10
|
export * from "./transport.ts";
|
|
9
11
|
export * from "./rate-limit.ts";
|
|
@@ -21,3 +23,4 @@ export * from "./providers/responses.ts";
|
|
|
21
23
|
export * from "./providers/gemini.ts";
|
|
22
24
|
export * from "./providers/zai.ts";
|
|
23
25
|
export * from "./providers/factory.ts";
|
|
26
|
+
export * from "./registry.ts";
|
package/src/key-pool.ts
CHANGED
|
@@ -101,6 +101,17 @@ export class KeyPool {
|
|
|
101
101
|
return this.free.length + (this.paid ? 1 : 0);
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
/** Each key's tier and when it is back (0 = serving now): free keys in
|
|
105
|
+
* config order, then the paid one. Never the key itself, so a status page
|
|
106
|
+
* can show it as is. */
|
|
107
|
+
status(): Array<{ tier: KeyTier; evictedUntil: number }> {
|
|
108
|
+
const now = this.now();
|
|
109
|
+
return [...this.free, ...(this.paid ? [this.paid] : [])].map(({ tier, evictedUntil }) => ({
|
|
110
|
+
tier,
|
|
111
|
+
evictedUntil: evictedUntil > now ? evictedUntil : 0,
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
|
|
104
115
|
/**
|
|
105
116
|
* Run `fn` with the next available key, rotating on key-specific and
|
|
106
117
|
* transient failures. Everything else (a bad request, a content block) throws
|
|
@@ -206,6 +217,10 @@ function cooldownFor(err: unknown, now: number): { kind: ErrorKind; forMs: numbe
|
|
|
206
217
|
* committed to one key, exactly as a retry is committed past its first chunk
|
|
207
218
|
* (retry.ts, rule 2): a mid-stream 429 evicts nothing and rotates nothing,
|
|
208
219
|
* because there is no way to resume a half-rendered answer on another key.
|
|
220
|
+
*
|
|
221
|
+
* The factory runs once per attempt. An adapter with a session header mints
|
|
222
|
+
* its id when it is built, so give the factory an `instanceSessionId`, or each
|
|
223
|
+
* attempt opens a new session on a gateway that pins sessions to one host.
|
|
209
224
|
*/
|
|
210
225
|
export function withKeyPool(pool: KeyPool, factory: (apiKey: string) => Provider): Provider {
|
|
211
226
|
// The identity every key shares. Building an adapter performs no I/O — it
|
package/src/presets.ts
CHANGED
|
@@ -431,6 +431,82 @@ export const PROVIDER_PRESETS = {
|
|
|
431
431
|
// privacy setting, and answers 500 without it.
|
|
432
432
|
rotation: ["mimo-v2.6-flash", "mimo-v2.5", "glm-5.3-flash", "qwen3.8-flash", "longcat-2.0"],
|
|
433
433
|
},
|
|
434
|
+
/**
|
|
435
|
+
* OpenCode Zen: free and pay-as-you-go models on `https://opencode.ai/zen/v1`,
|
|
436
|
+
* matching cc-proxy's `opencode-zen` provider.
|
|
437
|
+
*
|
|
438
|
+
* Free models (`big-pickle` and every `-free` id) lead the list and the default
|
|
439
|
+
* rotation. Zen's free tier requires the OpenCode CLI headers, a `ses_` session
|
|
440
|
+
* id and a `read` tool on the request; the factory adds those automatically and
|
|
441
|
+
* retries a free model anonymously if a stale key answers 401.
|
|
442
|
+
*
|
|
443
|
+
* `routes` picks each model's wire from its id: `claude-` and Qwen (except
|
|
444
|
+
* `qwen3.8-max`) on Anthropic `/v1/messages`, `gpt-`, `grok-` and `muse-` on
|
|
445
|
+
* `/v1/responses`, `gemini-` on `/v1/models/<id>:streamGenerateContent?alt=sse`,
|
|
446
|
+
* and everything else on `/v1/chat/completions`.
|
|
447
|
+
*/
|
|
448
|
+
"opencode-zen": {
|
|
449
|
+
shape: "openai",
|
|
450
|
+
baseUrl: "https://opencode.ai/zen/v1",
|
|
451
|
+
auth: "bearer",
|
|
452
|
+
defaultModel: "minimax-m3.1-flash-free",
|
|
453
|
+
models: [
|
|
454
|
+
"minimax-m3.1-flash-free",
|
|
455
|
+
"mimo-v2.6-flash-free",
|
|
456
|
+
"deepseek-v4.1-flash-free",
|
|
457
|
+
"glm-5.3-flash-free",
|
|
458
|
+
"big-pickle",
|
|
459
|
+
"mimo-v2.5-free",
|
|
460
|
+
"nemotron-3.5-super-vl-free",
|
|
461
|
+
"nemotron-3.5-lightning-free",
|
|
462
|
+
"longcat-2.5-preview-free",
|
|
463
|
+
"beam-free",
|
|
464
|
+
"ll-alpha-free",
|
|
465
|
+
"exo-free",
|
|
466
|
+
"nemotron-3-ultra-free",
|
|
467
|
+
"claude-sonnet-5-5",
|
|
468
|
+
"claude-opus-5-5",
|
|
469
|
+
"claude-fable-5-1",
|
|
470
|
+
"claude-haiku-5-5",
|
|
471
|
+
"gpt-6-sol",
|
|
472
|
+
"gpt-5.6-luna",
|
|
473
|
+
"gemini-4-argon",
|
|
474
|
+
"gemini-3.8-flash",
|
|
475
|
+
"grok-4.7",
|
|
476
|
+
"glm-5.3-flash",
|
|
477
|
+
"deepseek-v4.1-flash",
|
|
478
|
+
"kimi-k3",
|
|
479
|
+
"minimax-m3",
|
|
480
|
+
"qwen3.8-flash",
|
|
481
|
+
],
|
|
482
|
+
sessionHeader: "x-opencode-session",
|
|
483
|
+
routes: [
|
|
484
|
+
{
|
|
485
|
+
prefixes: ["claude-", "qwen3.8-flash", "qwen3.7", "qwen3.6", "qwen3.5"],
|
|
486
|
+
shape: "anthropic",
|
|
487
|
+
baseUrl: "https://opencode.ai/zen",
|
|
488
|
+
auth: "key",
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
prefixes: ["gpt-", "grok-", "muse-"],
|
|
492
|
+
shape: "responses",
|
|
493
|
+
baseUrl: "https://opencode.ai/zen",
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
prefixes: ["gemini-"],
|
|
497
|
+
shape: "gemini",
|
|
498
|
+
baseUrl: "https://opencode.ai/zen/v1",
|
|
499
|
+
auth: "key",
|
|
500
|
+
},
|
|
501
|
+
],
|
|
502
|
+
rotation: [
|
|
503
|
+
"minimax-m3.1-flash-free",
|
|
504
|
+
"mimo-v2.6-flash-free",
|
|
505
|
+
"deepseek-v4.1-flash-free",
|
|
506
|
+
"big-pickle",
|
|
507
|
+
"glm-5.3-flash-free",
|
|
508
|
+
],
|
|
509
|
+
},
|
|
434
510
|
kimi: {
|
|
435
511
|
shape: "anthropic",
|
|
436
512
|
baseUrl: "https://api.kimi.ai/coding",
|
|
@@ -617,6 +693,30 @@ export const PROVIDER_PRESETS = {
|
|
|
617
693
|
},
|
|
618
694
|
initiatorHeader: true,
|
|
619
695
|
},
|
|
696
|
+
/**
|
|
697
|
+
* Google Antigravity (Cloud Code Assist) OAuth, matching cc-proxy's
|
|
698
|
+
* `antigravity` provider. Sign in with `createAuthFlow("antigravity", host)`
|
|
699
|
+
* from `@providerkit/core/auth`, which discovers the Cloud Code Assist
|
|
700
|
+
* `projectId` and stores it on the credential; `withToken` and `createRegistry`
|
|
701
|
+
* forward it automatically.
|
|
702
|
+
*/
|
|
703
|
+
antigravity: {
|
|
704
|
+
shape: "gemini",
|
|
705
|
+
baseUrl: "https://daily-cloudcode-pa.googleapis.com",
|
|
706
|
+
path: "/v1internal:streamGenerateContent?alt=sse",
|
|
707
|
+
auth: "oauth",
|
|
708
|
+
defaultModel: "gemini-4-argon",
|
|
709
|
+
models: [
|
|
710
|
+
"gemini-4-argon",
|
|
711
|
+
"gemini-3.8-flash",
|
|
712
|
+
"gemini-3.7-flash",
|
|
713
|
+
"gemini-3.1-pro",
|
|
714
|
+
"claude-opus-5-5",
|
|
715
|
+
"claude-sonnet-5-5",
|
|
716
|
+
"claude-sonnet-4-6",
|
|
717
|
+
"claude-opus-4-6-thinking",
|
|
718
|
+
],
|
|
719
|
+
},
|
|
620
720
|
} as const satisfies Record<string, ProviderPreset>;
|
|
621
721
|
|
|
622
722
|
export type ProviderPresetId = keyof typeof PROVIDER_PRESETS;
|
|
@@ -55,6 +55,13 @@ export interface AnthropicConfig extends ProviderFallbackConfig {
|
|
|
55
55
|
/** Header that carries `StreamOptions.sessionId`. When set and a call has no
|
|
56
56
|
* sessionId, one stable id for this provider instance rides instead. */
|
|
57
57
|
sessionHeader?: string;
|
|
58
|
+
/** The id `sessionHeader` carries when a call has no `sessionId`. Absent, the
|
|
59
|
+
* adapter mints one when it is built. Pass one when something rebuilds the
|
|
60
|
+
* adapter (`withKeyPool` and `withToken` build one per attempt): each build
|
|
61
|
+
* mints a new id, and a gateway that pins a session to one upstream host
|
|
62
|
+
* then treats every call as a new session, re-routes it and misses its
|
|
63
|
+
* prompt cache. Ignored without `sessionHeader`. */
|
|
64
|
+
instanceSessionId?: string;
|
|
58
65
|
/** Say "no thinking" with an explicit `thinking: { type: "disabled" }` when
|
|
59
66
|
* effort resolves to none, instead of omitting the field. For endpoints
|
|
60
67
|
* where an absent field means the model's default, not off. Z.ai's coding
|
|
@@ -402,7 +409,9 @@ interface AnthropicEvent {
|
|
|
402
409
|
export function createAnthropicProvider(config: AnthropicConfig): Provider {
|
|
403
410
|
const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
|
|
404
411
|
const id = config.id ?? "anthropic";
|
|
405
|
-
const instanceSessionId = config.sessionHeader
|
|
412
|
+
const instanceSessionId = config.sessionHeader
|
|
413
|
+
? (config.instanceSessionId ?? crypto.randomUUID())
|
|
414
|
+
: undefined;
|
|
406
415
|
|
|
407
416
|
const provider: Provider = {
|
|
408
417
|
id,
|