@providerkit/core 0.21.1 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -7
- package/dist/attribution.d.ts +2 -2
- package/dist/auth/flows.d.ts.map +1 -1
- package/dist/auth/flows.js +5 -5
- package/dist/auth/flows.js.map +1 -1
- package/dist/auth/index.d.ts +1 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +1 -0
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/oauth.d.ts +12 -0
- package/dist/auth/oauth.d.ts.map +1 -1
- package/dist/auth/oauth.js +19 -1
- package/dist/auth/oauth.js.map +1 -1
- package/dist/auth/token-source.d.ts +9 -3
- package/dist/auth/token-source.d.ts.map +1 -1
- package/dist/auth/token-source.js +24 -9
- package/dist/auth/token-source.js.map +1 -1
- package/dist/auth/with-token.d.ts +29 -0
- package/dist/auth/with-token.d.ts.map +1 -0
- package/dist/auth/with-token.js +85 -0
- package/dist/auth/with-token.js.map +1 -0
- package/dist/capability.d.ts +9 -9
- package/dist/capability.js +9 -9
- package/dist/capability.js.map +1 -1
- package/dist/context.d.ts +43 -5
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +123 -4
- package/dist/context.js.map +1 -1
- package/dist/errors.d.ts +26 -17
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +57 -18
- package/dist/errors.js.map +1 -1
- package/dist/fallback.d.ts +45 -1
- package/dist/fallback.d.ts.map +1 -1
- package/dist/fallback.js +138 -75
- package/dist/fallback.js.map +1 -1
- package/dist/image.d.ts +1 -1
- package/dist/image.d.ts.map +1 -1
- package/dist/jev.d.ts +9 -9
- package/dist/jev.d.ts.map +1 -1
- package/dist/jev.js +1 -1
- package/dist/key-pool.d.ts +14 -8
- package/dist/key-pool.d.ts.map +1 -1
- package/dist/key-pool.js +35 -18
- package/dist/key-pool.js.map +1 -1
- package/dist/presets.d.ts +33 -24
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +25 -14
- package/dist/presets.js.map +1 -1
- package/dist/providers/anthropic.d.ts +5 -5
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +89 -34
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/factory.d.ts +2 -2
- package/dist/providers/factory.d.ts.map +1 -1
- package/dist/providers/factory.js +2 -1
- package/dist/providers/factory.js.map +1 -1
- package/dist/providers/gemini.d.ts +8 -2
- package/dist/providers/gemini.d.ts.map +1 -1
- package/dist/providers/gemini.js +117 -41
- package/dist/providers/gemini.js.map +1 -1
- package/dist/providers/openai.d.ts +12 -9
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +72 -21
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/responses.d.ts +3 -3
- package/dist/providers/responses.d.ts.map +1 -1
- package/dist/providers/responses.js +103 -57
- package/dist/providers/responses.js.map +1 -1
- package/dist/providers/zai.d.ts +2 -2
- package/dist/providers/zai.js +2 -2
- package/dist/rate-limit.d.ts +13 -3
- package/dist/rate-limit.d.ts.map +1 -1
- package/dist/rate-limit.js +10 -4
- package/dist/rate-limit.js.map +1 -1
- package/dist/retry.d.ts +16 -5
- package/dist/retry.d.ts.map +1 -1
- package/dist/retry.js +29 -16
- package/dist/retry.js.map +1 -1
- package/dist/schema.d.ts +6 -6
- package/dist/schema.js +8 -8
- package/dist/tool-args.d.ts +23 -1
- package/dist/tool-args.d.ts.map +1 -1
- package/dist/tool-args.js +30 -2
- package/dist/tool-args.js.map +1 -1
- package/dist/tools.d.ts +3 -3
- package/dist/tools.js +2 -2
- package/dist/tools.js.map +1 -1
- package/dist/transport.d.ts +40 -10
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +82 -21
- package/dist/transport.js.map +1 -1
- package/dist/types.d.ts +37 -23
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +3 -11
- package/dist/types.js.map +1 -1
- package/dist/usage.d.ts +4 -4
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js +3 -3
- package/dist/usage.js.map +1 -1
- package/dist/watchdog.d.ts +14 -12
- package/dist/watchdog.d.ts.map +1 -1
- package/dist/watchdog.js +35 -16
- package/dist/watchdog.js.map +1 -1
- package/dist/zod.d.ts +4 -4
- package/dist/zod.js +3 -3
- package/package.json +1 -1
- package/src/attribution.ts +2 -2
- package/src/auth/flows.ts +5 -4
- package/src/auth/index.ts +1 -0
- package/src/auth/oauth.ts +23 -1
- package/src/auth/token-source.ts +30 -11
- package/src/auth/with-token.ts +97 -0
- package/src/capability.ts +12 -12
- package/src/context.ts +141 -6
- package/src/errors.ts +63 -23
- package/src/fallback.ts +173 -75
- package/src/image.ts +1 -1
- package/src/jev.ts +11 -11
- package/src/key-pool.ts +37 -20
- package/src/presets.ts +38 -24
- package/src/providers/anthropic.ts +97 -38
- package/src/providers/factory.ts +4 -3
- package/src/providers/gemini.ts +121 -44
- package/src/providers/openai.ts +78 -25
- package/src/providers/responses.ts +113 -61
- package/src/providers/zai.ts +2 -2
- package/src/rate-limit.ts +35 -7
- package/src/retry.ts +46 -15
- package/src/schema.ts +8 -8
- package/src/tool-args.ts +46 -2
- package/src/tools.ts +4 -4
- package/src/transport.ts +103 -23
- package/src/types.ts +37 -23
- package/src/usage.ts +5 -5
- package/src/watchdog.ts +38 -19
- package/src/zod.ts +4 -4
package/dist/watchdog.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"watchdog.js","sourceRoot":"","sources":["../src/watchdog.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,8BAA8B;AAC9B,EAAE;AACF,uBAAuB;AACvB,EAAE;AACF,+EAA+E;AAC/E,6EAA6E;AAC7E,+EAA+E;AAC/E,wEAAwE;AACxE,EAAE;AACF,6EAA6E;AAC7E,EAAE;AACF,uEAAuE;AACvE,2EAA2E;AAC3E,wEAAwE;AACxE,gFAAgF;AAChF,8EAA8E;AAC9E,6EAA6E;AAC7E,wEAAwE;AACxE,8EAA8E;AAC9E,+EAA+E;AAC/E,8EAA8E;AAC9E,2EAA2E;AAC3E,gEAAgE;AAChE,EAAE;AACF,6EAA6E;AAC7E,
|
|
1
|
+
{"version":3,"file":"watchdog.js","sourceRoot":"","sources":["../src/watchdog.ts"],"names":[],"mappings":"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"}
|
package/dist/zod.d.ts
CHANGED
|
@@ -5,8 +5,8 @@ import type { JsonObjectSchema } from "./types.ts";
|
|
|
5
5
|
* A zod schema as the JSON Schema every provider's tool contract wants.
|
|
6
6
|
*
|
|
7
7
|
* `$schema` is dropped. zod emits the dialect URI at the root, and a provider
|
|
8
|
-
* validating a tool's `parameters` against its own supported subset
|
|
9
|
-
* under `strict: true`, Gemini's `parametersJsonSchema`
|
|
8
|
+
* validating a tool's `parameters` against its own supported subset (OpenAI
|
|
9
|
+
* under `strict: true`, Gemini's `parametersJsonSchema`) rejects the whole
|
|
10
10
|
* tool over that one key, with a message that names neither zod nor the field.
|
|
11
11
|
*/
|
|
12
12
|
export declare function toJsonObjectSchema(schema: z.ZodType, label?: string): JsonObjectSchema;
|
|
@@ -26,13 +26,13 @@ export interface ZodToolSpec<I, O> {
|
|
|
26
26
|
* rejecting the call. Worth it for a TERMINAL tool, which gets no second
|
|
27
27
|
* chance: a forced-submit salvage turn runs exactly once, and discarding an
|
|
28
28
|
* otherwise-valid answer over a few extra characters loses the whole run.
|
|
29
|
-
* Off by default
|
|
29
|
+
* Off by default: an ordinary tool can simply be called again.
|
|
30
30
|
*/
|
|
31
31
|
clampOverflow?: boolean;
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
34
34
|
* A tool whose arguments are validated by zod, with the failure reported to
|
|
35
|
-
* the
|
|
35
|
+
* the model in words it can act on. `topic: expected string, received number`
|
|
36
36
|
* beats a stack trace it cannot read.
|
|
37
37
|
*/
|
|
38
38
|
export declare function zodTool<I, O>(spec: ZodToolSpec<I, O>): Tool<I, O>;
|
package/dist/zod.js
CHANGED
|
@@ -10,8 +10,8 @@ import { clampToSchema } from "./schema.js";
|
|
|
10
10
|
* A zod schema as the JSON Schema every provider's tool contract wants.
|
|
11
11
|
*
|
|
12
12
|
* `$schema` is dropped. zod emits the dialect URI at the root, and a provider
|
|
13
|
-
* validating a tool's `parameters` against its own supported subset
|
|
14
|
-
* under `strict: true`, Gemini's `parametersJsonSchema`
|
|
13
|
+
* validating a tool's `parameters` against its own supported subset (OpenAI
|
|
14
|
+
* under `strict: true`, Gemini's `parametersJsonSchema`) rejects the whole
|
|
15
15
|
* tool over that one key, with a message that names neither zod nor the field.
|
|
16
16
|
*/
|
|
17
17
|
export function toJsonObjectSchema(schema, label = "schema") {
|
|
@@ -26,7 +26,7 @@ export function toJsonObjectSchema(schema, label = "schema") {
|
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
28
|
* A tool whose arguments are validated by zod, with the failure reported to
|
|
29
|
-
* the
|
|
29
|
+
* the model in words it can act on. `topic: expected string, received number`
|
|
30
30
|
* beats a stack trace it cannot read.
|
|
31
31
|
*/
|
|
32
32
|
export function zodTool(spec) {
|
package/package.json
CHANGED
package/src/attribution.ts
CHANGED
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
// HTTP header names are case-insensitive but this seam carries them as a
|
|
10
10
|
// plain record, so `http-referer` and `HTTP-Referer` must mean the same opt-out.
|
|
11
11
|
export interface Attribution {
|
|
12
|
-
/** Site URL, sent as `HTTP-Referer`
|
|
12
|
+
/** Site URL, sent as `HTTP-Referer` (e.g. "https://your.app"). */
|
|
13
13
|
siteUrl?: string;
|
|
14
|
-
/** App name, sent as `X-Title`
|
|
14
|
+
/** App name, sent as `X-Title` (e.g. "Your App"). */
|
|
15
15
|
siteName?: string;
|
|
16
16
|
}
|
|
17
17
|
|
package/src/auth/flows.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
accountFromToken,
|
|
9
9
|
asRecord,
|
|
10
10
|
authError,
|
|
11
|
+
authFetch,
|
|
11
12
|
generatePKCE,
|
|
12
13
|
jwtClaims,
|
|
13
14
|
num,
|
|
@@ -308,7 +309,7 @@ export function copilotHeaders(appName: string, turn: "user" | "agent"): Record<
|
|
|
308
309
|
}
|
|
309
310
|
|
|
310
311
|
async function mintCopilotToken(host: AuthHost, githubToken: string): Promise<Credential> {
|
|
311
|
-
const res = await (host
|
|
312
|
+
const res = await authFetch(host, COPILOT_TOKEN_URL, {
|
|
312
313
|
headers: {
|
|
313
314
|
Accept: "application/json",
|
|
314
315
|
Authorization: `Bearer ${githubToken}`,
|
|
@@ -361,7 +362,7 @@ async function refreshCopilot(host: AuthHost, credential: Credential): Promise<C
|
|
|
361
362
|
* lookup did, so any failure means no name. */
|
|
362
363
|
async function githubLogin(host: AuthHost, githubToken: string): Promise<string | undefined> {
|
|
363
364
|
try {
|
|
364
|
-
const res = await (host
|
|
365
|
+
const res = await authFetch(host, GITHUB_USER_URL, {
|
|
365
366
|
headers: { Accept: "application/json", Authorization: `Bearer ${githubToken}` },
|
|
366
367
|
});
|
|
367
368
|
return res.ok ? str(asRecord(await res.json()).login) : undefined;
|
|
@@ -405,7 +406,7 @@ const META_KEY_URL = "https://api.meta.ai/muse-code/key";
|
|
|
405
406
|
const META_KEY_LIFETIME_MS = 24 * 60 * 60 * 1000;
|
|
406
407
|
|
|
407
408
|
async function mintMetaKey(host: AuthHost, identityToken: string): Promise<Credential> {
|
|
408
|
-
const res = await (host
|
|
409
|
+
const res = await authFetch(host, META_KEY_URL, {
|
|
409
410
|
method: "POST",
|
|
410
411
|
headers: {
|
|
411
412
|
Accept: "application/json",
|
|
@@ -494,7 +495,7 @@ function openrouterFlow(host: AuthHost): AuthFlow {
|
|
|
494
495
|
redirectUri: OPENROUTER.redirectUri,
|
|
495
496
|
signal,
|
|
496
497
|
});
|
|
497
|
-
const res = await (host
|
|
498
|
+
const res = await authFetch(host, OPENROUTER.keysUrl, {
|
|
498
499
|
method: "POST",
|
|
499
500
|
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
500
501
|
body: JSON.stringify({ code, code_verifier: verifier, code_challenge_method: "S256" }),
|
package/src/auth/index.ts
CHANGED
package/src/auth/oauth.ts
CHANGED
|
@@ -25,6 +25,8 @@ export interface AuthHost {
|
|
|
25
25
|
}): Promise<string>;
|
|
26
26
|
/** Replaces `fetch`, for tests. */
|
|
27
27
|
fetchImpl?: typeof fetch;
|
|
28
|
+
/** How long one request to a sign-in server may take, in ms. Default 30 s. */
|
|
29
|
+
timeoutMs?: number;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
/** The stored result of a sign-in. */
|
|
@@ -76,6 +78,26 @@ export function authError(
|
|
|
76
78
|
/** Refresh this long before the server's own expiry. */
|
|
77
79
|
export const REFRESH_SKEW_MS = 5 * 60 * 1000;
|
|
78
80
|
|
|
81
|
+
/** The deadline cc-proxy puts on its own token requests. */
|
|
82
|
+
const AUTH_TIMEOUT_MS = 30_000;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* `fetch` with a deadline, for every request in `auth/`.
|
|
86
|
+
*
|
|
87
|
+
* A sign-in server that accepts the connection and never answers would hold
|
|
88
|
+
* the one shared refresh forever, and every call waiting on it with it. No
|
|
89
|
+
* clock above this one cuts it: the watchdog only starts once the model request
|
|
90
|
+
* is sent. The timeout rejects with a `TimeoutError`, which is not a refusal,
|
|
91
|
+
* so `tokenSource` keeps the stored credential.
|
|
92
|
+
*/
|
|
93
|
+
export function authFetch(host: AuthHost, url: string, init: RequestInit = {}): Promise<Response> {
|
|
94
|
+
const deadline = AbortSignal.timeout(host.timeoutMs ?? AUTH_TIMEOUT_MS);
|
|
95
|
+
return (host.fetchImpl ?? fetch)(url, {
|
|
96
|
+
...init,
|
|
97
|
+
signal: init.signal ? AbortSignal.any([init.signal, deadline]) : deadline,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
79
101
|
export const str = (value: unknown): string | undefined =>
|
|
80
102
|
typeof value === "string" && value.length > 0 ? value : undefined;
|
|
81
103
|
|
|
@@ -119,7 +141,7 @@ export async function postToken(
|
|
|
119
141
|
opts: { encode: "json" | "form"; allowErrorBody?: boolean },
|
|
120
142
|
): Promise<Record<string, unknown>> {
|
|
121
143
|
const json = opts.encode === "json";
|
|
122
|
-
const res = await (host
|
|
144
|
+
const res = await authFetch(host, url, {
|
|
123
145
|
method: "POST",
|
|
124
146
|
headers: {
|
|
125
147
|
"Content-Type": json ? "application/json" : "application/x-www-form-urlencoded",
|
package/src/auth/token-source.ts
CHANGED
|
@@ -16,12 +16,16 @@ export interface TokenSourceOptions<C extends Credential = Credential> {
|
|
|
16
16
|
|
|
17
17
|
export interface TokenSource<C extends Credential = Credential> {
|
|
18
18
|
/** A credential whose `accessToken` is good now. Throws a `ProviderError`
|
|
19
|
-
* coded `refresh_dead` when the user must sign in again.
|
|
20
|
-
|
|
19
|
+
* coded `refresh_dead` when the user must sign in again.
|
|
20
|
+
*
|
|
21
|
+
* Pass `rejected`, the access token a server just answered 401 to, to renew
|
|
22
|
+
* it even though it has not expired. A server can refuse a token early: it
|
|
23
|
+
* was revoked, or another client rotated it. */
|
|
24
|
+
getToken(opts?: { rejected?: string }): Promise<C>;
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
/**
|
|
24
|
-
* Refresh when the stored credential has expired, and only then.
|
|
28
|
+
* Refresh when the stored credential has expired or was rejected, and only then.
|
|
25
29
|
*
|
|
26
30
|
* - Concurrent calls share one refresh and the result is saved before use.
|
|
27
31
|
* - When the server rejects the refresh, the stored credential is reloaded. If
|
|
@@ -36,7 +40,20 @@ export interface TokenSource<C extends Credential = Credential> {
|
|
|
36
40
|
export function tokenSource<C extends Credential>(options: TokenSourceOptions<C>): TokenSource<C> {
|
|
37
41
|
let inFlight: Promise<C> | undefined;
|
|
38
42
|
|
|
39
|
-
async function
|
|
43
|
+
async function loadSignedIn(): Promise<C> {
|
|
44
|
+
const credential = await options.load();
|
|
45
|
+
if (!credential) throw authError("refresh_dead", "Not signed in. Sign in first.");
|
|
46
|
+
return credential;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function refreshStored(rejected: string | undefined): Promise<C> {
|
|
50
|
+
// Read again inside the single flight. A caller whose read raced a refresh
|
|
51
|
+
// that had just saved would otherwise start a second one with the refresh
|
|
52
|
+
// token the first already spent. Five calls refused on one token is the
|
|
53
|
+
// normal case for that race, and a server that rotates refresh tokens may
|
|
54
|
+
// take a reused one as stolen and end the whole sign-in.
|
|
55
|
+
const stale = await loadSignedIn();
|
|
56
|
+
if (isUsable(stale, rejected)) return stale;
|
|
40
57
|
try {
|
|
41
58
|
const fresh = await options.refresh(stale);
|
|
42
59
|
await options.save(fresh);
|
|
@@ -50,13 +67,10 @@ export function tokenSource<C extends Credential>(options: TokenSourceOptions<C>
|
|
|
50
67
|
}
|
|
51
68
|
|
|
52
69
|
return {
|
|
53
|
-
async getToken() {
|
|
54
|
-
const credential = await
|
|
55
|
-
if (
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
if (Date.now() < credential.expiresAt) return credential;
|
|
59
|
-
inFlight ??= refreshStored(credential).finally(() => {
|
|
70
|
+
async getToken({ rejected } = {}) {
|
|
71
|
+
const credential = await loadSignedIn();
|
|
72
|
+
if (isUsable(credential, rejected)) return credential;
|
|
73
|
+
inFlight ??= refreshStored(rejected).finally(() => {
|
|
60
74
|
inFlight = undefined;
|
|
61
75
|
});
|
|
62
76
|
return inFlight;
|
|
@@ -64,6 +78,11 @@ export function tokenSource<C extends Credential>(options: TokenSourceOptions<C>
|
|
|
64
78
|
};
|
|
65
79
|
}
|
|
66
80
|
|
|
81
|
+
/** Unexpired, and not the token a server just refused. */
|
|
82
|
+
function isUsable(credential: Credential, rejected: string | undefined): boolean {
|
|
83
|
+
return Date.now() < credential.expiresAt && credential.accessToken !== rejected;
|
|
84
|
+
}
|
|
85
|
+
|
|
67
86
|
/** A refresh the server turned down for good, as opposed to one that never
|
|
68
87
|
* reached it or came back unreadable. */
|
|
69
88
|
function isRejected(error: unknown): error is ProviderError {
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// A provider that signs every call with the stored sign-in, and renews the token
|
|
2
|
+
// once when a server refuses it before it expired. The sibling of
|
|
3
|
+
// `withKeyPool`: that one rotates between keys, this one renews the one token.
|
|
4
|
+
//
|
|
5
|
+
// Type-only imports from the sign-in side, so core's root can export this
|
|
6
|
+
// without pulling the flows in.
|
|
7
|
+
import { ProviderError } from "../errors.ts";
|
|
8
|
+
import type { PresetProviderConfig } from "../providers/factory.ts";
|
|
9
|
+
import type { Provider } from "../types.ts";
|
|
10
|
+
import type { Credential } from "./oauth.ts";
|
|
11
|
+
import type { TokenSource } from "./token-source.ts";
|
|
12
|
+
|
|
13
|
+
/** The credential half of `PresetProviderConfig`, so `build` can spread it in. */
|
|
14
|
+
export type TokenAuth = Pick<PresetProviderConfig, "apiKey" | "baseUrl" | "headers">;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Give any provider the signed-in user's token, read fresh on every call.
|
|
18
|
+
*
|
|
19
|
+
* ```ts
|
|
20
|
+
* const provider = withToken(token, (auth) => createPresetProvider("github-copilot", auth));
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* `build` runs once per attempt and must do no I/O, like `withKeyPool`'s
|
|
24
|
+
* factory. It gets the access token as `apiKey`, the credential's own host as
|
|
25
|
+
* `baseUrl` (Copilot gives each plan its own) and its `ChatGPT-Account-Id`
|
|
26
|
+
* header, which the Codex backend refuses the request without. To add headers
|
|
27
|
+
* of your own, merge them with `auth.headers` rather than replacing them.
|
|
28
|
+
*
|
|
29
|
+
* A 401 before any output renews the token and replays the call once: a token
|
|
30
|
+
* can be refused before its expiry, and without this the caller sees an auth
|
|
31
|
+
* failure that a fallback pool benches for an hour. A second 401 is a real one
|
|
32
|
+
* and surfaces. A 401 after output never replays (invariant 1).
|
|
33
|
+
*
|
|
34
|
+
* Only 401 counts. A 400 or 403 on a model call is a bad request or a plan
|
|
35
|
+
* that lacks the model, and a new token fixes neither.
|
|
36
|
+
*/
|
|
37
|
+
export function withToken<C extends Credential>(
|
|
38
|
+
source: TokenSource<C>,
|
|
39
|
+
build: (auth: TokenAuth) => Provider,
|
|
40
|
+
): Provider {
|
|
41
|
+
// Building does no I/O, so a throwaway instance reads `id` and `model`
|
|
42
|
+
// without a token.
|
|
43
|
+
const identity = build({ apiKey: "" });
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
id: identity.id,
|
|
47
|
+
model: identity.model,
|
|
48
|
+
|
|
49
|
+
async *createStream(messages, tools, opts = {}) {
|
|
50
|
+
const credential = await unlessAborted(source.getToken(), opts.signal);
|
|
51
|
+
let emitted = false;
|
|
52
|
+
try {
|
|
53
|
+
for await (const chunk of build(authOf(credential)).createStream(messages, tools, opts)) {
|
|
54
|
+
emitted = true;
|
|
55
|
+
yield chunk;
|
|
56
|
+
}
|
|
57
|
+
return;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (emitted || !(error instanceof ProviderError) || error.status !== 401) throw error;
|
|
60
|
+
}
|
|
61
|
+
const renewed = await unlessAborted(
|
|
62
|
+
source.getToken({ rejected: credential.accessToken }),
|
|
63
|
+
opts.signal,
|
|
64
|
+
);
|
|
65
|
+
yield* build(authOf(renewed)).createStream(messages, tools, opts);
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function authOf(credential: Credential): TokenAuth {
|
|
71
|
+
return {
|
|
72
|
+
apiKey: credential.accessToken,
|
|
73
|
+
...(credential.baseUrl ? { baseUrl: credential.baseUrl } : {}),
|
|
74
|
+
...(credential.chatgptAccountId
|
|
75
|
+
? { headers: { "ChatGPT-Account-Id": credential.chatgptAccountId } }
|
|
76
|
+
: {}),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Wait for the token, or reject with the caller's abort, whichever is first.
|
|
82
|
+
*
|
|
83
|
+
* The refresh itself is never aborted. It is shared by every caller, and once
|
|
84
|
+
* its POST is out the server may already have rotated the refresh token, so
|
|
85
|
+
* abandoning it before the save loses the only copy of the new one. And
|
|
86
|
+
* without the race an unanswered refresh would outlive the watchdog, which
|
|
87
|
+
* aborts the signal but never races a read.
|
|
88
|
+
*/
|
|
89
|
+
function unlessAborted<T>(pending: Promise<T>, signal: AbortSignal | undefined): Promise<T> {
|
|
90
|
+
if (!signal) return pending;
|
|
91
|
+
return new Promise<T>((resolve, reject) => {
|
|
92
|
+
const onAbort = () => reject(signal.reason);
|
|
93
|
+
if (signal.aborted) onAbort();
|
|
94
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
95
|
+
pending.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
|
|
96
|
+
});
|
|
97
|
+
}
|
package/src/capability.ts
CHANGED
|
@@ -18,12 +18,12 @@
|
|
|
18
18
|
|
|
19
19
|
import type { JsonWithTools, Provider } from "./types.ts";
|
|
20
20
|
|
|
21
|
-
/**
|
|
22
|
-
* turn
|
|
21
|
+
/** A question the model cannot answer from memory, so a tool call is the only
|
|
22
|
+
* correct turn. Not making one is a real signal rather than a judgement call. */
|
|
23
23
|
const PROBE_TOOL = {
|
|
24
24
|
name: "get_current_time",
|
|
25
25
|
description:
|
|
26
|
-
"The only source of the current time. You cannot know the time without calling this
|
|
26
|
+
"The only source of the current time. You cannot know the time without calling this. Never answer a time question from memory or guess.",
|
|
27
27
|
inputSchema: {
|
|
28
28
|
type: "object" as const,
|
|
29
29
|
properties: { timezone: { type: "string", description: "IANA timezone, e.g. Asia/Tokyo" } },
|
|
@@ -46,7 +46,7 @@ const SHAPES: JsonWithTools[] = ["response_format", "prompt"];
|
|
|
46
46
|
export interface JsonWithToolsProbe {
|
|
47
47
|
/**
|
|
48
48
|
* The shape to configure, or `null` when neither called the tool on every
|
|
49
|
-
* sample
|
|
49
|
+
* sample. That model cannot be trusted with tools and a schema together,
|
|
50
50
|
* whichever way the schema rides, and the honest fix is a different model.
|
|
51
51
|
*/
|
|
52
52
|
use: JsonWithTools | null;
|
|
@@ -58,8 +58,8 @@ export interface JsonWithToolsProbe {
|
|
|
58
58
|
|
|
59
59
|
export interface ProbeOptions {
|
|
60
60
|
/**
|
|
61
|
-
* Samples per shape. Above one because the interesting failures are partial
|
|
62
|
-
* `gemini-3.8-flash` called its tool 3/10
|
|
61
|
+
* Samples per shape. Above one because the interesting failures are partial:
|
|
62
|
+
* `gemini-3.8-flash` called its tool 3/10. A single sample reports a
|
|
63
63
|
* coin flip as a capability. A shape passes only when EVERY sample calls.
|
|
64
64
|
*/
|
|
65
65
|
samples?: number;
|
|
@@ -95,12 +95,12 @@ async function callsTool(
|
|
|
95
95
|
|
|
96
96
|
/**
|
|
97
97
|
* Ask a model, on the wire, which {@link JsonWithTools} shape it actually
|
|
98
|
-
* serves
|
|
98
|
+
* serves. Then configure the provider with `probe.use`.
|
|
99
99
|
*
|
|
100
100
|
* Both shapes are always tried, because a result you can read beats a result
|
|
101
101
|
* you have to trust: `calls` is the table to log, and "0/3 and 3/3" is what
|
|
102
102
|
* makes a later regression obvious. It costs `samples × 2` short calls, made
|
|
103
|
-
* one after another, once, at whatever moment the app decides to ask
|
|
103
|
+
* one after another, once, at whatever moment the app decides to ask. Boot is
|
|
104
104
|
* the usual one.
|
|
105
105
|
*
|
|
106
106
|
* Probe each provider on its own, never a fallback chain: the shape is a fact
|
|
@@ -117,8 +117,8 @@ async function callsTool(
|
|
|
117
117
|
* report "this model cannot call tools", which is a much worse thing to
|
|
118
118
|
* believe than "the call failed".
|
|
119
119
|
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
120
|
+
* It catches a structural failure: a model that cannot emit the call, every
|
|
121
|
+
* time, on the easiest question there is. A model that is merely
|
|
122
122
|
* unreliable will pass: `gemini-3.8-flash` answers this probe 3/3 and a real
|
|
123
123
|
* sales turn 3/10. That is the right line to draw. A harder probe would start
|
|
124
124
|
* failing good models for being terse, and "sometimes forgets its tools" is a
|
|
@@ -226,8 +226,8 @@ function normalizeId(id: string): string {
|
|
|
226
226
|
* - `deepseek/deepseek-v4.1-flash` matches `deepseek-v4.1-flash`
|
|
227
227
|
* - `accounts/fireworks/models/glm-5p3-flash` matches `glm-5.3-flash`
|
|
228
228
|
*
|
|
229
|
-
* Never throws
|
|
230
|
-
* returns `undefined` so capability lookups never break caller execution.
|
|
229
|
+
* Never throws. If the catalog cannot be retrieved or the model is unknown,
|
|
230
|
+
* it returns `undefined` so capability lookups never break caller execution.
|
|
231
231
|
*/
|
|
232
232
|
export async function resolveModelCapabilities(
|
|
233
233
|
modelId: string,
|
package/src/context.ts
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
|
-
// Deciding when a conversation no longer fits,
|
|
1
|
+
// Deciding when a conversation no longer fits, where to cut it, and how to
|
|
2
|
+
// make a saved one valid to send again.
|
|
2
3
|
//
|
|
3
4
|
// Everything here is pure and testable. The model call that writes the summary
|
|
4
5
|
// and the row that stores it belong to the caller — what is decidable without
|
|
5
6
|
// I/O is decided here.
|
|
6
|
-
import type { ChatMessage } from "./types.ts";
|
|
7
|
+
import type { ChatMessage, ToolCall } from "./types.ts";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* What the answer needs after the prompt: an output budget, plus the tools'
|
|
10
11
|
* own schemas, plus the slack no provider documents.
|
|
11
12
|
*
|
|
12
|
-
* Absolute rather than a percentage on purpose
|
|
13
|
+
* Absolute rather than a percentage on purpose: a 1M window does not need a
|
|
13
14
|
* 100k cushion, and a 128k window needs more than 12k.
|
|
14
15
|
*/
|
|
15
16
|
export const CONTEXT_RESERVE_TOKENS = 32_000;
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
|
-
* Four characters per token
|
|
19
|
+
* Four characters per token is the rule of thumb every provider's own
|
|
19
20
|
* calculator agrees with to within a fifth, which is all the precision this
|
|
20
21
|
* needs. It decides WHEN to fold: folding one turn early costs a cheap model
|
|
21
22
|
* call, folding one turn late costs the whole turn.
|
|
@@ -64,7 +65,7 @@ export function conversationTokens(messages: readonly ChatMessage[]): number {
|
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
/**
|
|
67
|
-
* The provider's own input count says the
|
|
68
|
+
* The provider's own input count says the limit is close. Fold before the next
|
|
68
69
|
* step rather than after the 400.
|
|
69
70
|
*
|
|
70
71
|
* Takes the REPORTED count, not an estimate, because it is the only figure
|
|
@@ -99,7 +100,7 @@ export function historyBudgetTokens(contextWindow: number): number {
|
|
|
99
100
|
*
|
|
100
101
|
* 1. Never split a tool call from its result. Every provider rejects a tool
|
|
101
102
|
* result whose call is missing, so a cut landing between them produces a
|
|
102
|
-
* 400 on the very next turn
|
|
103
|
+
* 400 on the very next turn, the failure compaction was called to avoid.
|
|
103
104
|
* 2. Never cut into the system prompt. It is not history.
|
|
104
105
|
*
|
|
105
106
|
* Returns the index the kept tail starts at, or 0 when everything already fits.
|
|
@@ -153,6 +154,140 @@ export function applyCompaction(
|
|
|
153
154
|
];
|
|
154
155
|
}
|
|
155
156
|
|
|
157
|
+
type ToolResult = Extract<ChatMessage, { role: "tool" }>;
|
|
158
|
+
|
|
159
|
+
/** What a call with no result answers, by whether running it again is safe.
|
|
160
|
+
* The model reads this and decides whether to call again, so the two must
|
|
161
|
+
* differ on exactly that. */
|
|
162
|
+
const INTERRUPTED_READ_ONLY =
|
|
163
|
+
"This call was interrupted before it returned. It changed nothing, so it is safe to call again.";
|
|
164
|
+
const INTERRUPTED =
|
|
165
|
+
"This call was interrupted before it returned. It may have partly run, so check its effect before calling it again.";
|
|
166
|
+
|
|
167
|
+
/** `id`, or `id_2`, `id_3`… whichever is free first. Only ids already handed
|
|
168
|
+
* out are consulted, so an id never depends on what comes after it. */
|
|
169
|
+
function freshId(id: string, used: Set<string>): string {
|
|
170
|
+
let fresh = id;
|
|
171
|
+
for (let k = 2; used.has(fresh); k++) fresh = `${id}_${k}`;
|
|
172
|
+
used.add(fresh);
|
|
173
|
+
return fresh;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
interface Span {
|
|
177
|
+
calls: ToolCall[];
|
|
178
|
+
vendorIds: string[];
|
|
179
|
+
results: (ToolResult | undefined)[];
|
|
180
|
+
rest: ChatMessage[];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** The first call in the span asked under `id` that has no result yet, or -1.
|
|
184
|
+
* First unanswered rather than first: two calls in one reply can share an id,
|
|
185
|
+
* and their results then pair in order. */
|
|
186
|
+
function openSlot(span: Span, id: string): number {
|
|
187
|
+
return span.vendorIds.findIndex((asked, i) => asked === id && !span.results[i]);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** A result with no call to answer, kept as words the model can still read
|
|
191
|
+
* instead of a `tool` message every provider answers with a 400. */
|
|
192
|
+
function orphanNote(result: ToolResult): ChatMessage {
|
|
193
|
+
const text = `[Tool result for ${result.toolCallId}; the tool call is missing or already has a result]\n${result.content}`;
|
|
194
|
+
return {
|
|
195
|
+
role: "user",
|
|
196
|
+
content: result.images?.length ? [{ type: "text", text }, ...result.images] : text,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Make a saved, resumed or interrupted history valid to send: every call has
|
|
202
|
+
* exactly one result, right after it, under an id no other call uses.
|
|
203
|
+
*
|
|
204
|
+
* A span runs from an assistant message with tool calls to the next assistant
|
|
205
|
+
* message. Its results move to just after the calls, in call order, and the
|
|
206
|
+
* rest of the span (a steer the user typed while the tools ran) keeps its
|
|
207
|
+
* order after them. Then:
|
|
208
|
+
*
|
|
209
|
+
* - A call with no result in its span gets one under the same id, saying it was
|
|
210
|
+
* interrupted. `isReadOnly` picks the wording; without it every tool may
|
|
211
|
+
* have partly run. These calls come back as `interrupted`, under the ids
|
|
212
|
+
* the request carries.
|
|
213
|
+
* - A result whose call is not in its span becomes a user note, images kept.
|
|
214
|
+
* Paired against the span only, because chat and Messages both require the
|
|
215
|
+
* result in the turn right after its call: a looser pairing still 400s.
|
|
216
|
+
* - A repeated id becomes `{id}_{k}`, the smallest free k, call and result
|
|
217
|
+
* together. Kimi and Gemini restart their ids every reply
|
|
218
|
+
* (`functions.search:0`, `call_0`), and Anthropic and Responses reject the
|
|
219
|
+
* second use.
|
|
220
|
+
* - An empty id becomes `call_{message}_{call}`, never `normalizeToolId`'s
|
|
221
|
+
* random fallback.
|
|
222
|
+
* - An assistant message with no calls and only whitespace is dropped, as if
|
|
223
|
+
* it were never there, so a placeholder saved before the tools ran orphans
|
|
224
|
+
* nothing.
|
|
225
|
+
*
|
|
226
|
+
* No side effects, and the same input always gives the same bytes: a span's
|
|
227
|
+
* output and every id depend only on what came before, so repairing a longer
|
|
228
|
+
* history keeps the shorter one's bytes and the prompt cache keeps hitting.
|
|
229
|
+
* Repairing a repaired history changes nothing. Every adapter here runs this
|
|
230
|
+
* on what it sends; call it yourself to know what `interrupted` holds.
|
|
231
|
+
*/
|
|
232
|
+
export function repairHistory(
|
|
233
|
+
messages: readonly ChatMessage[],
|
|
234
|
+
opts: { isReadOnly?: (toolName: string) => boolean } = {},
|
|
235
|
+
): { messages: ChatMessage[]; interrupted: ToolCall[] } {
|
|
236
|
+
const out: ChatMessage[] = [];
|
|
237
|
+
const interrupted: ToolCall[] = [];
|
|
238
|
+
const used = new Set<string>();
|
|
239
|
+
// The open span: its calls as sent, the id the vendor gave each, the result
|
|
240
|
+
// each has found so far, and everything else in arrival order.
|
|
241
|
+
let span: Span | undefined;
|
|
242
|
+
|
|
243
|
+
const close = () => {
|
|
244
|
+
if (!span) return;
|
|
245
|
+
for (const [i, call] of span.calls.entries()) {
|
|
246
|
+
const result = span.results[i];
|
|
247
|
+
if (result) {
|
|
248
|
+
out.push(result);
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
interrupted.push(call);
|
|
252
|
+
const content = opts.isReadOnly?.(call.name) ? INTERRUPTED_READ_ONLY : INTERRUPTED;
|
|
253
|
+
out.push({ role: "tool", toolCallId: call.id, name: call.name, content });
|
|
254
|
+
}
|
|
255
|
+
out.push(...span.rest);
|
|
256
|
+
span = undefined;
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
for (const [index, message] of messages.entries()) {
|
|
260
|
+
if (message.role === "assistant") {
|
|
261
|
+
const calls = message.toolCalls ?? [];
|
|
262
|
+
if (calls.length === 0 && !message.content.trim()) continue;
|
|
263
|
+
close();
|
|
264
|
+
if (calls.length === 0) {
|
|
265
|
+
out.push(message);
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const sent = calls.map((call, c) => {
|
|
269
|
+
const id = freshId(call.id.trim() ? call.id : `call_${index}_${c}`, used);
|
|
270
|
+
return id === call.id ? call : { ...call, id };
|
|
271
|
+
});
|
|
272
|
+
const renamed = sent.some((call, c) => call !== calls[c]);
|
|
273
|
+
out.push(renamed ? { ...message, toolCalls: sent } : message);
|
|
274
|
+
span = { calls: sent, vendorIds: calls.map((call) => call.id), results: [], rest: [] };
|
|
275
|
+
} else if (message.role === "tool") {
|
|
276
|
+
const slot = span ? openSlot(span, message.toolCallId) : -1;
|
|
277
|
+
if (span && slot !== -1) {
|
|
278
|
+
const id = span.calls[slot]!.id;
|
|
279
|
+
span.results[slot] = id === message.toolCallId ? message : { ...message, toolCallId: id };
|
|
280
|
+
} else {
|
|
281
|
+
(span?.rest ?? out).push(orphanNote(message));
|
|
282
|
+
}
|
|
283
|
+
} else {
|
|
284
|
+
(span?.rest ?? out).push(message);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
close();
|
|
288
|
+
return { messages: out, interrupted };
|
|
289
|
+
}
|
|
290
|
+
|
|
156
291
|
/**
|
|
157
292
|
* The context window for a model, when nothing volunteered one.
|
|
158
293
|
*
|