@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.
Files changed (92) hide show
  1. package/README.md +7 -7
  2. package/dist/attribution.d.ts +2 -2
  3. package/dist/capability.d.ts +9 -9
  4. package/dist/capability.js +9 -9
  5. package/dist/capability.js.map +1 -1
  6. package/dist/context.d.ts +4 -4
  7. package/dist/context.js +4 -4
  8. package/dist/errors.d.ts +15 -15
  9. package/dist/errors.d.ts.map +1 -1
  10. package/dist/errors.js +14 -14
  11. package/dist/image.d.ts +1 -1
  12. package/dist/image.d.ts.map +1 -1
  13. package/dist/jev.d.ts +9 -9
  14. package/dist/jev.d.ts.map +1 -1
  15. package/dist/jev.js +1 -1
  16. package/dist/key-pool.d.ts +4 -4
  17. package/dist/key-pool.js +6 -6
  18. package/dist/key-pool.js.map +1 -1
  19. package/dist/presets.d.ts +26 -22
  20. package/dist/presets.d.ts.map +1 -1
  21. package/dist/presets.js +13 -12
  22. package/dist/presets.js.map +1 -1
  23. package/dist/providers/anthropic.d.ts +5 -5
  24. package/dist/providers/anthropic.d.ts.map +1 -1
  25. package/dist/providers/anthropic.js +9 -9
  26. package/dist/providers/factory.d.ts +2 -2
  27. package/dist/providers/factory.d.ts.map +1 -1
  28. package/dist/providers/factory.js +2 -1
  29. package/dist/providers/factory.js.map +1 -1
  30. package/dist/providers/gemini.d.ts +2 -2
  31. package/dist/providers/gemini.d.ts.map +1 -1
  32. package/dist/providers/gemini.js +15 -14
  33. package/dist/providers/gemini.js.map +1 -1
  34. package/dist/providers/openai.d.ts +12 -9
  35. package/dist/providers/openai.d.ts.map +1 -1
  36. package/dist/providers/openai.js +14 -13
  37. package/dist/providers/openai.js.map +1 -1
  38. package/dist/providers/responses.d.ts +3 -3
  39. package/dist/providers/responses.js +5 -5
  40. package/dist/providers/responses.js.map +1 -1
  41. package/dist/providers/zai.d.ts +2 -2
  42. package/dist/providers/zai.js +2 -2
  43. package/dist/rate-limit.d.ts +3 -3
  44. package/dist/rate-limit.js +3 -3
  45. package/dist/retry.d.ts +5 -5
  46. package/dist/retry.js +4 -4
  47. package/dist/schema.d.ts +6 -6
  48. package/dist/schema.js +8 -8
  49. package/dist/tool-args.d.ts +1 -1
  50. package/dist/tool-args.js +2 -2
  51. package/dist/tools.d.ts +3 -3
  52. package/dist/tools.js +2 -2
  53. package/dist/tools.js.map +1 -1
  54. package/dist/transport.d.ts +7 -7
  55. package/dist/transport.js +6 -6
  56. package/dist/types.d.ts +24 -23
  57. package/dist/types.d.ts.map +1 -1
  58. package/dist/types.js +3 -3
  59. package/dist/types.js.map +1 -1
  60. package/dist/usage.d.ts +4 -4
  61. package/dist/usage.d.ts.map +1 -1
  62. package/dist/usage.js +3 -3
  63. package/dist/usage.js.map +1 -1
  64. package/dist/watchdog.d.ts +12 -12
  65. package/dist/watchdog.js +10 -10
  66. package/dist/zod.d.ts +4 -4
  67. package/dist/zod.js +3 -3
  68. package/package.json +1 -1
  69. package/src/attribution.ts +2 -2
  70. package/src/capability.ts +12 -12
  71. package/src/context.ts +4 -4
  72. package/src/errors.ts +18 -18
  73. package/src/image.ts +1 -1
  74. package/src/jev.ts +11 -11
  75. package/src/key-pool.ts +7 -7
  76. package/src/presets.ts +26 -22
  77. package/src/providers/anthropic.ts +14 -14
  78. package/src/providers/factory.ts +4 -3
  79. package/src/providers/gemini.ts +19 -16
  80. package/src/providers/openai.ts +22 -17
  81. package/src/providers/responses.ts +11 -11
  82. package/src/providers/zai.ts +2 -2
  83. package/src/rate-limit.ts +4 -4
  84. package/src/retry.ts +5 -5
  85. package/src/schema.ts +8 -8
  86. package/src/tool-args.ts +2 -2
  87. package/src/tools.ts +4 -4
  88. package/src/transport.ts +7 -7
  89. package/src/types.ts +24 -23
  90. package/src/usage.ts +5 -5
  91. package/src/watchdog.ts +13 -13
  92. 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 — `null` when its name says it all.
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 — NOT the probability of the pick.
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 — a choice's options carry through. */
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 — `null` when that answer did not pass the checks in
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 — `jev-1.13.0` on
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 — not that this account can
143
- * run this model — and Vercel has none. Throws the classified failure. */
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 — `{ model, answers, usage }` —
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 — no Vercel key has run it here. If a real reply disagrees, this
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 — or null.
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 — its own error copy, its own auth refresh — and can
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 — minutes will not bring it back. */
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 — benched only long enough for the
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 — the pool's only report, in place of a
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 — it would fail identically on every key, and spending the
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 — nothing is gained by rotating, and benching would spend the
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
- * 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
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 — hand-curated, wire-level facts only.
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 — every adapter takes a per-call model,
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
- * point createOpenAIProvider's baseUrl at your resource instead.
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` — the OpenAI shapes' only mode, and the
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 — if it ends with /v1, /v4 or another version segment, the
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 ABSENT field means the model's default — thinking ON
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 — and refuse a call without it (OpenCode Go:
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 — the drop-in for codebases
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 — one $10/month key across many open models, each with its own
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 dashscope coding plan — OpenAI-compatible per its registry. */
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 — Anthropic wire (measured by tabrunner). */
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 — OpenAI-compatible endpoint. */
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 — standard pay-as-you-go API key. */
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 — Responses wire, no public
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) — the backend the Grok CLI
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 — Kimi bills the two separately. */
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 — the factory's unknown-id error lists these. */
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 — a proxy or gateway.
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 — the site URL rides as
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` — what a subscription
52
- * access token needs. */
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 MODEL's default, not off — Z.ai's coding
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 — Claude 4.5 and
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
- * SHARE `max_tokens`, so a budget is always left below the ceiling — a budget
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 — `thinking: { type: "adaptive" }` plus
106
- * `output_config.effort` — which every one accepts, and which the docs say to
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 — not as a role of their own. Consecutive
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 PER BLOCK — a plain string system prompt
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 — which, in the loop this package sits under, always comes.
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 — Anthropic has no native schema mode,
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 — paying for the
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 {
@@ -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 — the site URL rides as
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 — pinning keeps a conversation's rounds (and their cache)
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 — pass a model.`);
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;
@@ -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 — a proxy or
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} — and note that it is the MODEL's answer, not the
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
- * Gemini 2.5 uses `thinkingBudget` (0 disables thinking), while Gemini 3+ uses
75
- * `thinkingLevel` (MINIMAL, LOW, MEDIUM, HIGH).
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 — the shared
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` — which is where RetryInfo's `retryDelay` rides. */
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 — never on a real candidate. */
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
- if (opts.temperature !== undefined) generationConfig.temperature = opts.temperature;
290
- if (opts.topP !== undefined) generationConfig.topP = opts.topP;
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 = isGemini3
299
- ? { thinkingLevel: THINKING_LEVEL[effort] }
300
- : { thinkingBudget: GEMINI_2_THINKING_BUDGET[effort] };
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