@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.
- package/dist/errors.d.ts +30 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +94 -0
- package/dist/errors.js.map +1 -1
- package/dist/presets.d.ts +46 -0
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +46 -0
- package/dist/presets.js.map +1 -1
- package/dist/providers/anthropic.js +2 -2
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/gemini.d.ts.map +1 -1
- package/dist/providers/gemini.js +28 -10
- package/dist/providers/gemini.js.map +1 -1
- package/dist/providers/openai.d.ts +6 -0
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +54 -3
- package/dist/providers/openai.js.map +1 -1
- package/dist/retry.d.ts.map +1 -1
- package/dist/retry.js +2 -2
- package/dist/retry.js.map +1 -1
- package/dist/schema.d.ts +20 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +112 -0
- package/dist/schema.js.map +1 -1
- package/dist/tool-args.d.ts +13 -1
- package/dist/tool-args.d.ts.map +1 -1
- package/dist/tool-args.js +50 -1
- package/dist/tool-args.js.map +1 -1
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +3 -0
- package/dist/transport.js.map +1 -1
- package/dist/usage.d.ts +6 -0
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js +20 -0
- package/dist/usage.js.map +1 -1
- package/package.json +2 -1
- package/src/errors.ts +103 -0
- package/src/presets.ts +46 -0
- package/src/providers/anthropic.ts +2 -2
- package/src/providers/gemini.ts +30 -10
- package/src/providers/openai.ts +59 -3
- package/src/retry.ts +8 -2
- package/src/schema.ts +127 -0
- package/src/tool-args.ts +48 -1
- package/src/transport.ts +4 -0
- 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;
|
package/dist/usage.d.ts.map
CHANGED
|
@@ -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":"AASA,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAmBzC,6BAA6B;AAC7B,MAAM,UAAU,QAAQ,CAAC,CAAa,EAAE,CAAa;IACnD,OAAO;QACL,WAAW,EAAE,CAAC,CAAC,WAAW,GAAG,CAAC,CAAC,WAAW;QAC1C,iBAAiB,EAAE,CAAC,CAAC,iBAAiB,GAAG,CAAC,CAAC,iBAAiB;QAC5D,gBAAgB,EAAE,CAAC,CAAC,CAAC,gBAAgB,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,gBAAgB,IAAI,CAAC,CAAC;QACvE,YAAY,EAAE,CAAC,CAAC,YAAY,GAAG,CAAC,CAAC,YAAY;KAC9C,CAAC;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,OAAO,CAAC,KAAiB,EAAE,IAAe;IACxD,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,WAAW,CAAC,CAAC;IAC7C,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,iBAAiB,CAAC,EAAE,KAAK,CAAC,CAAC;IACrE,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,gBAAgB,IAAI,CAAC,CAAC,CAAC;IACzD,MAAM,IAAI,GAAG,KAAK,GAAG,MAAM,CAAC;IAC5B,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,KAAK,CAAC;IAChD,OAAO,CACL,CAAC,IAAI,GAAG,IAAI,CAAC,KAAK;QAChB,MAAM,GAAG,IAAI,CAAC,SAAS;QACvB,OAAO,GAAG,SAAS;QACnB,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,YAAY,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC;QAChD,SAAS,CACV,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,OAAO,YAAY;IACf,KAAK,GAAe,EAAE,GAAG,WAAW,EAAE,gBAAgB,EAAE,CAAC,EAAE,CAAC;IAC5D,IAAI,GAAG,CAAC,CAAC;IACjB;6CACyC;IACjC,KAAK,GAAG,CAAC,CAAC;IAElB,GAAG,CAAC,KAAiB,EAAE,IAAgB;QACrC,IAAI,CAAC,KAAK,GAAG,QAAQ,CAAC,IAAI,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QACzC,IAAI,CAAC,IAAI;YAAE,OAAO;QAClB,IAAI,CAAC,IAAI,IAAI,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QAClC,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,iBAAiB,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC;QAC9F,IAAI,CAAC,KAAK,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,GAAG,SAAS,CAAC;IACrE,CAAC;IAED,IAAI,MAAM;QACR,OAAO,EAAE,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC;IAC3B,CAAC;IAED,IAAI,OAAO;QACT,OAAO,IAAI,CAAC,IAAI,CAAC;IACnB,CAAC;IAED,IAAI,eAAe;QACjB,OAAO,IAAI,CAAC,KAAK,CAAC;IACpB,CAAC;IAED;;yDAEqD;IACrD,YAAY,CAAC,MAAc;QACzB,OAAO,IAAI,CAAC,IAAI,IAAI,MAAM,CAAC;IAC7B,CAAC;IAED,KAAK;QACH,IAAI,CAAC,KAAK,GAAG,EAAE,GAAG,WAAW,EAAE,gBAAgB,EAAE,CAAC,EAAE,CAAC;QACrD,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC;QACd,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC;IACjB,CAAC;CACF"}
|
|
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.
|
|
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") {
|
package/src/providers/gemini.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
package/src/providers/openai.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
402
|
-
|
|
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 {
|
|
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 =>
|
|
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 —
|