@providerkit/core 0.9.0 → 0.10.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 (46) hide show
  1. package/dist/errors.d.ts +30 -0
  2. package/dist/errors.d.ts.map +1 -1
  3. package/dist/errors.js +94 -0
  4. package/dist/errors.js.map +1 -1
  5. package/dist/presets.d.ts +46 -0
  6. package/dist/presets.d.ts.map +1 -1
  7. package/dist/presets.js +46 -0
  8. package/dist/presets.js.map +1 -1
  9. package/dist/providers/anthropic.js +2 -2
  10. package/dist/providers/anthropic.js.map +1 -1
  11. package/dist/providers/gemini.d.ts.map +1 -1
  12. package/dist/providers/gemini.js +28 -10
  13. package/dist/providers/gemini.js.map +1 -1
  14. package/dist/providers/openai.d.ts +6 -0
  15. package/dist/providers/openai.d.ts.map +1 -1
  16. package/dist/providers/openai.js +54 -3
  17. package/dist/providers/openai.js.map +1 -1
  18. package/dist/retry.d.ts.map +1 -1
  19. package/dist/retry.js +2 -2
  20. package/dist/retry.js.map +1 -1
  21. package/dist/schema.d.ts +20 -0
  22. package/dist/schema.d.ts.map +1 -1
  23. package/dist/schema.js +112 -0
  24. package/dist/schema.js.map +1 -1
  25. package/dist/tool-args.d.ts +13 -1
  26. package/dist/tool-args.d.ts.map +1 -1
  27. package/dist/tool-args.js +50 -1
  28. package/dist/tool-args.js.map +1 -1
  29. package/dist/transport.d.ts.map +1 -1
  30. package/dist/transport.js +3 -0
  31. package/dist/transport.js.map +1 -1
  32. package/dist/usage.d.ts +6 -0
  33. package/dist/usage.d.ts.map +1 -1
  34. package/dist/usage.js +20 -0
  35. package/dist/usage.js.map +1 -1
  36. package/package.json +2 -1
  37. package/src/errors.ts +103 -0
  38. package/src/presets.ts +46 -0
  39. package/src/providers/anthropic.ts +2 -2
  40. package/src/providers/gemini.ts +30 -10
  41. package/src/providers/openai.ts +59 -3
  42. package/src/retry.ts +8 -2
  43. package/src/schema.ts +127 -0
  44. package/src/tool-args.ts +48 -1
  45. package/src/transport.ts +4 -0
  46. package/src/usage.ts +21 -0
package/dist/usage.d.ts CHANGED
@@ -17,6 +17,11 @@ export interface ModelRate {
17
17
  }
18
18
  /** Add two usage records. */
19
19
  export declare function addUsage(a: TokenUsage, b: TokenUsage): TokenUsage;
20
+ /**
21
+ * Subtract usage `b` from `a`, clamping each field at zero.
22
+ * Used when an optimistic turn or speculative stream is rolled back.
23
+ */
24
+ export declare function subtractUsage(a: TokenUsage, b: TokenUsage): TokenUsage;
20
25
  /**
21
26
  * USD for one call.
22
27
  *
@@ -38,6 +43,7 @@ export declare class UsageTracker {
38
43
  * Observability only — never billed. */
39
44
  private saved;
40
45
  add(usage: TokenUsage, rate?: ModelRate): void;
46
+ subtract(usage: TokenUsage, rate?: ModelRate): void;
41
47
  get totals(): TokenUsage;
42
48
  get costUsd(): number;
43
49
  get cacheSavingsUsd(): number;
@@ -1 +1 @@
1
- {"version":3,"file":"usage.d.ts","sourceRoot":"","sources":["../src/usage.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAG7C;qDACqD;AACrD,MAAM,WAAW,SAAS;IACxB,+DAA+D;IAC/D,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf;;;wEAGoE;IACpE,SAAS,EAAE,MAAM,CAAC;IAClB;;0EAEsE;IACtE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,6BAA6B;AAC7B,wBAAgB,QAAQ,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,UAAU,GAAG,UAAU,CAOjE;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,GAAG,MAAM,CAalE;AAED;;;;GAIG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,KAAK,CAAuD;IACpE,OAAO,CAAC,IAAI,CAAK;IACjB;6CACyC;IACzC,OAAO,CAAC,KAAK,CAAK;IAElB,GAAG,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,CAAC,EAAE,SAAS,GAAG,IAAI;IAQ9C,IAAI,MAAM,IAAI,UAAU,CAEvB;IAED,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,IAAI,eAAe,IAAI,MAAM,CAE5B;IAED;;yDAEqD;IACrD,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAIrC,KAAK,IAAI,IAAI;CAKd"}
1
+ {"version":3,"file":"usage.d.ts","sourceRoot":"","sources":["../src/usage.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAG7C;qDACqD;AACrD,MAAM,WAAW,SAAS;IACxB,+DAA+D;IAC/D,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf;;;wEAGoE;IACpE,SAAS,EAAE,MAAM,CAAC;IAClB;;0EAEsE;IACtE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,6BAA6B;AAC7B,wBAAgB,QAAQ,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,UAAU,GAAG,UAAU,CAOjE;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,UAAU,GAAG,UAAU,CAOtE;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,GAAG,MAAM,CAalE;AAED;;;;GAIG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,KAAK,CAAuD;IACpE,OAAO,CAAC,IAAI,CAAK;IACjB;6CACyC;IACzC,OAAO,CAAC,KAAK,CAAK;IAElB,GAAG,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,CAAC,EAAE,SAAS,GAAG,IAAI;IAQ9C,QAAQ,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,CAAC,EAAE,SAAS,GAAG,IAAI;IAQnD,IAAI,MAAM,IAAI,UAAU,CAEvB;IAED,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,IAAI,eAAe,IAAI,MAAM,CAE5B;IAED;;yDAEqD;IACrD,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAIrC,KAAK,IAAI,IAAI;CAKd"}
package/dist/usage.js CHANGED
@@ -8,6 +8,18 @@ export function addUsage(a, b) {
8
8
  outputTokens: a.outputTokens + b.outputTokens,
9
9
  };
10
10
  }
11
+ /**
12
+ * Subtract usage `b` from `a`, clamping each field at zero.
13
+ * Used when an optimistic turn or speculative stream is rolled back.
14
+ */
15
+ export function subtractUsage(a, b) {
16
+ return {
17
+ inputTokens: Math.max(0, a.inputTokens - b.inputTokens),
18
+ cachedInputTokens: Math.max(0, a.cachedInputTokens - b.cachedInputTokens),
19
+ cacheWriteTokens: Math.max(0, (a.cacheWriteTokens ?? 0) - (b.cacheWriteTokens ?? 0)),
20
+ outputTokens: Math.max(0, a.outputTokens - b.outputTokens),
21
+ };
22
+ }
11
23
  /**
12
24
  * USD for one call.
13
25
  *
@@ -47,6 +59,14 @@ export class UsageTracker {
47
59
  const cached = Math.min(Math.max(0, usage.cachedInputTokens), Math.max(0, usage.inputTokens));
48
60
  this.saved += (cached * (rate.input - rate.cacheRead)) / 1_000_000;
49
61
  }
62
+ subtract(usage, rate) {
63
+ this.usage = subtractUsage(this.usage, usage);
64
+ if (!rate)
65
+ return;
66
+ this.cost = Math.max(0, this.cost - costUsd(usage, rate));
67
+ const cached = Math.min(Math.max(0, usage.cachedInputTokens), Math.max(0, usage.inputTokens));
68
+ this.saved = Math.max(0, this.saved - (cached * (rate.input - rate.cacheRead)) / 1_000_000);
69
+ }
50
70
  get totals() {
51
71
  return { ...this.usage };
52
72
  }
package/dist/usage.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"usage.js","sourceRoot":"","sources":["../src/usage.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"usage.js","sourceRoot":"","sources":["../src/usage.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@providerkit/core",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "The layer under your agent loop: one seam for every LLM provider, plus the failure handling you only learn in production.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -48,6 +48,7 @@
48
48
  "lint": "eslint src test",
49
49
  "format": "prettier --write .",
50
50
  "check:presets": "bun run scripts/refresh-presets.ts",
51
+ "probe:model": "bun run scripts/probe-model.ts",
51
52
  "sync:docs": "cp ../README.md ../LICENSE .",
52
53
  "prepublishOnly": "bun run lint && bun run typecheck && bun run test && bun run build && bun run sync:docs",
53
54
  "release": "bun publish --access public",
package/src/errors.ts CHANGED
@@ -53,6 +53,17 @@ const TRANSIENT: ReadonlySet<ErrorKind> = new Set<ErrorKind>([
53
53
  * overload are per-model-endpoint; nothing else on this list is. */
54
54
  const BACKUP_ELIGIBLE: ReadonlySet<ErrorKind> = new Set<ErrorKind>(["overload", "rate"]);
55
55
 
56
+ /** Kinds that a retry on the same endpoint can never resolve. */
57
+ const NON_RETRYABLE_KINDS: ReadonlySet<ErrorKind> = new Set<ErrorKind>([
58
+ "quota",
59
+ "context",
60
+ "model",
61
+ "entitlement",
62
+ "content",
63
+ "invalid",
64
+ "aborted",
65
+ ]);
66
+
56
67
  export function isTransient(kind: ErrorKind): boolean {
57
68
  return TRANSIENT.has(kind);
58
69
  }
@@ -61,6 +72,35 @@ export function isBackupEligible(kind: ErrorKind): boolean {
61
72
  return BACKUP_ELIGIBLE.has(kind);
62
73
  }
63
74
 
75
+ /**
76
+ * Decides whether any failure (ProviderError, thrown Error, TypeError, HTTP response)
77
+ * is worth a retry.
78
+ *
79
+ * Checks in order:
80
+ * 1. Caller aborts are never retryable.
81
+ * 2. An explicit `shouldRetry` directive on ProviderError (e.g. from server `x-should-retry`)
82
+ * 3. Non-retryable kinds (quota, context, model, entitlement, content, invalid) return false.
83
+ * 4. Excessive retry-after waits (exceeding `maxWaitMs`) return false — long waits belong to fallbacks.
84
+ * 5. Transient kinds (rate, overload, network, timeout) return true.
85
+ * 6. Transport-level network faults (fetch failed, connection lost, SSL blips) return true.
86
+ */
87
+ export function isRetryable(err: unknown, maxWaitMs = 60_000): boolean {
88
+ if (isAbort(err)) return false;
89
+
90
+ if (err instanceof ProviderError) {
91
+ if (err.shouldRetry !== undefined) return err.shouldRetry;
92
+ if (NON_RETRYABLE_KINDS.has(err.kind)) return false;
93
+ if (err.retryAfterMs !== undefined && err.retryAfterMs > maxWaitMs) return false;
94
+ return err.isTransient;
95
+ }
96
+
97
+ if (isTransportFailure(err)) return true;
98
+
99
+ const kind = classify(err);
100
+ if (NON_RETRYABLE_KINDS.has(kind)) return false;
101
+ return isTransient(kind);
102
+ }
103
+
64
104
  export class ProviderError extends Error {
65
105
  readonly provider: string;
66
106
  readonly kind: ErrorKind;
@@ -72,6 +112,8 @@ export class ProviderError extends Error {
72
112
  /** Absolute reset of the binding limit. Can be days beyond Retry-After. */
73
113
  readonly resetAtMs?: number;
74
114
  readonly window?: RateLimitWindow;
115
+ /** Explicit server directive (`x-should-retry`), overriding default classification. */
116
+ readonly shouldRetry?: boolean;
75
117
  /** The provider's own response body, truncated — the actual reason, which is
76
118
  * otherwise lost behind "400 status code (no body)". */
77
119
  readonly body?: string;
@@ -86,6 +128,7 @@ export class ProviderError extends Error {
86
128
  retryAfterMs?: number;
87
129
  resetAtMs?: number;
88
130
  window?: RateLimitWindow;
131
+ shouldRetry?: boolean;
89
132
  body?: string;
90
133
  cause?: unknown;
91
134
  } = {},
@@ -99,10 +142,12 @@ export class ProviderError extends Error {
99
142
  this.retryAfterMs = opts.retryAfterMs;
100
143
  this.resetAtMs = opts.resetAtMs;
101
144
  this.window = opts.window;
145
+ this.shouldRetry = opts.shouldRetry;
102
146
  this.body = opts.body;
103
147
  }
104
148
 
105
149
  get isTransient(): boolean {
150
+ if (this.shouldRetry !== undefined) return this.shouldRetry;
106
151
  return isTransient(this.kind);
107
152
  }
108
153
 
@@ -293,6 +338,10 @@ const CONTEXT_PATTERNS: readonly RegExp[] = [
293
338
  /input is too long/i,
294
339
  /too many (?:input )?tokens/i,
295
340
  /reduce the length of the (?:messages|prompt|input)/i,
341
+ // Anthropic's exact wording when input + max_tokens overflows context limit:
342
+ // "input length and `max_tokens` exceed context limit: 188059 + 20000 > 200000"
343
+ /input length and `?max_tokens`? exceed context limit/i,
344
+ /exceed(?:s|ed)? (?:the )?context limit/i,
296
345
  // Scoped to the thing that overflowed: a bare "exceeds the maximum" also
297
346
  // covers image counts and per-minute token rates, which compaction can't fix.
298
347
  /exceeds? the (?:model'?s? )?maximum (?:input |prompt |context )?(?:tokens?|length|context)/i,
@@ -310,6 +359,7 @@ const ENTITLEMENT_PATTERNS: readonly RegExp[] = [
310
359
  /subscription[_\s]?required/i,
311
360
  /upgrade for access/i,
312
361
  /no api access/i,
362
+ /OAuth authentication is currently not allowed for this organization/i,
313
363
  // The ChatGPT backend's error code, which arrives with no prose at all.
314
364
  /usage_not_included/i,
315
365
  /usage not included/i,
@@ -356,6 +406,7 @@ const AUTH_PATTERNS: readonly RegExp[] = [
356
406
  /account (?:disabled|suspended|deactivated|banned)/i,
357
407
  /unrecognizedclient|unauthorizedexception|accessdeniedexception/i,
358
408
  /expired[_\s]?token/i,
409
+ /OAuth token has been revoked/i,
359
410
  ];
360
411
 
361
412
  const CONTENT_PATTERNS: readonly RegExp[] = [
@@ -363,6 +414,11 @@ const CONTENT_PATTERNS: readonly RegExp[] = [
363
414
  /content policy/i,
364
415
  /cyber[_\s]policy/i,
365
416
  /safety|PROHIBITED_CONTENT|blocked|refusal|policy violation|misalignment/i,
417
+ // Document and media boundary violations
418
+ /maximum of \d+ PDF pages/i,
419
+ /PDF specified is password protected/i,
420
+ /image(?:s)? exceed(?:s)? .* maximum/i,
421
+ /image dimensions exceed/i,
366
422
  ];
367
423
 
368
424
  /**
@@ -511,6 +567,53 @@ export function parseRetryAfterMs(err: unknown, body?: string): number | undefin
511
567
  return Number.isNaN(date) ? undefined : Math.max(0, date - Date.now());
512
568
  }
513
569
 
570
+ export interface ContextOverflowInfo {
571
+ inputTokens?: number;
572
+ maxTokens?: number;
573
+ contextLimit: number;
574
+ excessTokens: number;
575
+ }
576
+
577
+ /**
578
+ * Extracts context overflow details from provider error messages.
579
+ *
580
+ * Covers:
581
+ * - Anthropic / Bedrock: `input length and \`max_tokens\` exceed context limit: 188059 + 20000 > 200000`
582
+ * - Standard / Gateway: `prompt is too long: 137500 tokens > 135000 maximum`
583
+ */
584
+ export function parseContextOverflow(err: unknown, body?: string): ContextOverflowInfo | undefined {
585
+ const text = body ?? bodyTextOf(err) ?? messageOf(err);
586
+ if (!text) return undefined;
587
+
588
+ // Anthropic / Bedrock: "input length and `max_tokens` exceed context limit: 188059 + 20000 > 200000"
589
+ const m1 = /(?:input length and [`']?max_tokens[`']? exceed context limit|exceeds context limit):\s*(\d+)\s*\+\s*(\d+)\s*>\s*(\d+)/i.exec(text);
590
+ if (m1) {
591
+ const input = parseInt(m1[1]!, 10);
592
+ const max = parseInt(m1[2]!, 10);
593
+ const limit = parseInt(m1[3]!, 10);
594
+ return {
595
+ inputTokens: input,
596
+ maxTokens: max,
597
+ contextLimit: limit,
598
+ excessTokens: input + max - limit,
599
+ };
600
+ }
601
+
602
+ // "prompt is too long: 137500 tokens > 135000 maximum"
603
+ const m2 = /(?:prompt is too long|context length exceeded|maximum context length is \d+ tokens?)[^:]*:\s*(\d+)\s*(?:tokens)?\s*>\s*(\d+)/i.exec(text);
604
+ if (m2) {
605
+ const actual = parseInt(m2[1]!, 10);
606
+ const limit = parseInt(m2[2]!, 10);
607
+ return {
608
+ inputTokens: actual,
609
+ contextLimit: limit,
610
+ excessTokens: actual - limit,
611
+ };
612
+ }
613
+
614
+ return undefined;
615
+ }
616
+
514
617
  /**
515
618
  * A failure the backend reports INSIDE an already-200 stream.
516
619
  *
package/src/presets.ts CHANGED
@@ -177,6 +177,13 @@ export const PROVIDER_PRESETS = {
177
177
  defaultModel: "MiniMax-M3",
178
178
  models: ["MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2.5"],
179
179
  },
180
+ "minimax-openai": {
181
+ shape: "openai",
182
+ baseUrl: "https://api.minimax.io/v1",
183
+ auth: "key",
184
+ defaultModel: "MiniMax-M3",
185
+ models: ["MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2.5"],
186
+ },
180
187
 
181
188
  // ── Popular gateways & cloud providers ──────────────────────────────────
182
189
  nvidia: {
@@ -284,6 +291,14 @@ export const PROVIDER_PRESETS = {
284
291
  // (`z-ai/glm-5.3-flash`) answers 400 [1211] Unknown Model.
285
292
  explicitNone: true,
286
293
  },
294
+ "zai-openai": {
295
+ shape: "openai",
296
+ baseUrl: "https://api.z.ai/api/paas/v4",
297
+ path: "/chat/completions",
298
+ auth: "key",
299
+ defaultModel: "glm-5.3-flash",
300
+ models: ["glm-5.3-flash", "glm-5.3", "glm-5.2"],
301
+ },
287
302
  kimi: {
288
303
  shape: "anthropic",
289
304
  baseUrl: "https://api.kimi.ai/coding",
@@ -291,6 +306,13 @@ export const PROVIDER_PRESETS = {
291
306
  defaultModel: "kimi-for-coding",
292
307
  models: ["kimi-for-coding", "kimi-for-coding-highspeed", "k3", "k3-256k"],
293
308
  },
309
+ "kimi-openai": {
310
+ shape: "openai",
311
+ baseUrl: "https://api.moonshot.ai/v1",
312
+ auth: "key",
313
+ defaultModel: "kimi-k2.7-code",
314
+ models: ["kimi-k2.7-code", "kimi-k2.7-code-highspeed", "kimi-k3"],
315
+ },
294
316
  /** Alibaba's dashscope coding plan — OpenAI-compatible per its registry. */
295
317
  "alibaba-coding-plan": {
296
318
  shape: "openai",
@@ -307,6 +329,30 @@ export const PROVIDER_PRESETS = {
307
329
  defaultModel: "qwen3.8-max",
308
330
  models: ["qwen3.8-max", "qwen3.8-flash", "qwen3.6-flash"],
309
331
  },
332
+ /** QwenCloud's token plan — OpenAI-compatible endpoint. */
333
+ "qwen-token-plan-openai": {
334
+ shape: "openai",
335
+ baseUrl: "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1",
336
+ auth: "bearer",
337
+ defaultModel: "qwen3.8-max",
338
+ models: ["qwen3.8-max", "qwen3.8-flash", "qwen3.6-flash"],
339
+ },
340
+ /** Alibaba DashScope Platform API — standard pay-as-you-go API key. */
341
+ alibaba: {
342
+ shape: "openai",
343
+ baseUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
344
+ auth: "bearer",
345
+ defaultModel: "qwen-max",
346
+ models: ["qwen-max", "qwen-plus", "qwen-turbo", "qwen-flash"],
347
+ },
348
+ /** Alias for alibaba (DashScope platform API). */
349
+ "qwen-api": {
350
+ shape: "openai",
351
+ baseUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
352
+ auth: "bearer",
353
+ defaultModel: "qwen-max",
354
+ models: ["qwen-max", "qwen-plus", "qwen-turbo", "qwen-flash"],
355
+ },
310
356
 
311
357
  // ── Local runtimes ──────────────────────────────────────────────────────
312
358
  ollama: {
@@ -1,6 +1,6 @@
1
1
  // Anthropic-shape adapter — SSE from POST /v1/messages.
2
2
  import { streamError } from "../errors.ts";
3
- import { schemaPrompt } from "../schema.ts";
3
+ import { schemaPrompt, toAnthropicToolSchema } from "../schema.ts";
4
4
  import { parseToolArgs } from "../tool-args.ts";
5
5
  import { streamSse, apiUrl } from "../transport.ts";
6
6
  import { withConfiguredFallbacks, type ProviderFallbackConfig } from "../fallback.ts";
@@ -249,7 +249,7 @@ export function createAnthropicProvider(config: AnthropicConfig): Provider {
249
249
  request.tools = tools.map((tool) => ({
250
250
  name: tool.name,
251
251
  description: tool.description,
252
- input_schema: tool.inputSchema,
252
+ input_schema: toAnthropicToolSchema(tool.inputSchema),
253
253
  }));
254
254
  }
255
255
  if (opts.toolChoice && opts.toolChoice !== "auto") {
@@ -6,7 +6,7 @@
6
6
  // wire rather than @google/genai, because this package ships zero dependencies
7
7
  // and the SDK is a Node-shaped one.
8
8
  import { streamError } from "../errors.ts";
9
- import { schemaPrompt } from "../schema.ts";
9
+ import { schemaPrompt, toGeminiToolSchema } from "../schema.ts";
10
10
  import { parseToolArgs } from "../tool-args.ts";
11
11
  import { streamSse, apiUrl } from "../transport.ts";
12
12
  import { withConfiguredFallbacks, type ProviderFallbackConfig } from "../fallback.ts";
@@ -69,6 +69,19 @@ const THINKING_LEVEL: Record<Effort, string> = {
69
69
  max: "HIGH",
70
70
  };
71
71
 
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).
76
+ */
77
+ const GEMINI_2_THINKING_BUDGET: Record<Effort, number> = {
78
+ none: 0,
79
+ low: 1024,
80
+ medium: 4096,
81
+ high: 8192,
82
+ max: 16384,
83
+ };
84
+
72
85
  /** A turn in Gemini's history. `model` is its word for the assistant. */
73
86
  export interface GeminiContent {
74
87
  role: "user" | "model";
@@ -261,15 +274,27 @@ export function createGeminiProvider(config: GeminiConfig): Provider {
261
274
  const maxTokens = opts.maxTokens ?? config.maxTokens;
262
275
  const { system, contents } = toGeminiContents(messages);
263
276
 
277
+ // A model id copied out of Gemini's docs is often already `models/…`, and
278
+ // the doubled segment 404s as "model not found" — a confusing way to
279
+ // learn about a prefix.
280
+ const model = (opts.model ?? config.model).replace(/^models\//, "");
281
+ const isGemini3 = /gemini-3/i.test(model);
282
+
264
283
  const generationConfig: Record<string, unknown> = {};
265
284
  if (maxTokens !== undefined) generationConfig.maxOutputTokens = maxTokens;
266
285
  if (opts.temperature !== undefined) generationConfig.temperature = opts.temperature;
267
286
  if (opts.topP !== undefined) generationConfig.topP = opts.topP;
268
287
  if (opts.stopSequences?.length) generationConfig.stopSequences = opts.stopSequences;
269
- // No effort means the model's own dynamic thinking. Sending MINIMAL here
288
+ // No effort means the model's own dynamic thinking. Sending MINIMAL / 0 here
270
289
  // would switch that off for a caller who never asked, which is the whole
271
290
  // reason the seam treats an absent effort as "never sent".
272
- if (effort) generationConfig.thinkingConfig = { thinkingLevel: THINKING_LEVEL[effort] };
291
+ // Gemini 3 uses thinkingLevel (MINIMAL..HIGH), while Gemini 2.5 uses
292
+ // thinkingBudget (0 tokens disables thinking, positive numbers set budget).
293
+ if (effort) {
294
+ generationConfig.thinkingConfig = isGemini3
295
+ ? { thinkingLevel: THINKING_LEVEL[effort] }
296
+ : { thinkingBudget: GEMINI_2_THINKING_BUDGET[effort] };
297
+ }
273
298
  // A schema that shares its call with tools pins the decoder, and a pinned
274
299
  // decoder cannot emit a functionCall — the model narrates the call it
275
300
  // could not make and the turn ends, with nothing logged. See
@@ -281,7 +306,7 @@ export function createGeminiProvider(config: GeminiConfig): Provider {
281
306
  // `responseJsonSchema` takes JSON Schema as written; `responseSchema`
282
307
  // is Gemini's own trimmed dialect and rejects most of what a real
283
308
  // schema carries.
284
- generationConfig.responseJsonSchema = opts.json.schema;
309
+ generationConfig.responseJsonSchema = toGeminiToolSchema(opts.json.schema);
285
310
  }
286
311
 
287
312
  const request: Record<string, unknown> = { contents, generationConfig };
@@ -302,7 +327,7 @@ export function createGeminiProvider(config: GeminiConfig): Provider {
302
327
  description: tool.description,
303
328
  // Same rule as the response schema: `parameters` is the trimmed
304
329
  // dialect, `parametersJsonSchema` is the schema we actually wrote.
305
- parametersJsonSchema: tool.inputSchema,
330
+ parametersJsonSchema: toGeminiToolSchema(tool.inputSchema),
306
331
  })),
307
332
  },
308
333
  ];
@@ -312,11 +337,6 @@ export function createGeminiProvider(config: GeminiConfig): Provider {
312
337
  if (toolConfig) request.toolConfig = toolConfig;
313
338
  }
314
339
 
315
- // A model id copied out of Gemini's docs is often already `models/…`, and
316
- // the doubled segment 404s as "model not found" — a confusing way to
317
- // learn about a prefix.
318
- const model = (opts.model ?? config.model).replace(/^models\//, "");
319
-
320
340
  // Gemini reports the finish reason on a candidate that can arrive AFTER
321
341
  // the chunk carrying the function calls, so a turn's tool use has to be
322
342
  // remembered rather than read off the final chunk.
@@ -83,10 +83,56 @@ export interface OpenAIConfig extends ProviderFallbackConfig {
83
83
  * not a lock.
84
84
  */
85
85
  providerOrder?: string[];
86
+ /**
87
+ * Automatically pin OpenRouter calls to the first-party model vendor's host
88
+ * when hitting openrouter.ai and providerOrder is not passed. Defaults to true.
89
+ */
90
+ pinHost?: boolean;
86
91
  }
87
92
 
88
93
  const DEFAULT_BASE_URL = "https://api.openai.com";
89
94
 
95
+ /**
96
+ * Known first-party host slugs on OpenRouter for route pinning.
97
+ * Pinning keeps consecutive turns on the vendor's own endpoint, preserving
98
+ * the server-side KV prompt cache across rounds.
99
+ */
100
+ const OPENROUTER_HOSTS: Record<string, string> = {
101
+ anthropic: "anthropic",
102
+ "arcee-ai": "arcee-ai",
103
+ cohere: "cohere",
104
+ deepseek: "deepseek",
105
+ google: "google-ai-studio",
106
+ meta: "meta",
107
+ minimax: "minimax",
108
+ mistralai: "mistral",
109
+ moonshotai: "moonshotai",
110
+ morph: "morph",
111
+ openai: "openai",
112
+ perplexity: "perplexity",
113
+ qwen: "alibaba",
114
+ stepfun: "stepfun",
115
+ tencent: "tencent",
116
+ upstage: "upstage",
117
+ "x-ai": "xai",
118
+ "z-ai": "z-ai",
119
+ zai: "z-ai",
120
+ };
121
+
122
+ export function openRouterHostFor(baseUrl: string, model: string): string | undefined {
123
+ let host: string;
124
+ try {
125
+ host = new URL(baseUrl).hostname;
126
+ } catch {
127
+ return undefined;
128
+ }
129
+ if (!host.includes("openrouter.ai")) return undefined;
130
+ const slash = model.indexOf("/");
131
+ if (slash <= 0) return undefined;
132
+ const vendor = model.slice(0, slash).toLowerCase();
133
+ return OPENROUTER_HOSTS[vendor];
134
+ }
135
+
90
136
  /** The spellings of "think this hard" across the dialects that share this
91
137
  * adapter. `off` sends nothing and leaves the model on its own default. */
92
138
  export type EffortDialect = "openai" | "openrouter" | "deepseek" | "off";
@@ -316,10 +362,11 @@ export function createOpenAIProvider(config: OpenAIConfig): Provider {
316
362
  opts: StreamOptions = {},
317
363
  ): AsyncIterable<ProviderChunk> {
318
364
  const effort = opts.effort ?? config.effort;
365
+ const model = opts.model ?? config.model;
319
366
 
320
367
  const body = toOpenAIMessages(messages);
321
368
  const request: Record<string, unknown> = {
322
- model: opts.model ?? config.model,
369
+ model,
323
370
  messages: body,
324
371
  stream: true,
325
372
  // Without this the usage record never arrives and every call costs
@@ -398,8 +445,17 @@ export function createOpenAIProvider(config: OpenAIConfig): Provider {
398
445
  body.push({ role: "system", content: schemaPrompt(opts.json.schema) });
399
446
  }
400
447
  }
401
- if (config.providerOrder?.length) {
402
- request.provider = { order: config.providerOrder, allow_fallbacks: true };
448
+ const pinOrder = config.providerOrder?.length
449
+ ? config.providerOrder
450
+ : config.pinHost !== false
451
+ ? (() => {
452
+ const pinned = openRouterHostFor(baseUrl, model);
453
+ return pinned ? [pinned] : undefined;
454
+ })()
455
+ : undefined;
456
+
457
+ if (pinOrder?.length) {
458
+ request.provider = { order: pinOrder, allow_fallbacks: true };
403
459
  }
404
460
 
405
461
  const defaultPath = /\/v\d+[^/]*$/i.test(baseUrl.replace(/\/+$/, ""))
package/src/retry.ts CHANGED
@@ -13,7 +13,13 @@
13
13
  //
14
14
  // 3. Honour the provider's own number. When it says `Retry-After: 30`, a
15
15
  // one-second backoff is three wasted attempts before the same wait.
16
- import { classify, isBackupEligible, isTransient, parseRetryAfterMs, ProviderError } from "./errors.ts";
16
+ import {
17
+ classify,
18
+ isBackupEligible,
19
+ isRetryable,
20
+ parseRetryAfterMs,
21
+ ProviderError,
22
+ } from "./errors.ts";
17
23
 
18
24
  export interface RetryOptions {
19
25
  /** Total attempts including the first. Default 3. */
@@ -83,7 +89,7 @@ function delayFor(err: unknown, attempt: number, opts: RetryOptions): number | n
83
89
  return backoffMs(attempt, opts.baseDelayMs ?? DEFAULT_BASE_DELAY_MS, cap);
84
90
  }
85
91
 
86
- const defaultShouldRetry = (err: unknown): boolean => isTransient(classify(err));
92
+ const defaultShouldRetry = (err: unknown, _attempt: number): boolean => isRetryable(err);
87
93
 
88
94
  /**
89
95
  * Run `fn`, retrying transient failures with backoff. For one-shot calls —