pi-anthropic-oauth 0.3.0 → 0.3.1
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/package.json +1 -1
- package/src/auth.ts +138 -65
- package/src/convert.ts +23 -3
- package/src/prompt.ts +27 -6
- package/src/stream.ts +83 -59
package/package.json
CHANGED
package/src/auth.ts
CHANGED
|
@@ -23,10 +23,48 @@ const CALLBACK_HOST = "127.0.0.1";
|
|
|
23
23
|
const LOCAL_CALLBACK_TIMEOUT = 5 * 60 * 1000;
|
|
24
24
|
const MAX_TOKEN_RETRIES = 2;
|
|
25
25
|
const INITIAL_RETRY_DELAY_MS = 5000;
|
|
26
|
+
const MAX_RETRY_DELAY_MS = 30_000;
|
|
27
|
+
const MANUAL_SETTLE_GRACE_MS = 10_000;
|
|
28
|
+
const LOCAL_REDIRECT_URI = `http://localhost:${CALLBACK_PORT}/callback`;
|
|
26
29
|
|
|
27
30
|
export { USER_AGENT };
|
|
28
31
|
|
|
29
|
-
|
|
32
|
+
class TokenHttpError extends Error {
|
|
33
|
+
constructor(
|
|
34
|
+
message: string,
|
|
35
|
+
readonly status: number,
|
|
36
|
+
) {
|
|
37
|
+
super(message);
|
|
38
|
+
this.name = "TokenHttpError";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isTransientTokenError(error: unknown): boolean {
|
|
43
|
+
if (error instanceof TokenHttpError) {
|
|
44
|
+
return error.status === 429 ||
|
|
45
|
+
(error.status >= 500 && error.status < 600);
|
|
46
|
+
}
|
|
47
|
+
return error instanceof TypeError;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function parseRetryAfter(value: string | null): number | undefined {
|
|
51
|
+
if (!value) return undefined;
|
|
52
|
+
|
|
53
|
+
const seconds = Number(value);
|
|
54
|
+
if (Number.isFinite(seconds)) {
|
|
55
|
+
return Math.min(Math.max(seconds, 0) * 1000, MAX_RETRY_DELAY_MS);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const at = Date.parse(value);
|
|
59
|
+
if (Number.isNaN(at)) return undefined;
|
|
60
|
+
return Math.min(Math.max(at - Date.now(), 0), MAX_RETRY_DELAY_MS);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type ParsedAuthInput = {
|
|
64
|
+
code: string;
|
|
65
|
+
state: string;
|
|
66
|
+
redirectUri?: string;
|
|
67
|
+
};
|
|
30
68
|
type LocalAuthorization = {
|
|
31
69
|
redirectUri: string;
|
|
32
70
|
waitForCallback: () => Promise<string | null>;
|
|
@@ -45,55 +83,68 @@ export async function loginAnthropic(
|
|
|
45
83
|
|
|
46
84
|
let authInput: string | null = null;
|
|
47
85
|
let redirectUri = REDIRECT_URI;
|
|
86
|
+
const initiatedRedirectUris = new Set<string>();
|
|
48
87
|
|
|
88
|
+
let localAuthorization: LocalAuthorization | null = null;
|
|
49
89
|
try {
|
|
50
|
-
|
|
51
|
-
|
|
90
|
+
localAuthorization = await createLocalAuthorization(state);
|
|
91
|
+
} catch {
|
|
92
|
+
// Failure to bind the callback server falls back to the paste flow.
|
|
93
|
+
}
|
|
52
94
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
95
|
+
try {
|
|
96
|
+
if (localAuthorization) {
|
|
97
|
+
const local = localAuthorization;
|
|
98
|
+
redirectUri = local.redirectUri;
|
|
99
|
+
initiatedRedirectUris.add(redirectUri);
|
|
100
|
+
|
|
101
|
+
callbacks.onAuth({
|
|
102
|
+
url: makeAuthorizeUrl(challenge, state, redirectUri),
|
|
103
|
+
instructions:
|
|
104
|
+
"Complete login in your browser. If the browser is on another machine, paste the final redirect URL here.",
|
|
105
|
+
});
|
|
58
106
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (manualError) throw manualError;
|
|
77
|
-
|
|
78
|
-
if (callbackResult) {
|
|
79
|
-
authInput = callbackResult;
|
|
80
|
-
} else if (manualInput) {
|
|
81
|
-
authInput = manualInput;
|
|
82
|
-
}
|
|
107
|
+
if (callbacks.onManualCodeInput) {
|
|
108
|
+
let manualInput: string | undefined;
|
|
109
|
+
let manualError: Error | undefined;
|
|
110
|
+
const manualPromise = callbacks
|
|
111
|
+
.onManualCodeInput()
|
|
112
|
+
.then((input) => {
|
|
113
|
+
manualInput = input;
|
|
114
|
+
local.cancel();
|
|
115
|
+
})
|
|
116
|
+
.catch((err) => {
|
|
117
|
+
manualError =
|
|
118
|
+
err instanceof Error ? err : new Error(String(err));
|
|
119
|
+
local.cancel();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const callbackResult = await local.waitForCallback();
|
|
83
123
|
|
|
84
|
-
if (!authInput) {
|
|
85
|
-
await manualPromise;
|
|
86
124
|
if (manualError) throw manualError;
|
|
87
|
-
|
|
125
|
+
|
|
126
|
+
if (callbackResult) {
|
|
127
|
+
authInput = callbackResult;
|
|
128
|
+
} else if (manualInput) {
|
|
129
|
+
authInput = manualInput;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!authInput) {
|
|
133
|
+
await Promise.race([manualPromise, settleGrace()]);
|
|
134
|
+
if (manualError) throw manualError;
|
|
135
|
+
if (manualInput) authInput = manualInput;
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
authInput = await local.waitForCallback();
|
|
88
139
|
}
|
|
89
|
-
} else {
|
|
90
|
-
authInput = await localAuthorization.waitForCallback();
|
|
91
140
|
}
|
|
92
|
-
}
|
|
141
|
+
} finally {
|
|
142
|
+
localAuthorization?.cancel();
|
|
93
143
|
}
|
|
94
144
|
|
|
95
145
|
if (!authInput) {
|
|
96
146
|
redirectUri = REDIRECT_URI;
|
|
147
|
+
initiatedRedirectUris.add(redirectUri);
|
|
97
148
|
callbacks.onAuth({
|
|
98
149
|
url: makeAuthorizeUrl(challenge, state, redirectUri),
|
|
99
150
|
instructions:
|
|
@@ -104,10 +155,12 @@ export async function loginAnthropic(
|
|
|
104
155
|
});
|
|
105
156
|
}
|
|
106
157
|
|
|
107
|
-
const parsed = parseAuthInput(authInput);
|
|
158
|
+
const parsed = parseAuthInput(authInput, initiatedRedirectUris);
|
|
108
159
|
if (!parsed) throw new Error("Could not parse authorization callback input.");
|
|
109
160
|
if (parsed.state !== state) throw new Error("OAuth state mismatch.");
|
|
110
161
|
|
|
162
|
+
const exchangeRedirectUri = parsed.redirectUri ?? redirectUri;
|
|
163
|
+
|
|
111
164
|
const tokenResponse = await fetchWithRetry(
|
|
112
165
|
TOKEN_URL,
|
|
113
166
|
{
|
|
@@ -118,7 +171,7 @@ export async function loginAnthropic(
|
|
|
118
171
|
client_id: CLIENT_ID,
|
|
119
172
|
code: parsed.code,
|
|
120
173
|
state: parsed.state,
|
|
121
|
-
redirect_uri:
|
|
174
|
+
redirect_uri: exchangeRedirectUri,
|
|
122
175
|
code_verifier: verifier,
|
|
123
176
|
}),
|
|
124
177
|
signal: callbacks.signal,
|
|
@@ -157,11 +210,12 @@ export async function refreshAnthropicToken(
|
|
|
157
210
|
},
|
|
158
211
|
"Token refresh",
|
|
159
212
|
);
|
|
160
|
-
} catch {
|
|
161
|
-
if (credentials.expires > Date.now()) {
|
|
213
|
+
} catch (error) {
|
|
214
|
+
if (isTransientTokenError(error) && credentials.expires > Date.now()) {
|
|
162
215
|
return { ...credentials, expires: Date.now() + 30_000 };
|
|
163
216
|
}
|
|
164
|
-
|
|
217
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
218
|
+
throw new Error(`Token refresh failed: ${detail}`);
|
|
165
219
|
}
|
|
166
220
|
|
|
167
221
|
const data = (await response.json()) as {
|
|
@@ -210,28 +264,29 @@ async function fetchWithRetry(
|
|
|
210
264
|
|
|
211
265
|
const bodyText = await response.text();
|
|
212
266
|
|
|
267
|
+
const failure = new TokenHttpError(
|
|
268
|
+
`${label} failed: ${response.status} ${bodyText}`,
|
|
269
|
+
response.status,
|
|
270
|
+
);
|
|
271
|
+
|
|
213
272
|
const shouldRetry = response.headers.get("x-should-retry");
|
|
214
|
-
if (shouldRetry === "false")
|
|
215
|
-
throw new Error(`${label} failed: ${response.status} ${bodyText}`);
|
|
216
|
-
}
|
|
273
|
+
if (shouldRetry === "false") throw failure;
|
|
217
274
|
|
|
218
275
|
if (
|
|
219
276
|
attempt < MAX_TOKEN_RETRIES &&
|
|
220
|
-
(response.status === 429 ||
|
|
277
|
+
(response.status === 429 ||
|
|
278
|
+
(response.status >= 500 && response.status < 600))
|
|
221
279
|
) {
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
: INITIAL_RETRY_DELAY_MS * 2 ** attempt;
|
|
280
|
+
const delayMs =
|
|
281
|
+
parseRetryAfter(response.headers.get("retry-after")) ??
|
|
282
|
+
INITIAL_RETRY_DELAY_MS * 2 ** attempt;
|
|
226
283
|
|
|
227
284
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
228
|
-
lastError =
|
|
229
|
-
`${label} failed: ${response.status} ${bodyText}`,
|
|
230
|
-
);
|
|
285
|
+
lastError = failure;
|
|
231
286
|
continue;
|
|
232
287
|
}
|
|
233
288
|
|
|
234
|
-
throw
|
|
289
|
+
throw failure;
|
|
235
290
|
}
|
|
236
291
|
|
|
237
292
|
throw lastError ?? new Error(`${label} failed after retries`);
|
|
@@ -250,6 +305,7 @@ async function createLocalAuthorization(
|
|
|
250
305
|
|
|
251
306
|
return new Promise((resolve, reject) => {
|
|
252
307
|
let done = false;
|
|
308
|
+
let settled = false;
|
|
253
309
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
254
310
|
let complete!: (value: string | null) => void;
|
|
255
311
|
const wait = new Promise<string | null>((innerResolve) => {
|
|
@@ -268,10 +324,7 @@ async function createLocalAuthorization(
|
|
|
268
324
|
};
|
|
269
325
|
|
|
270
326
|
server.on("request", (req, res) => {
|
|
271
|
-
const url = new URL(
|
|
272
|
-
req.url ?? "/",
|
|
273
|
-
`http://${req.headers.host ?? "localhost"}`,
|
|
274
|
-
);
|
|
327
|
+
const url = new URL(req.url ?? "/", LOCAL_REDIRECT_URI);
|
|
275
328
|
|
|
276
329
|
if (url.pathname !== "/callback") {
|
|
277
330
|
res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
|
@@ -290,7 +343,6 @@ async function createLocalAuthorization(
|
|
|
290
343
|
if (gotState !== state) {
|
|
291
344
|
res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
|
|
292
345
|
res.end("Invalid state");
|
|
293
|
-
finish(null);
|
|
294
346
|
return;
|
|
295
347
|
}
|
|
296
348
|
|
|
@@ -299,15 +351,25 @@ async function createLocalAuthorization(
|
|
|
299
351
|
Connection: "close",
|
|
300
352
|
});
|
|
301
353
|
res.end(makeCallbackPage());
|
|
302
|
-
finish(url.toString());
|
|
354
|
+
finish(`${LOCAL_REDIRECT_URI}?${url.searchParams.toString()}`);
|
|
303
355
|
});
|
|
304
356
|
|
|
305
|
-
server.
|
|
357
|
+
server.on("error", (error) => {
|
|
358
|
+
if (!settled) {
|
|
359
|
+
settled = true;
|
|
360
|
+
finish(null);
|
|
361
|
+
reject(error);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
finish(null);
|
|
365
|
+
});
|
|
306
366
|
|
|
307
367
|
server.listen(CALLBACK_PORT, CALLBACK_HOST, () => {
|
|
368
|
+
settled = true;
|
|
308
369
|
timer = setTimeout(() => finish(null), LOCAL_CALLBACK_TIMEOUT);
|
|
370
|
+
timer.unref?.();
|
|
309
371
|
resolve({
|
|
310
|
-
redirectUri:
|
|
372
|
+
redirectUri: LOCAL_REDIRECT_URI,
|
|
311
373
|
waitForCallback: () => wait,
|
|
312
374
|
cancel: () => finish(null),
|
|
313
375
|
});
|
|
@@ -326,6 +388,10 @@ function makeCallbackPage(): string {
|
|
|
326
388
|
</html>`;
|
|
327
389
|
}
|
|
328
390
|
|
|
391
|
+
function settleGrace(): Promise<void> {
|
|
392
|
+
return new Promise((resolve) => setTimeout(resolve, MANUAL_SETTLE_GRACE_MS));
|
|
393
|
+
}
|
|
394
|
+
|
|
329
395
|
async function generatePKCE(): Promise<{
|
|
330
396
|
verifier: string;
|
|
331
397
|
challenge: string;
|
|
@@ -351,14 +417,21 @@ function toBase64Url(bytes: Uint8Array): string {
|
|
|
351
417
|
.replace(/=+$/g, "");
|
|
352
418
|
}
|
|
353
419
|
|
|
354
|
-
function parseAuthInput(
|
|
420
|
+
function parseAuthInput(
|
|
421
|
+
input: string,
|
|
422
|
+
initiatedRedirectUris: ReadonlySet<string>,
|
|
423
|
+
): ParsedAuthInput | null {
|
|
355
424
|
const text = input.trim();
|
|
356
425
|
|
|
357
426
|
try {
|
|
358
427
|
const url = new URL(text);
|
|
359
428
|
const code = url.searchParams.get("code");
|
|
360
429
|
const state = url.searchParams.get("state");
|
|
361
|
-
if (code && state)
|
|
430
|
+
if (code && state) {
|
|
431
|
+
const redirectUri = `${url.origin}${url.pathname}`;
|
|
432
|
+
if (!initiatedRedirectUris.has(redirectUri)) return null;
|
|
433
|
+
return { code, state, redirectUri };
|
|
434
|
+
}
|
|
362
435
|
} catch {}
|
|
363
436
|
|
|
364
437
|
const split = text.split("#");
|
package/src/convert.ts
CHANGED
|
@@ -245,10 +245,30 @@ export function convertPiMessagesToAnthropic(
|
|
|
245
245
|
}
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
+
// The loop flushes pending tool uses before the next message. Flush once more
|
|
249
|
+
// so a history ending on an unresolved tool call is still valid.
|
|
250
|
+
flushPendingToolResults();
|
|
251
|
+
|
|
252
|
+
// Anthropic prompt caching requires an explicit breakpoint. Plain-text user
|
|
253
|
+
// turns must be promoted to a block so there is somewhere to attach it.
|
|
248
254
|
const last = params.at(-1);
|
|
249
|
-
if (last?.role === "user"
|
|
250
|
-
|
|
251
|
-
|
|
255
|
+
if (last?.role === "user") {
|
|
256
|
+
if (typeof last.content === "string") {
|
|
257
|
+
if (last.content) {
|
|
258
|
+
last.content = [
|
|
259
|
+
{
|
|
260
|
+
type: "text",
|
|
261
|
+
text: last.content,
|
|
262
|
+
cache_control: { type: "ephemeral" },
|
|
263
|
+
},
|
|
264
|
+
];
|
|
265
|
+
}
|
|
266
|
+
} else if (last.content.length > 0) {
|
|
267
|
+
const lastBlock = last.content[last.content.length - 1] as {
|
|
268
|
+
cache_control?: { type: string };
|
|
269
|
+
};
|
|
270
|
+
lastBlock.cache_control = { type: "ephemeral" };
|
|
271
|
+
}
|
|
252
272
|
}
|
|
253
273
|
|
|
254
274
|
return params;
|
package/src/prompt.ts
CHANGED
|
@@ -70,7 +70,9 @@ export function buildAnthropicSystemPrompt(
|
|
|
70
70
|
blocks.push({ type: "text", text: CLAUDE_CODE_IDENTITY });
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
const sanitized = systemPrompt
|
|
73
|
+
const sanitized = systemPrompt
|
|
74
|
+
? sanitizeSystemText(sanitizeSurrogates(systemPrompt))
|
|
75
|
+
: "";
|
|
74
76
|
if (sanitized) {
|
|
75
77
|
blocks.push({
|
|
76
78
|
type: "text",
|
|
@@ -93,10 +95,11 @@ export function sanitizeSystemText(
|
|
|
93
95
|
return !PI_REMOVAL_ANCHORS.some((anchor) => paragraph.includes(anchor));
|
|
94
96
|
});
|
|
95
97
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
const filteredText = filtered.join("\n\n");
|
|
99
|
+
const rewritePattern = resolvePiRewritePattern(env);
|
|
100
|
+
rejectZeroLengthMatches(rewritePattern, filteredText);
|
|
101
|
+
|
|
102
|
+
return filteredText.replace(rewritePattern, "Claude Code").trim();
|
|
100
103
|
}
|
|
101
104
|
|
|
102
105
|
function resolvePiRewritePattern(env: NodeJS.ProcessEnv): RegExp {
|
|
@@ -122,12 +125,30 @@ function compileCustomPiRewritePattern(value: string | undefined): RegExp {
|
|
|
122
125
|
|
|
123
126
|
const parsed = parseRegexLiteral(pattern) ?? { source: pattern, flags: "" };
|
|
124
127
|
const flags = parsed.flags.includes("g") ? parsed.flags : `${parsed.flags}g`;
|
|
128
|
+
let compiled: RegExp;
|
|
125
129
|
try {
|
|
126
|
-
|
|
130
|
+
compiled = new RegExp(parsed.source, flags);
|
|
127
131
|
} catch (error) {
|
|
128
132
|
const message = error instanceof Error ? error.message : String(error);
|
|
129
133
|
throw new Error(`Invalid ${PI_REWRITE_PATTERN_ENV}: ${message}`);
|
|
130
134
|
}
|
|
135
|
+
|
|
136
|
+
rejectZeroLengthMatches(compiled, "");
|
|
137
|
+
return compiled;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function rejectZeroLengthMatches(pattern: RegExp, text: string): void {
|
|
141
|
+
pattern.lastIndex = 0;
|
|
142
|
+
let match: RegExpExecArray | null;
|
|
143
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
144
|
+
if (match[0].length === 0) {
|
|
145
|
+
pattern.lastIndex = 0;
|
|
146
|
+
throw new Error(
|
|
147
|
+
`Invalid ${PI_REWRITE_PATTERN_ENV}: the pattern can match without consuming text. Use (?!) to disable rewriting.`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
pattern.lastIndex = 0;
|
|
131
152
|
}
|
|
132
153
|
|
|
133
154
|
function parseRegexLiteral(value: string): { source: string; flags: string } | undefined {
|
package/src/stream.ts
CHANGED
|
@@ -36,6 +36,26 @@ const REQUIRED_BETAS = [
|
|
|
36
36
|
"interleaved-thinking-2025-05-14",
|
|
37
37
|
] as const;
|
|
38
38
|
|
|
39
|
+
const LEGACY_THINKING_MODEL =
|
|
40
|
+
/^claude-(?:opus|sonnet|haiku)-4-[0-5](?:-|$)|^claude-[0-3][-.]/;
|
|
41
|
+
const MIN_THINKING_BUDGET = 1024;
|
|
42
|
+
const EFFORT_BY_REASONING: Record<string, string> = {
|
|
43
|
+
minimal: "low",
|
|
44
|
+
low: "low",
|
|
45
|
+
medium: "medium",
|
|
46
|
+
high: "high",
|
|
47
|
+
xhigh: "xhigh",
|
|
48
|
+
max: "max",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export function usesAdaptiveThinking(model: Model<Api>): boolean {
|
|
52
|
+
const forced = (
|
|
53
|
+
model.compat as { forceAdaptiveThinking?: boolean } | undefined
|
|
54
|
+
)?.forceAdaptiveThinking;
|
|
55
|
+
if (typeof forced === "boolean") return forced;
|
|
56
|
+
return !LEGACY_THINKING_MODEL.test(model.id.toLowerCase().replace(/\./g, "-"));
|
|
57
|
+
}
|
|
58
|
+
|
|
39
59
|
function mapStopReason(reason: string | null | undefined): StopReason {
|
|
40
60
|
switch (reason) {
|
|
41
61
|
case "end_turn":
|
|
@@ -51,6 +71,43 @@ function mapStopReason(reason: string | null | undefined): StopReason {
|
|
|
51
71
|
}
|
|
52
72
|
}
|
|
53
73
|
|
|
74
|
+
type MessageDeltaUsage = {
|
|
75
|
+
input_tokens?: number | null;
|
|
76
|
+
output_tokens?: number | null;
|
|
77
|
+
cache_read_input_tokens?: number | null;
|
|
78
|
+
cache_creation_input_tokens?: number | null;
|
|
79
|
+
output_tokens_details?: { thinking_tokens?: number | null };
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export function applyMessageDelta(
|
|
83
|
+
output: AssistantMessage,
|
|
84
|
+
model: Model<Api>,
|
|
85
|
+
stopReason: string | null | undefined,
|
|
86
|
+
usage: MessageDeltaUsage | undefined,
|
|
87
|
+
): void {
|
|
88
|
+
if (stopReason != null) output.stopReason = mapStopReason(stopReason);
|
|
89
|
+
|
|
90
|
+
if (usage) {
|
|
91
|
+
if (usage.input_tokens != null) output.usage.input = usage.input_tokens;
|
|
92
|
+
if (usage.output_tokens != null) output.usage.output = usage.output_tokens;
|
|
93
|
+
if (usage.cache_read_input_tokens != null) {
|
|
94
|
+
output.usage.cacheRead = usage.cache_read_input_tokens;
|
|
95
|
+
}
|
|
96
|
+
if (usage.cache_creation_input_tokens != null) {
|
|
97
|
+
output.usage.cacheWrite = usage.cache_creation_input_tokens;
|
|
98
|
+
}
|
|
99
|
+
const thinkingTokens = usage.output_tokens_details?.thinking_tokens;
|
|
100
|
+
if (thinkingTokens != null) output.usage.reasoning = thinkingTokens;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
output.usage.totalTokens =
|
|
104
|
+
output.usage.input +
|
|
105
|
+
output.usage.output +
|
|
106
|
+
output.usage.cacheRead +
|
|
107
|
+
output.usage.cacheWrite;
|
|
108
|
+
calculateCost(model, output.usage);
|
|
109
|
+
}
|
|
110
|
+
|
|
54
111
|
function headersToRecord(headers: Headers): Record<string, string> {
|
|
55
112
|
const record: Record<string, string> = {};
|
|
56
113
|
headers.forEach((value, key) => {
|
|
@@ -180,39 +237,22 @@ export function streamAnthropicOAuth(
|
|
|
180
237
|
];
|
|
181
238
|
const requestedBudget =
|
|
182
239
|
customBudget ?? defaultBudgets[options.reasoning] ?? 10240;
|
|
183
|
-
|
|
184
|
-
const forceAdaptive = (
|
|
185
|
-
model.compat as { forceAdaptiveThinking?: boolean } | undefined
|
|
186
|
-
)?.forceAdaptiveThinking;
|
|
187
|
-
const id = model.id.toLowerCase().replace(/\./g, "-");
|
|
188
|
-
const adaptive =
|
|
189
|
-
forceAdaptive === true ||
|
|
190
|
-
(forceAdaptive !== false &&
|
|
191
|
-
(/claude-(?:opus|sonnet|haiku|fable|mythos)-5(?:-|$)/.test(id) ||
|
|
192
|
-
/claude-(?:opus|sonnet|haiku|fable|mythos)-4-(?:[6-9]|\d{2,})(?:-|$)/.test(
|
|
193
|
-
id,
|
|
194
|
-
)));
|
|
195
|
-
|
|
196
|
-
if (adaptive) {
|
|
240
|
+
if (usesAdaptiveThinking(model)) {
|
|
197
241
|
const mapped = model.thinkingLevelMap?.[options.reasoning];
|
|
198
242
|
const effort =
|
|
199
243
|
typeof mapped === "string"
|
|
200
244
|
? mapped
|
|
201
|
-
: options.reasoning
|
|
202
|
-
? "low"
|
|
203
|
-
: options.reasoning === "medium"
|
|
204
|
-
? "medium"
|
|
205
|
-
: options.reasoning === "high"
|
|
206
|
-
? "high"
|
|
207
|
-
: "high";
|
|
208
|
-
params.thinking = { type: "adaptive", display } as never;
|
|
209
|
-
Object.assign(params, { output_config: { effort } });
|
|
210
|
-
} else {
|
|
245
|
+
: (EFFORT_BY_REASONING[options.reasoning] ?? "high");
|
|
211
246
|
params.thinking = {
|
|
212
|
-
type: "
|
|
213
|
-
|
|
214
|
-
display,
|
|
247
|
+
type: "adaptive",
|
|
248
|
+
display: "summarized",
|
|
215
249
|
} as never;
|
|
250
|
+
Object.assign(params, { output_config: { effort } });
|
|
251
|
+
} else {
|
|
252
|
+
const budget = Math.min(requestedBudget, maxTokens - 1);
|
|
253
|
+
if (budget >= MIN_THINKING_BUDGET) {
|
|
254
|
+
params.thinking = { type: "enabled", budget_tokens: budget };
|
|
255
|
+
}
|
|
216
256
|
}
|
|
217
257
|
}
|
|
218
258
|
|
|
@@ -413,42 +453,26 @@ export function streamAnthropicOAuth(
|
|
|
413
453
|
}
|
|
414
454
|
|
|
415
455
|
if (event.type === "message_delta") {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
output.usage.output;
|
|
423
|
-
output.usage.cacheRead =
|
|
424
|
-
(event.usage as { cache_read_input_tokens?: number })
|
|
425
|
-
.cache_read_input_tokens || 0;
|
|
426
|
-
output.usage.cacheWrite =
|
|
427
|
-
(event.usage as { cache_creation_input_tokens?: number })
|
|
428
|
-
.cache_creation_input_tokens || 0;
|
|
429
|
-
const thinkingTokens = (
|
|
430
|
-
event.usage as {
|
|
431
|
-
output_tokens_details?: { thinking_tokens?: number };
|
|
432
|
-
}
|
|
433
|
-
).output_tokens_details?.thinking_tokens;
|
|
434
|
-
if (thinkingTokens != null) {
|
|
435
|
-
output.usage.reasoning = thinkingTokens;
|
|
436
|
-
}
|
|
437
|
-
output.usage.totalTokens =
|
|
438
|
-
output.usage.input +
|
|
439
|
-
output.usage.output +
|
|
440
|
-
output.usage.cacheRead +
|
|
441
|
-
output.usage.cacheWrite;
|
|
442
|
-
calculateCost(model, output.usage);
|
|
456
|
+
applyMessageDelta(
|
|
457
|
+
output,
|
|
458
|
+
model,
|
|
459
|
+
event.delta.stop_reason,
|
|
460
|
+
event.usage as MessageDeltaUsage | undefined,
|
|
461
|
+
);
|
|
443
462
|
}
|
|
444
463
|
}
|
|
445
464
|
|
|
446
465
|
if (options?.signal?.aborted) throw new Error("Request aborted");
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
466
|
+
if (output.stopReason === "error") {
|
|
467
|
+
output.errorMessage ??= "Unrecognized stop reason from the API.";
|
|
468
|
+
stream.push({ type: "error", reason: "error", error: output });
|
|
469
|
+
} else {
|
|
470
|
+
stream.push({
|
|
471
|
+
type: "done",
|
|
472
|
+
reason: output.stopReason as "stop" | "length" | "toolUse",
|
|
473
|
+
message: output,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
452
476
|
stream.end();
|
|
453
477
|
} catch (error) {
|
|
454
478
|
for (const block of output.content as Array<{
|