@providerkit/core 0.21.0 → 0.22.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 +7 -7
- package/dist/attribution.d.ts +2 -2
- package/dist/capability.d.ts +9 -9
- package/dist/capability.js +9 -9
- package/dist/capability.js.map +1 -1
- package/dist/context.d.ts +4 -4
- package/dist/context.js +4 -4
- package/dist/errors.d.ts +15 -15
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +14 -14
- package/dist/image.d.ts +1 -1
- package/dist/image.d.ts.map +1 -1
- package/dist/jev.d.ts +9 -9
- package/dist/jev.d.ts.map +1 -1
- package/dist/jev.js +1 -1
- package/dist/key-pool.d.ts +4 -4
- package/dist/key-pool.js +6 -6
- package/dist/key-pool.js.map +1 -1
- package/dist/presets.d.ts +26 -22
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +13 -12
- package/dist/presets.js.map +1 -1
- package/dist/providers/anthropic.d.ts +5 -5
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +9 -9
- package/dist/providers/factory.d.ts +2 -2
- package/dist/providers/factory.d.ts.map +1 -1
- package/dist/providers/factory.js +2 -1
- package/dist/providers/factory.js.map +1 -1
- package/dist/providers/gemini.d.ts +2 -2
- package/dist/providers/gemini.d.ts.map +1 -1
- package/dist/providers/gemini.js +15 -14
- package/dist/providers/gemini.js.map +1 -1
- package/dist/providers/openai.d.ts +12 -9
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +14 -13
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/responses.d.ts +3 -3
- package/dist/providers/responses.js +5 -5
- package/dist/providers/responses.js.map +1 -1
- package/dist/providers/zai.d.ts +2 -2
- package/dist/providers/zai.js +2 -2
- package/dist/rate-limit.d.ts +3 -3
- package/dist/rate-limit.js +3 -3
- package/dist/retry.d.ts +5 -5
- package/dist/retry.js +4 -4
- package/dist/schema.d.ts +6 -6
- package/dist/schema.js +8 -8
- package/dist/tool-args.d.ts +1 -1
- package/dist/tool-args.js +2 -2
- package/dist/tools.d.ts +3 -3
- package/dist/tools.js +2 -2
- package/dist/tools.js.map +1 -1
- package/dist/transport.d.ts +7 -7
- package/dist/transport.js +6 -6
- package/dist/types.d.ts +24 -23
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +3 -3
- package/dist/types.js.map +1 -1
- package/dist/usage.d.ts +4 -4
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js +3 -3
- package/dist/usage.js.map +1 -1
- package/dist/watchdog.d.ts +12 -12
- package/dist/watchdog.js +10 -10
- package/dist/zod.d.ts +4 -4
- package/dist/zod.js +3 -3
- package/package.json +1 -1
- package/src/attribution.ts +2 -2
- package/src/capability.ts +12 -12
- package/src/context.ts +4 -4
- package/src/errors.ts +18 -18
- package/src/image.ts +1 -1
- package/src/jev.ts +11 -11
- package/src/key-pool.ts +7 -7
- package/src/presets.ts +26 -22
- package/src/providers/anthropic.ts +14 -14
- package/src/providers/factory.ts +4 -3
- package/src/providers/gemini.ts +19 -16
- package/src/providers/openai.ts +22 -17
- package/src/providers/responses.ts +11 -11
- package/src/providers/zai.ts +2 -2
- package/src/rate-limit.ts +4 -4
- package/src/retry.ts +5 -5
- package/src/schema.ts +8 -8
- package/src/tool-args.ts +2 -2
- package/src/tools.ts +4 -4
- package/src/transport.ts +7 -7
- package/src/types.ts +24 -23
- package/src/usage.ts +5 -5
- package/src/watchdog.ts +13 -13
- package/src/zod.ts +4 -4
package/src/jev.ts
CHANGED
|
@@ -34,7 +34,7 @@ export type JevInstructions = string | Record<string, unknown> | readonly unknow
|
|
|
34
34
|
export interface ChoiceQuestion<Option extends string = string> {
|
|
35
35
|
type: "choice";
|
|
36
36
|
instructions?: JevInstructions;
|
|
37
|
-
/** Each option, mapped to what it means
|
|
37
|
+
/** Each option, mapped to what it means. Use `null` when its name says it all.
|
|
38
38
|
* TypeSafe takes up to 255. */
|
|
39
39
|
criteria: Record<Option, unknown>;
|
|
40
40
|
}
|
|
@@ -64,7 +64,7 @@ export interface ChoiceAnswer<Option extends string = string> {
|
|
|
64
64
|
* because a caller acting on it needs more than the pick: the runner-up,
|
|
65
65
|
* P(yes) when the pick was no, a joint probability across two questions. */
|
|
66
66
|
probabilities: Record<Option, number>;
|
|
67
|
-
/** How peaked `probabilities` is, 0 to 1
|
|
67
|
+
/** How peaked `probabilities` is, from 0 to 1. Not the probability of the pick.
|
|
68
68
|
* TypeSafe's own example spreads 0.57 / 0.43 and reports 0.35, so reading
|
|
69
69
|
* it as P(pick) would put the pick below the option it beat. Null when the
|
|
70
70
|
* host sent none. */
|
|
@@ -90,7 +90,7 @@ export interface ScoreAnswer {
|
|
|
90
90
|
|
|
91
91
|
export type JevAnswer = ChoiceAnswer | NoulAnswer | ScoreAnswer;
|
|
92
92
|
|
|
93
|
-
/** The answer type a question gets back
|
|
93
|
+
/** The answer type a question gets back. A choice's options carry through. */
|
|
94
94
|
export type AnswerTo<Q> =
|
|
95
95
|
Q extends ChoiceQuestion<infer Option extends string>
|
|
96
96
|
? ChoiceAnswer<Option>
|
|
@@ -101,12 +101,12 @@ export type AnswerTo<Q> =
|
|
|
101
101
|
: never;
|
|
102
102
|
|
|
103
103
|
export interface JevReply<Q extends Record<string, JevQuestion>> {
|
|
104
|
-
/** One per question
|
|
104
|
+
/** One per question. `null` when that answer did not pass the checks in
|
|
105
105
|
* `readAnswer`. Each fails alone: in a fan-out, a malformed answer to a
|
|
106
106
|
* question the caller never acts on must not cost the one it does. */
|
|
107
107
|
answers: { [Id in keyof Q]: AnswerTo<Q[Id]> | null };
|
|
108
108
|
usage: TokenUsage;
|
|
109
|
-
/** The model that answered, as the host names it
|
|
109
|
+
/** The model that answered, as the host names it: `jev-1.13.0` on
|
|
110
110
|
* TypeSafe, `typesafe/jev-1.13-20260917` on OpenRouter. */
|
|
111
111
|
model: string;
|
|
112
112
|
}
|
|
@@ -139,8 +139,8 @@ export interface JevClient {
|
|
|
139
139
|
): Promise<JevReply<Q>>;
|
|
140
140
|
/** Proves the key, the account and the model work, with one real question:
|
|
141
141
|
* about 40 input tokens, a few millionths of a dollar. A free read (a
|
|
142
|
-
* model list, a balance) proves only the key
|
|
143
|
-
* run this model
|
|
142
|
+
* model list, a balance) proves only the key, not that this account can
|
|
143
|
+
* run this model. Vercel has no such free read. Throws the classified failure. */
|
|
144
144
|
checkKey(opts?: { signal?: AbortSignal }): Promise<void>;
|
|
145
145
|
}
|
|
146
146
|
|
|
@@ -158,7 +158,7 @@ interface HostRow {
|
|
|
158
158
|
url(config: JevConfig): string;
|
|
159
159
|
headers(config: JevConfig, model: string): Record<string, string>;
|
|
160
160
|
body(model: string, state: unknown, questions: Record<string, JevQuestion>): unknown;
|
|
161
|
-
/** The reply in TypeSafe's own shape
|
|
161
|
+
/** The reply in TypeSafe's own shape (`{ model, answers, usage }`),
|
|
162
162
|
* whatever the host wrapped it in. */
|
|
163
163
|
unwrap(json: unknown): Record<string, unknown> | undefined;
|
|
164
164
|
}
|
|
@@ -228,7 +228,7 @@ const HOSTS: Record<JevHost, HostRow> = {
|
|
|
228
228
|
* `providerMetadata.typesafe.confidence`, and usage is camelCase.
|
|
229
229
|
*
|
|
230
230
|
* ponytail: read from @jkudish/jev-agent-tools' adapter, not from a recorded
|
|
231
|
-
* reply
|
|
231
|
+
* reply. No Vercel key has run it here. If a real reply disagrees, this
|
|
232
232
|
* function is the whole fix.
|
|
233
233
|
*/
|
|
234
234
|
function fromVercel(json: unknown): Record<string, unknown> | undefined {
|
|
@@ -282,7 +282,7 @@ function distribution(value: unknown, keys: readonly string[]): Record<string, n
|
|
|
282
282
|
}
|
|
283
283
|
|
|
284
284
|
/**
|
|
285
|
-
* One answer, checked against the question that asked it
|
|
285
|
+
* One answer, checked against the question that asked it, or null.
|
|
286
286
|
*
|
|
287
287
|
* A Jev answer usually becomes an action (a click, a label, a route), so it is
|
|
288
288
|
* read fail-closed: the right type; for a choice, a pick among the offered
|
|
@@ -291,7 +291,7 @@ function distribution(value: unknown, keys: readonly string[]): Record<string, n
|
|
|
291
291
|
* the pick.
|
|
292
292
|
*
|
|
293
293
|
* `ask` runs this on every answer. It is exported for the app that has to
|
|
294
|
-
* keep its own request
|
|
294
|
+
* keep its own request (its own error copy, its own auth refresh) and can
|
|
295
295
|
* still take the checks.
|
|
296
296
|
*/
|
|
297
297
|
export function readAnswer<Q extends JevQuestion>(question: Q, raw: unknown): AnswerTo<Q> | null;
|
package/src/key-pool.ts
CHANGED
|
@@ -25,10 +25,10 @@ import type {
|
|
|
25
25
|
|
|
26
26
|
/** A per-minute throttle: the window it names is the next one. */
|
|
27
27
|
const RATE_COOLDOWN_MS = 60_000;
|
|
28
|
-
/** A balance or a daily window
|
|
28
|
+
/** A balance or a daily window. Minutes will not bring it back. */
|
|
29
29
|
const QUOTA_COOLDOWN_MS = 60 * 60_000;
|
|
30
30
|
const AUTH_COOLDOWN_MS = 12 * 60 * 60_000;
|
|
31
|
-
/** The vendor's bad time, not the key's
|
|
31
|
+
/** The vendor's bad time, not the key's. Benched only long enough for the
|
|
32
32
|
* next call to land somewhere else. */
|
|
33
33
|
const TRANSIENT_COOLDOWN_MS = 60_000;
|
|
34
34
|
const MIN_EVICTION_MS = 1_000;
|
|
@@ -47,7 +47,7 @@ export interface KeyPoolOptions {
|
|
|
47
47
|
/** Free-tier keys, walked round-robin before the paid one. */
|
|
48
48
|
keys: readonly string[];
|
|
49
49
|
paidKey?: string | null;
|
|
50
|
-
/** Fires whenever a key is benched
|
|
50
|
+
/** Fires whenever a key is benched. The pool's only report, in place of a
|
|
51
51
|
* logger a zero-dependency package has no business owning. */
|
|
52
52
|
onEvict?: (info: { tier: KeyTier; kind: ErrorKind; forMs: number }) => void;
|
|
53
53
|
/** Injected in tests, so an expiry can be exercised without waiting out a
|
|
@@ -96,7 +96,7 @@ export class KeyPool {
|
|
|
96
96
|
/**
|
|
97
97
|
* Run `fn` with the next available key, rotating on key-specific and
|
|
98
98
|
* transient failures. Everything else (a bad request, a content block) throws
|
|
99
|
-
* straight through
|
|
99
|
+
* straight through: it would fail identically on every key, and spending the
|
|
100
100
|
* pool on it only turns one bad request into an outage.
|
|
101
101
|
*/
|
|
102
102
|
async with<T>(fn: (apiKey: string, tier: KeyTier) => Promise<T>): Promise<T> {
|
|
@@ -146,7 +146,7 @@ export class KeyPool {
|
|
|
146
146
|
|
|
147
147
|
/**
|
|
148
148
|
* How long to bench a key for this failure, or null when the failure is not the
|
|
149
|
-
* key's fault
|
|
149
|
+
* key's fault. Nothing is gained by rotating, and benching would spend the
|
|
150
150
|
* pool on a request that fails the same way everywhere.
|
|
151
151
|
*/
|
|
152
152
|
function cooldownFor(err: unknown): { kind: ErrorKind; forMs: number } | null {
|
|
@@ -179,8 +179,8 @@ function cooldownFor(err: unknown): { kind: ErrorKind; forMs: number } | null {
|
|
|
179
179
|
* Give any provider a rotating pool of keys.
|
|
180
180
|
*
|
|
181
181
|
* The subtlety is the seam's shape. `createStream` is an async generator, so
|
|
182
|
-
*
|
|
183
|
-
*
|
|
182
|
+
* calling it performs no I/O. The POST and the 429 that should rotate the key
|
|
183
|
+
* happen on the first `next()`, long after a `pool.with` wrapped around the
|
|
184
184
|
* call itself would have returned, with nothing left to rotate. So the first
|
|
185
185
|
* chunk is pulled INSIDE the pool and only the rest of the stream is consumed
|
|
186
186
|
* outside it.
|
package/src/presets.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Provider presets
|
|
2
|
+
* Provider presets: hand-curated, wire-level facts only.
|
|
3
3
|
*
|
|
4
4
|
* A preset is everything `createPresetProvider` needs to speak to one
|
|
5
5
|
* endpoint: which adapter shape it speaks, its API root, how the credential
|
|
6
|
-
* rides, and what it is called. Deliberately absent: prices (invariant 9
|
|
6
|
+
* rides, and what it is called. Deliberately absent: prices (invariant 9;
|
|
7
7
|
* they drift on a vendor's schedule and a wrong number in a library is a
|
|
8
8
|
* wrong number in everyone's ledger), icons, colors, marketing names, and
|
|
9
9
|
* sign-in flows (an `oauth` preset expects the CALLER to hand over an access
|
|
@@ -11,12 +11,12 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Sources: the coding-plan endpoints and auth quirks are measured
|
|
13
13
|
* (tabrunner's extension + this repo's adopters, 2026-09); the model lists
|
|
14
|
-
* are representative, not exhaustive
|
|
14
|
+
* are representative, not exhaustive. Every adapter takes a per-call model,
|
|
15
15
|
* so a missing id costs nothing.
|
|
16
16
|
*
|
|
17
17
|
* Azure is deliberately ABSENT: its base URL is per-deployment
|
|
18
|
-
* (`https://<resource>.openai.azure.com`), so no static preset can carry it
|
|
19
|
-
*
|
|
18
|
+
* (`https://<resource>.openai.azure.com`), so no static preset can carry it.
|
|
19
|
+
* Point createOpenAIProvider's baseUrl at your resource instead.
|
|
20
20
|
*/
|
|
21
21
|
import type { ImageLimits } from "./image.ts";
|
|
22
22
|
|
|
@@ -27,7 +27,7 @@ export type PresetShape = "anthropic" | "openai" | "responses" | "gemini";
|
|
|
27
27
|
* - `key`: the vendor's native key header (Anthropic reads `x-api-key`,
|
|
28
28
|
* Gemini `x-goog-api-key`); the OpenAI shapes have only Bearer, so they
|
|
29
29
|
* never use this.
|
|
30
|
-
* - `bearer`: `Authorization: Bearer
|
|
30
|
+
* - `bearer`: `Authorization: Bearer`, the OpenAI shapes' only mode, and the
|
|
31
31
|
* coding-plan gateways' mode on the Anthropic wire (measured: Z.ai's coding
|
|
32
32
|
* endpoint ignores `x-api-key` and reads Bearer).
|
|
33
33
|
* - `oauth`: same wire as `bearer`, but the token is a short-lived access
|
|
@@ -54,7 +54,7 @@ export interface PresetRoute {
|
|
|
54
54
|
export interface ProviderPreset {
|
|
55
55
|
/** Adapter wire format. */
|
|
56
56
|
shape: PresetShape;
|
|
57
|
-
/** API root
|
|
57
|
+
/** API root. If it ends with /v1, /v4 or another version segment, the
|
|
58
58
|
* adapter automatically routes to /chat/completions instead of /v1/chat/completions. */
|
|
59
59
|
baseUrl: string;
|
|
60
60
|
/** Explicit path override when a gateway has custom routing. */
|
|
@@ -71,8 +71,8 @@ export interface ProviderPreset {
|
|
|
71
71
|
textOnly?: boolean;
|
|
72
72
|
/** Anthropic-shape dialect: say "no thinking" with an explicit
|
|
73
73
|
* `thinking: { type: "disabled" }` marker when effort is none. For
|
|
74
|
-
* endpoints where an
|
|
75
|
-
* for reasoning-mandatory models (measured on Z.ai's coding endpoint:
|
|
74
|
+
* endpoints where an absent field means the model's default, with thinking
|
|
75
|
+
* on for reasoning-mandatory models (measured on Z.ai's coding endpoint:
|
|
76
76
|
* omit → thinking block; disabled → none). Leave it unset for native
|
|
77
77
|
* Anthropic: the adapter spells none per Claude model, and ignores this
|
|
78
78
|
* flag for every Claude from 4.6 on. */
|
|
@@ -82,7 +82,7 @@ export interface ProviderPreset {
|
|
|
82
82
|
* neither text nor a tool call. */
|
|
83
83
|
maxTokens?: number;
|
|
84
84
|
/** Header that carries `StreamOptions.sessionId`, for gateways that route
|
|
85
|
-
* and cache per conversation
|
|
85
|
+
* and cache per conversation and refuse a call without it (OpenCode Go:
|
|
86
86
|
* `MissingSessionID`, measured 2026-09-27). */
|
|
87
87
|
sessionHeader?: string;
|
|
88
88
|
/** Wires for model ids that don't use the preset's own `shape`. The first
|
|
@@ -103,6 +103,9 @@ export interface ProviderPreset {
|
|
|
103
103
|
/** OpenAI shape: send `X-Initiator` (`user` or `agent`) on every call. GitHub
|
|
104
104
|
* Copilot bills a premium request per `user` turn. */
|
|
105
105
|
initiatorHeader?: boolean;
|
|
106
|
+
/** OpenAI shape: never send `temperature` or `top_p`, for an endpoint that
|
|
107
|
+
* fixes its own sampling and answers the fields with an error. */
|
|
108
|
+
noSampling?: boolean;
|
|
106
109
|
}
|
|
107
110
|
|
|
108
111
|
export const PROVIDER_PRESETS = {
|
|
@@ -130,12 +133,13 @@ export const PROVIDER_PRESETS = {
|
|
|
130
133
|
defaultModel: "gemini-3.1-pro-preview",
|
|
131
134
|
models: ["gemini-3.1-pro-preview", "gemini-3.8-flash", "gemini-flash-latest"],
|
|
132
135
|
},
|
|
133
|
-
/** Gemini behind its OpenAI-compatible route
|
|
136
|
+
/** Gemini behind its OpenAI-compatible route, for codebases
|
|
134
137
|
* that only speak the openai shape. */
|
|
135
138
|
"google-openai": {
|
|
136
139
|
shape: "openai",
|
|
137
140
|
baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai",
|
|
138
141
|
auth: "key",
|
|
142
|
+
noSampling: true,
|
|
139
143
|
defaultModel: "gemini-3.1-pro-preview",
|
|
140
144
|
models: ["gemini-3.1-pro-preview", "gemini-3.8-flash", "gemini-flash-latest"],
|
|
141
145
|
},
|
|
@@ -357,7 +361,7 @@ export const PROVIDER_PRESETS = {
|
|
|
357
361
|
models: ["glm-5.3-flash", "glm-5.3", "glm-5.2"],
|
|
358
362
|
},
|
|
359
363
|
/**
|
|
360
|
-
* OpenCode Go
|
|
364
|
+
* OpenCode Go: one $10/month key across many open models, each with its own
|
|
361
365
|
* monthly dollar limit (5h = 20%, week = 50%). Chain several models as
|
|
362
366
|
* `fallbacks` so a spent one hands over to the next.
|
|
363
367
|
*
|
|
@@ -431,7 +435,7 @@ export const PROVIDER_PRESETS = {
|
|
|
431
435
|
defaultModel: "kimi-k2.7-code",
|
|
432
436
|
models: ["kimi-k2.7-code", "kimi-k2.7-code-highspeed", "kimi-k3"],
|
|
433
437
|
},
|
|
434
|
-
/** Alibaba's
|
|
438
|
+
/** Alibaba's DashScope coding plan, OpenAI-compatible per its registry. */
|
|
435
439
|
"alibaba-coding-plan": {
|
|
436
440
|
shape: "openai",
|
|
437
441
|
baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1",
|
|
@@ -439,7 +443,7 @@ export const PROVIDER_PRESETS = {
|
|
|
439
443
|
defaultModel: "qwen3.5-plus",
|
|
440
444
|
models: ["qwen3.5-plus", "qwen3-coder-next", "glm-5"],
|
|
441
445
|
},
|
|
442
|
-
/** QwenCloud's token plan
|
|
446
|
+
/** QwenCloud's token plan on the Anthropic wire (measured by tabrunner). */
|
|
443
447
|
qwen: {
|
|
444
448
|
shape: "anthropic",
|
|
445
449
|
baseUrl: "https://token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic",
|
|
@@ -447,7 +451,7 @@ export const PROVIDER_PRESETS = {
|
|
|
447
451
|
defaultModel: "qwen3.8-max",
|
|
448
452
|
models: ["qwen3.8-max", "qwen3.8-flash", "qwen3.6-flash"],
|
|
449
453
|
},
|
|
450
|
-
/** QwenCloud's token plan
|
|
454
|
+
/** QwenCloud's token plan on an OpenAI-compatible endpoint. */
|
|
451
455
|
"qwen-token-plan-openai": {
|
|
452
456
|
shape: "openai",
|
|
453
457
|
baseUrl: "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1",
|
|
@@ -455,7 +459,7 @@ export const PROVIDER_PRESETS = {
|
|
|
455
459
|
defaultModel: "qwen3.8-max",
|
|
456
460
|
models: ["qwen3.8-max", "qwen3.8-flash", "qwen3.6-flash"],
|
|
457
461
|
},
|
|
458
|
-
/** Alibaba DashScope Platform API
|
|
462
|
+
/** Alibaba DashScope Platform API, with a standard pay-as-you-go API key. */
|
|
459
463
|
alibaba: {
|
|
460
464
|
shape: "openai",
|
|
461
465
|
baseUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
|
|
@@ -501,8 +505,8 @@ export const PROVIDER_PRESETS = {
|
|
|
501
505
|
headers: { "anthropic-beta": "claude-code-20250219,oauth-2025-04-20" },
|
|
502
506
|
},
|
|
503
507
|
/**
|
|
504
|
-
* The Codex backend behind a ChatGPT sign-in
|
|
505
|
-
* model list, so these ids are the list.
|
|
508
|
+
* The Codex backend behind a ChatGPT sign-in, on the Responses wire. It has no
|
|
509
|
+
* public model list, so these ids are the list.
|
|
506
510
|
*
|
|
507
511
|
* The endpoint is `/backend-api/codex/responses`, with no version segment.
|
|
508
512
|
* Without the `path` the adapter appends `/v1/responses`, the POST 404s, and
|
|
@@ -540,8 +544,8 @@ export const PROVIDER_PRESETS = {
|
|
|
540
544
|
replayReasoning: true,
|
|
541
545
|
},
|
|
542
546
|
/**
|
|
543
|
-
* A Grok subscription (SuperGrok, X Premium)
|
|
544
|
-
* talks to, Responses wire. Distinct from `xai`, which is the keyed API and
|
|
547
|
+
* A Grok subscription (SuperGrok, X Premium), on the backend the Grok CLI
|
|
548
|
+
* talks to, using the Responses wire. Distinct from `xai`, which is the keyed API and
|
|
545
549
|
* bills per token.
|
|
546
550
|
*
|
|
547
551
|
* The two `x-…` headers are what switch the backend into CLI-token mode;
|
|
@@ -568,7 +572,7 @@ export const PROVIDER_PRESETS = {
|
|
|
568
572
|
imageLimits: { minSide: 8, minArea: 512, maxDecodedBytes: 5 * 1024 * 1024, maxImages: 4 },
|
|
569
573
|
},
|
|
570
574
|
/** Kimi's coding endpoint reached with a subscription token instead of a
|
|
571
|
-
* key
|
|
575
|
+
* key. Kimi bills the two separately. */
|
|
572
576
|
"kimi-plan": {
|
|
573
577
|
shape: "anthropic",
|
|
574
578
|
baseUrl: "https://api.kimi.ai/coding",
|
|
@@ -607,5 +611,5 @@ export const PROVIDER_PRESETS = {
|
|
|
607
611
|
|
|
608
612
|
export type ProviderPresetId = keyof typeof PROVIDER_PRESETS;
|
|
609
613
|
|
|
610
|
-
/** Every valid preset id
|
|
614
|
+
/** Every valid preset id. The factory's unknown-id error lists these. */
|
|
611
615
|
export const PRESET_IDS = Object.keys(PROVIDER_PRESETS) as ProviderPresetId[];
|
|
@@ -21,7 +21,7 @@ import type {
|
|
|
21
21
|
export interface AnthropicConfig extends ProviderFallbackConfig {
|
|
22
22
|
apiKey: string;
|
|
23
23
|
model: string;
|
|
24
|
-
/** Any endpoint speaking the Anthropic Messages dialect
|
|
24
|
+
/** Any endpoint speaking the Anthropic Messages dialect, including a proxy or gateway.
|
|
25
25
|
* Defaults to Anthropic itself. */
|
|
26
26
|
baseUrl?: string;
|
|
27
27
|
/** Names the provider in errors and logs. The subscription backend is the
|
|
@@ -38,7 +38,7 @@ export interface AnthropicConfig extends ProviderFallbackConfig {
|
|
|
38
38
|
* headers, and a gateway in front usually wants one of its own. */
|
|
39
39
|
headers?: Record<string, string>;
|
|
40
40
|
/**
|
|
41
|
-
* App attribution for OpenRouter's rankings
|
|
41
|
+
* App attribution for OpenRouter's rankings. The site URL rides as
|
|
42
42
|
* `HTTP-Referer`, the app name as `X-Title`. Public, not secret. Sent
|
|
43
43
|
* whenever set (other vendors ignore unknown headers); an explicit entry
|
|
44
44
|
* in `headers` always wins. Covers OpenRouter's Anthropic-dialect endpoint.
|
|
@@ -48,15 +48,15 @@ export interface AnthropicConfig extends ProviderFallbackConfig {
|
|
|
48
48
|
* App name for OpenRouter's rankings, sent as `X-Title`. See `siteUrl`.
|
|
49
49
|
*/
|
|
50
50
|
siteName?: string;
|
|
51
|
-
/** Send the key as a Bearer instead of `x-api-key
|
|
52
|
-
* access token
|
|
51
|
+
/** Send the key as a Bearer instead of `x-api-key`, as a subscription
|
|
52
|
+
* access token requires. */
|
|
53
53
|
bearer?: boolean;
|
|
54
54
|
/** Header that carries `StreamOptions.sessionId`. When set and a call has no
|
|
55
55
|
* sessionId, one stable id for this provider instance rides instead. */
|
|
56
56
|
sessionHeader?: string;
|
|
57
57
|
/** Say "no thinking" with an explicit `thinking: { type: "disabled" }` when
|
|
58
58
|
* effort resolves to none, instead of omitting the field. For endpoints
|
|
59
|
-
* where an absent field means the
|
|
59
|
+
* where an absent field means the model's default, not off. Z.ai's coding
|
|
60
60
|
* endpoint reads silence as thinking ON for reasoning-mandatory models like
|
|
61
61
|
* GLM 5.3 Flash (measured 2026-09-13: omit → thinking block; disabled →
|
|
62
62
|
* none), and it accepts the marker natively. Ignored for a Claude id that
|
|
@@ -72,9 +72,9 @@ const DEFAULT_VERSION = "2023-06-01";
|
|
|
72
72
|
const DEFAULT_MAX_TOKENS = 8_192;
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
|
-
* Thinking budgets, in output tokens, for extended thinking
|
|
75
|
+
* Thinking budgets, in output tokens, for extended thinking on Claude 4.5 and
|
|
76
76
|
* older, and every non-Claude endpoint on this wire. Thinking and the answer
|
|
77
|
-
*
|
|
77
|
+
* share `max_tokens`, so a budget is always left below the ceiling. A budget
|
|
78
78
|
* at or above it leaves no room to answer, and the turn ends mid-thought.
|
|
79
79
|
*/
|
|
80
80
|
const THINKING_BUDGET: Record<Exclude<Effort, "none">, number> = {
|
|
@@ -102,8 +102,8 @@ const LATEST: ClaudeThinking = { ...ALWAYS_ON, forcedTool: false };
|
|
|
102
102
|
|
|
103
103
|
/**
|
|
104
104
|
* Claude models that take adaptive thinking, keyed by id. A graded effort is
|
|
105
|
-
* said the same way on all of them
|
|
106
|
-
* `output_config.effort
|
|
105
|
+
* said the same way on all of them: `thinking: { type: "adaptive" }` plus
|
|
106
|
+
* `output_config.effort`, which every one accepts, and which the docs say to
|
|
107
107
|
* use wherever a model also takes `budget_tokens`. What differs is `none`,
|
|
108
108
|
* because what each model does when told nothing differs. Documented (Anthropic per-model table, read
|
|
109
109
|
* 2026-09-27: platform.claude.com/docs/en/build-with-claude/thinking-troubleshooting);
|
|
@@ -233,13 +233,13 @@ function partsToAnthropic(content: string | ContentPart[], provider: string): un
|
|
|
233
233
|
|
|
234
234
|
/**
|
|
235
235
|
* Anthropic takes `system` at the top level and expects tool RESULTS as user
|
|
236
|
-
* turns carrying `tool_result` blocks
|
|
236
|
+
* turns carrying `tool_result` blocks, not as a role of their own. Consecutive
|
|
237
237
|
* tool results are merged into one user turn, which the API requires.
|
|
238
238
|
*/
|
|
239
239
|
/**
|
|
240
240
|
* The system prompt as ONE cached block.
|
|
241
241
|
*
|
|
242
|
-
* Anthropic's prompt caching is opt-in
|
|
242
|
+
* Anthropic's prompt caching is opt-in per block. A plain string system prompt
|
|
243
243
|
* is never cached, however many times it is re-sent. An agent loop re-sends this
|
|
244
244
|
* every single turn, and it is the largest stable prefix in the request, so
|
|
245
245
|
* without the breakpoint the whole thing bills at the full input rate on every
|
|
@@ -247,14 +247,14 @@ function partsToAnthropic(content: string | ContentPart[], provider: string): un
|
|
|
247
247
|
*
|
|
248
248
|
* Unconditional. Below the model's minimum cacheable length the field is
|
|
249
249
|
* ignored rather than rejected, and above it the one-time 1.25× write is repaid
|
|
250
|
-
* by the second turn
|
|
250
|
+
* by the second turn, which always comes in the loop this package sits under.
|
|
251
251
|
*/
|
|
252
252
|
function systemBlocks(text: string): unknown[] | undefined {
|
|
253
253
|
return text ? [{ type: "text", text, cache_control: { type: "ephemeral" } }] : undefined;
|
|
254
254
|
}
|
|
255
255
|
|
|
256
256
|
/**
|
|
257
|
-
* The schema, as an extra system block
|
|
257
|
+
* The schema, as an extra system block. Anthropic has no native schema mode,
|
|
258
258
|
* and the seam promises that a provider without one gets the schema in the
|
|
259
259
|
* prompt instead. Without this an `opts.json` request went out carrying
|
|
260
260
|
* nothing at all: the model answered in prose, the caller's `JSON.parse` threw,
|
|
@@ -263,7 +263,7 @@ function systemBlocks(text: string): unknown[] | undefined {
|
|
|
263
263
|
* It rides AFTER the cached block, and that order is load-bearing. A cache
|
|
264
264
|
* breakpoint caches everything before it, so folding a per-call schema into the
|
|
265
265
|
* cached block would change the cached prefix on every turn whose schema
|
|
266
|
-
* differs and throw the whole system prompt's cache away
|
|
266
|
+
* differs and throw the whole system prompt's cache away, paying for the
|
|
267
267
|
* schema with the most expensive thing in an agent loop.
|
|
268
268
|
*/
|
|
269
269
|
function jsonBlock(json: JsonOutput): unknown {
|
package/src/providers/factory.ts
CHANGED
|
@@ -47,7 +47,7 @@ export interface PresetProviderConfig extends ProviderFallbackConfig {
|
|
|
47
47
|
* preset's static protocol headers. */
|
|
48
48
|
headers?: Record<string, string>;
|
|
49
49
|
/**
|
|
50
|
-
* App attribution for OpenRouter's rankings
|
|
50
|
+
* App attribution for OpenRouter's rankings. The site URL rides as
|
|
51
51
|
* `HTTP-Referer`, the app name as `X-Title`. Public, not secret. Sent on
|
|
52
52
|
* the OpenAI-compatible and Anthropic shapes (OpenRouter reads them for app
|
|
53
53
|
* attribution); the Gemini and Responses shapes ignore them. An explicit
|
|
@@ -60,7 +60,7 @@ export interface PresetProviderConfig extends ProviderFallbackConfig {
|
|
|
60
60
|
siteName?: string;
|
|
61
61
|
/** OpenRouter-only: preferred upstream hosts, in order. OpenRouter's cache
|
|
62
62
|
* lives on the upstream host's account and default routing hops hosts
|
|
63
|
-
* between rounds
|
|
63
|
+
* between rounds. Pinning keeps a conversation's rounds (and their cache)
|
|
64
64
|
* on one host. Empty/absent = default routing. */
|
|
65
65
|
providerOrder?: string[];
|
|
66
66
|
/** Default `service_tier` for every call on the OpenAI and Responses shapes;
|
|
@@ -92,7 +92,7 @@ export function createPresetProvider(id: ProviderPresetId, config: PresetProvide
|
|
|
92
92
|
}
|
|
93
93
|
const model = chain?.[0] ?? config.model ?? preset.defaultModel;
|
|
94
94
|
if (!model) {
|
|
95
|
-
throw new Error(`[providerkit] Preset "${id}" has no defaultModel
|
|
95
|
+
throw new Error(`[providerkit] Preset "${id}" has no defaultModel. Pass a model.`);
|
|
96
96
|
}
|
|
97
97
|
const wire = wireFor(preset, model);
|
|
98
98
|
if (config.serviceTier && (wire.shape === "anthropic" || wire.shape === "gemini")) {
|
|
@@ -149,6 +149,7 @@ export function createPresetProvider(id: ProviderPresetId, config: PresetProvide
|
|
|
149
149
|
...(config.providerOrder ? { providerOrder: config.providerOrder } : {}),
|
|
150
150
|
...(preset.sessionHeader ? { sessionHeader: preset.sessionHeader } : {}),
|
|
151
151
|
...(preset.initiatorHeader ? { initiatorHeader: true } : {}),
|
|
152
|
+
...(preset.noSampling ? { noSampling: true } : {}),
|
|
152
153
|
fetchImpl: config.fetchImpl,
|
|
153
154
|
});
|
|
154
155
|
break;
|
package/src/providers/gemini.ts
CHANGED
|
@@ -26,7 +26,7 @@ import type {
|
|
|
26
26
|
export interface GeminiConfig extends ProviderFallbackConfig {
|
|
27
27
|
apiKey: string;
|
|
28
28
|
model: string;
|
|
29
|
-
/** Any endpoint speaking the Generative Language REST dialect
|
|
29
|
+
/** Any endpoint speaking the Generative Language REST dialect, including a proxy or
|
|
30
30
|
* gateway. Defaults to the Generative Language API.
|
|
31
31
|
*
|
|
32
32
|
* Not Vertex: it serves `/v1/projects/…/locations/…/publishers/google/models`
|
|
@@ -39,7 +39,7 @@ export interface GeminiConfig extends ProviderFallbackConfig {
|
|
|
39
39
|
effort?: Effort;
|
|
40
40
|
/**
|
|
41
41
|
* How the schema rides on a call that also carries tools. See
|
|
42
|
-
* {@link JsonWithTools}
|
|
42
|
+
* {@link JsonWithTools}. Note that it is the model's answer, not the
|
|
43
43
|
* endpoint's: measured 2026-09-07, `gemini-3.5-flash` and
|
|
44
44
|
* `gemini-3.5-flash-lite` called their tool under `responseJsonSchema` every
|
|
45
45
|
* time, while `gemini-3.8-flash` managed 3/10. `probeJsonWithTools` asks the
|
|
@@ -70,9 +70,10 @@ const THINKING_LEVEL: Record<Effort, string> = {
|
|
|
70
70
|
};
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
|
-
* Gemini 2.x thinking budget dial (tokens).
|
|
74
|
-
*
|
|
75
|
-
* `thinkingLevel
|
|
73
|
+
* Gemini 2.x thinking budget dial (tokens). Gemini 2.5 takes `thinkingBudget`
|
|
74
|
+
* (0 disables thinking) and has no level. Every newer model takes
|
|
75
|
+
* `thinkingLevel`, and Google has said the next ones answer a budget with a
|
|
76
|
+
* 400 instead of remapping it.
|
|
76
77
|
*/
|
|
77
78
|
const GEMINI_2_THINKING_BUDGET: Record<Effort, number> = {
|
|
78
79
|
none: 0,
|
|
@@ -95,7 +96,7 @@ function isJsonObject(value: unknown): value is Record<string, unknown> {
|
|
|
95
96
|
/**
|
|
96
97
|
* Tool-call arguments as the OBJECT Gemini's `functionCall.args` requires.
|
|
97
98
|
*
|
|
98
|
-
* A bare JSON scalar or array is wrapped rather than rejected
|
|
99
|
+
* A bare JSON scalar or array is wrapped rather than rejected. The shared
|
|
99
100
|
* `parseToolArgs` drops a non-object as a protocol violation, which is right
|
|
100
101
|
* for reading a fresh tool call and wrong here, where replaying `{}` deletes an
|
|
101
102
|
* argument the model did send. Everything else defers to it, so a truncated or
|
|
@@ -205,7 +206,7 @@ export function toGeminiContents(messages: readonly ChatMessage[]): {
|
|
|
205
206
|
|
|
206
207
|
/**
|
|
207
208
|
* `auto` and an absent choice are Gemini's own default, so the field is omitted
|
|
208
|
-
* rather than sent as AUTO. `required` and a pinned tool are both mode ANY
|
|
209
|
+
* rather than sent as AUTO. `required` and a pinned tool are both mode ANY:
|
|
209
210
|
* the pin is the allow-list, not the mode.
|
|
210
211
|
*/
|
|
211
212
|
function toToolConfig(choice: ToolChoice | undefined): unknown {
|
|
@@ -236,7 +237,7 @@ interface GeminiPart {
|
|
|
236
237
|
}
|
|
237
238
|
|
|
238
239
|
/** Google's `google.rpc.Status`: an HTTP `code`, the canonical `status` name,
|
|
239
|
-
* and `details
|
|
240
|
+
* and `details`, which is where RetryInfo's `retryDelay` rides. */
|
|
240
241
|
interface GeminiStatus {
|
|
241
242
|
code?: number;
|
|
242
243
|
message?: string;
|
|
@@ -255,7 +256,7 @@ interface GeminiResponse {
|
|
|
255
256
|
thoughtsTokenCount?: number;
|
|
256
257
|
cachedContentTokenCount?: number;
|
|
257
258
|
};
|
|
258
|
-
/** Present only on the in-band failure below
|
|
259
|
+
/** Present only on the in-band failure below. Never on a real candidate. */
|
|
259
260
|
error?: GeminiStatus;
|
|
260
261
|
/** A prompt Gemini refused outright: no candidate at all, just the reason. */
|
|
261
262
|
promptFeedback?: { blockReason?: string };
|
|
@@ -283,21 +284,23 @@ export function createGeminiProvider(config: GeminiConfig): Provider {
|
|
|
283
284
|
// learn about a prefix.
|
|
284
285
|
const model = (opts.model ?? config.model).replace(/^models\//, "");
|
|
285
286
|
const isGemini3 = /gemini-3/i.test(model);
|
|
287
|
+
// Not `!isGemini3`: `gemini-flash-latest` and models newer than 3 are not
|
|
288
|
+
// Gemini 3 either, and they must get a level.
|
|
289
|
+
const isGemini2 = /gemini-2/i.test(model);
|
|
286
290
|
|
|
287
291
|
const generationConfig: Record<string, unknown> = {};
|
|
288
292
|
if (maxTokens !== undefined) generationConfig.maxOutputTokens = maxTokens;
|
|
289
|
-
|
|
290
|
-
|
|
293
|
+
// `temperature` and `topP` are never sent: Gemini 3.6 Flash and later ignore
|
|
294
|
+
// them and the next models reject them. Dropping them, rather than refusing,
|
|
295
|
+
// keeps a fallback chain that shares one set of options working.
|
|
291
296
|
if (opts.stopSequences?.length) generationConfig.stopSequences = opts.stopSequences;
|
|
292
297
|
// No effort means the model's own dynamic thinking. Sending MINIMAL / 0 here
|
|
293
298
|
// would switch that off for a caller who never asked, which is the whole
|
|
294
299
|
// reason the seam treats an absent effort as "never sent".
|
|
295
|
-
// Gemini 3 uses thinkingLevel (MINIMAL..HIGH), while Gemini 2.5 uses
|
|
296
|
-
// thinkingBudget (0 tokens disables thinking, positive numbers set budget).
|
|
297
300
|
if (effort) {
|
|
298
|
-
generationConfig.thinkingConfig =
|
|
299
|
-
? {
|
|
300
|
-
: {
|
|
301
|
+
generationConfig.thinkingConfig = isGemini2
|
|
302
|
+
? { thinkingBudget: GEMINI_2_THINKING_BUDGET[effort] }
|
|
303
|
+
: { thinkingLevel: THINKING_LEVEL[effort] };
|
|
301
304
|
}
|
|
302
305
|
// A schema that shares its call with tools pins the decoder, and a pinned
|
|
303
306
|
// decoder cannot emit a functionCall — the model narrates the call it
|