@oh-my-pi/pi-ai 6.7.67 → 6.7.670
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/providers/anthropic.ts +215 -166
- package/src/utils/oauth/anthropic.ts +16 -5
- package/src/utils/oauth/pkce.ts +1 -1
package/package.json
CHANGED
|
@@ -29,21 +29,20 @@ import { sanitizeSurrogates } from "../utils/sanitize-unicode";
|
|
|
29
29
|
|
|
30
30
|
import { transformMessages } from "./transform-messages";
|
|
31
31
|
|
|
32
|
-
// Stealth mode: Mimic Claude Code headers
|
|
33
|
-
export const claudeCodeVersion = "
|
|
32
|
+
// Stealth mode: Mimic Claude Code headers and tool prefixing.
|
|
33
|
+
export const claudeCodeVersion = "1.0.83";
|
|
34
34
|
export const claudeToolPrefix = "proxy_";
|
|
35
35
|
export const claudeCodeSystemInstruction = "You are Claude Code, Anthropic's official CLI for Claude.";
|
|
36
36
|
export const claudeCodeHeaders = {
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"x-stainless-timeout": "60",
|
|
37
|
+
"X-Stainless-Helper-Method": "stream",
|
|
38
|
+
"X-Stainless-Retry-Count": "0",
|
|
39
|
+
"X-Stainless-Runtime-Version": "v24.3.0",
|
|
40
|
+
"X-Stainless-Package-Version": "0.55.1",
|
|
41
|
+
"X-Stainless-Runtime": "node",
|
|
42
|
+
"X-Stainless-Lang": "js",
|
|
43
|
+
"X-Stainless-Arch": "arm64",
|
|
44
|
+
"X-Stainless-Os": "MacOS",
|
|
45
|
+
"X-Stainless-Timeout": "60",
|
|
47
46
|
} as const;
|
|
48
47
|
|
|
49
48
|
export const applyClaudeToolPrefix = (name: string) => {
|
|
@@ -60,45 +59,18 @@ export const stripClaudeToolPrefix = (name: string) => {
|
|
|
60
59
|
return name.slice(claudeToolPrefix.length);
|
|
61
60
|
};
|
|
62
61
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
"
|
|
68
|
-
"Write",
|
|
69
|
-
"Edit",
|
|
70
|
-
"Bash",
|
|
71
|
-
"Grep",
|
|
72
|
-
"Glob",
|
|
73
|
-
"AskUserQuestion",
|
|
74
|
-
"EnterPlanMode",
|
|
75
|
-
"ExitPlanMode",
|
|
76
|
-
"KillShell",
|
|
77
|
-
"NotebookEdit",
|
|
78
|
-
"Skill",
|
|
79
|
-
"Task",
|
|
80
|
-
"TaskOutput",
|
|
81
|
-
"TodoWrite",
|
|
82
|
-
"WebFetch",
|
|
83
|
-
"WebSearch",
|
|
62
|
+
const claudeCodeBetaDefaults = [
|
|
63
|
+
"claude-code-20250219",
|
|
64
|
+
"oauth-2025-04-20",
|
|
65
|
+
"interleaved-thinking-2025-05-14",
|
|
66
|
+
"fine-grained-tool-streaming-2025-05-14",
|
|
84
67
|
];
|
|
85
68
|
|
|
86
|
-
|
|
69
|
+
// Prefix tool names for OAuth traffic.
|
|
70
|
+
const toClaudeCodeName = (name: string) => applyClaudeToolPrefix(name);
|
|
87
71
|
|
|
88
|
-
//
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
// Convert CC tool name back to original, checking provided tools for case-insensitive match
|
|
92
|
-
const fromClaudeCodeName = (name: string, tools?: Tool[]) => {
|
|
93
|
-
// First try to find by case-insensitive match in provided tools
|
|
94
|
-
if (tools && tools.length > 0) {
|
|
95
|
-
const lowerName = name.toLowerCase();
|
|
96
|
-
const matchedTool = tools.find((tool) => tool.name.toLowerCase() === lowerName);
|
|
97
|
-
if (matchedTool) return matchedTool.name;
|
|
98
|
-
}
|
|
99
|
-
// Fall back to stripping prefix if no match found
|
|
100
|
-
return stripClaudeToolPrefix(name);
|
|
101
|
-
};
|
|
72
|
+
// Strip Claude Code tool prefix on response.
|
|
73
|
+
const fromClaudeCodeName = (name: string) => stripClaudeToolPrefix(name);
|
|
102
74
|
|
|
103
75
|
/**
|
|
104
76
|
* Convert content blocks to Anthropic API format
|
|
@@ -189,7 +161,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
189
161
|
try {
|
|
190
162
|
const apiKey = options?.apiKey ?? getEnvApiKey(model.provider) ?? "";
|
|
191
163
|
const extraBetas = normalizeExtraBetas(options?.betas);
|
|
192
|
-
const { client, isOAuthToken } = createClient(model, apiKey,
|
|
164
|
+
const { client, isOAuthToken } = createClient(model, apiKey, extraBetas, true);
|
|
193
165
|
const params = buildParams(model, context, isOAuthToken, options);
|
|
194
166
|
const anthropicStream = client.messages.stream({ ...params, stream: true }, { signal: options?.signal });
|
|
195
167
|
stream.push({ type: "start", partial: output });
|
|
@@ -231,9 +203,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
231
203
|
const block: Block = {
|
|
232
204
|
type: "toolCall",
|
|
233
205
|
id: event.content_block.id,
|
|
234
|
-
name: isOAuthToken
|
|
235
|
-
? fromClaudeCodeName(event.content_block.name, context.tools)
|
|
236
|
-
: event.content_block.name,
|
|
206
|
+
name: isOAuthToken ? fromClaudeCodeName(event.content_block.name) : event.content_block.name,
|
|
237
207
|
arguments: event.content_block.input as Record<string, any>,
|
|
238
208
|
partialJson: "",
|
|
239
209
|
index: event.index,
|
|
@@ -358,6 +328,16 @@ function isOAuthToken(apiKey: string): boolean {
|
|
|
358
328
|
return apiKey.includes("sk-ant-oat");
|
|
359
329
|
}
|
|
360
330
|
|
|
331
|
+
function isAnthropicBaseUrl(baseUrl?: string): boolean {
|
|
332
|
+
if (!baseUrl) return true;
|
|
333
|
+
try {
|
|
334
|
+
const url = new URL(baseUrl);
|
|
335
|
+
return url.protocol === "https:" && url.hostname === "api.anthropic.com";
|
|
336
|
+
} catch {
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
361
341
|
export function normalizeExtraBetas(betas?: string[] | string): string[] {
|
|
362
342
|
if (!betas) return [];
|
|
363
343
|
const raw = Array.isArray(betas) ? betas : betas.split(",");
|
|
@@ -378,84 +358,105 @@ export function buildBetaHeader(baseBetas: string[], extraBetas: string[]): stri
|
|
|
378
358
|
return result.join(",");
|
|
379
359
|
}
|
|
380
360
|
|
|
361
|
+
export type AnthropicHeaderOptions = {
|
|
362
|
+
apiKey: string;
|
|
363
|
+
baseUrl?: string;
|
|
364
|
+
isOAuth?: boolean;
|
|
365
|
+
extraBetas?: string[];
|
|
366
|
+
stream?: boolean;
|
|
367
|
+
modelHeaders?: Record<string, string>;
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
export function buildAnthropicHeaders(options: AnthropicHeaderOptions): Record<string, string> {
|
|
371
|
+
const oauthToken = options.isOAuth ?? isOAuthToken(options.apiKey);
|
|
372
|
+
const extraBetas = options.extraBetas ?? [];
|
|
373
|
+
const stream = options.stream ?? false;
|
|
374
|
+
const betaHeader = buildBetaHeader(claudeCodeBetaDefaults, extraBetas);
|
|
375
|
+
const acceptHeader = stream ? "text/event-stream" : "application/json";
|
|
376
|
+
const enforcedHeaderKeys = new Set(
|
|
377
|
+
[
|
|
378
|
+
...Object.keys(claudeCodeHeaders),
|
|
379
|
+
"Accept",
|
|
380
|
+
"Accept-Encoding",
|
|
381
|
+
"Connection",
|
|
382
|
+
"Content-Type",
|
|
383
|
+
"Anthropic-Version",
|
|
384
|
+
"Anthropic-Dangerous-Direct-Browser-Access",
|
|
385
|
+
"Anthropic-Beta",
|
|
386
|
+
"User-Agent",
|
|
387
|
+
"X-App",
|
|
388
|
+
"Authorization",
|
|
389
|
+
"X-Api-Key",
|
|
390
|
+
].map((key) => key.toLowerCase()),
|
|
391
|
+
);
|
|
392
|
+
const modelHeaders = Object.fromEntries(
|
|
393
|
+
Object.entries(options.modelHeaders ?? {}).filter(([key]) => !enforcedHeaderKeys.has(key.toLowerCase())),
|
|
394
|
+
);
|
|
395
|
+
const headers: Record<string, string> = {
|
|
396
|
+
...modelHeaders,
|
|
397
|
+
...claudeCodeHeaders,
|
|
398
|
+
Accept: acceptHeader,
|
|
399
|
+
"Accept-Encoding": "gzip, deflate, br, zstd",
|
|
400
|
+
Connection: "keep-alive",
|
|
401
|
+
"Content-Type": "application/json",
|
|
402
|
+
"Anthropic-Version": "2023-06-01",
|
|
403
|
+
"Anthropic-Dangerous-Direct-Browser-Access": "true",
|
|
404
|
+
"Anthropic-Beta": betaHeader,
|
|
405
|
+
"User-Agent": `claude-cli/${claudeCodeVersion} (external, cli)`,
|
|
406
|
+
"X-App": "cli",
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
if (oauthToken || !isAnthropicBaseUrl(options.baseUrl)) {
|
|
410
|
+
headers.Authorization = `Bearer ${options.apiKey}`;
|
|
411
|
+
} else {
|
|
412
|
+
headers["X-Api-Key"] = options.apiKey;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return headers;
|
|
416
|
+
}
|
|
417
|
+
|
|
381
418
|
function createClient(
|
|
382
419
|
model: Model<"anthropic-messages">,
|
|
383
420
|
apiKey: string,
|
|
384
|
-
interleavedThinking: boolean,
|
|
385
421
|
extraBetas: string[],
|
|
422
|
+
stream: boolean,
|
|
386
423
|
): { client: Anthropic; isOAuthToken: boolean } {
|
|
387
424
|
const oauthToken = isOAuthToken(apiKey);
|
|
388
425
|
|
|
389
|
-
// Base betas required for Claude Code compatibility
|
|
390
|
-
const baseBetas = oauthToken
|
|
391
|
-
? ["claude-code-20250219", "oauth-2025-04-20", "fine-grained-tool-streaming-2025-05-14"]
|
|
392
|
-
: ["fine-grained-tool-streaming-2025-05-14"];
|
|
393
|
-
|
|
394
|
-
// Add interleaved thinking if requested
|
|
395
426
|
const mergedBetas: string[] = [];
|
|
396
|
-
if (interleavedThinking) {
|
|
397
|
-
mergedBetas.push("interleaved-thinking-2025-05-14");
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
// Include any betas from model headers
|
|
401
427
|
const modelBeta = model.headers?.["anthropic-beta"];
|
|
402
428
|
if (modelBeta) {
|
|
403
429
|
mergedBetas.push(...normalizeExtraBetas(modelBeta));
|
|
404
430
|
}
|
|
405
|
-
|
|
406
|
-
// Include any betas passed via options
|
|
407
431
|
if (extraBetas.length > 0) {
|
|
408
432
|
mergedBetas.push(...extraBetas);
|
|
409
433
|
}
|
|
410
434
|
|
|
411
|
-
const
|
|
412
|
-
|
|
413
|
-
if (oauthToken) {
|
|
414
|
-
// Stealth mode: Mimic Claude Code's headers exactly
|
|
415
|
-
const defaultHeaders = {
|
|
416
|
-
accept: "application/json",
|
|
417
|
-
"anthropic-dangerous-direct-browser-access": "true",
|
|
418
|
-
"anthropic-beta": betaHeader,
|
|
419
|
-
"user-agent": `claude-cli/${claudeCodeVersion} (external, cli)`,
|
|
420
|
-
"x-app": "cli",
|
|
421
|
-
...claudeCodeHeaders,
|
|
422
|
-
...(model.headers || {}),
|
|
423
|
-
};
|
|
424
|
-
// Don't duplicate anthropic-beta from model.headers
|
|
425
|
-
delete (defaultHeaders as Record<string, string>)["anthropic-beta"];
|
|
426
|
-
(defaultHeaders as Record<string, string>)["anthropic-beta"] = betaHeader;
|
|
427
|
-
|
|
428
|
-
const client = new Anthropic({
|
|
429
|
-
apiKey: null,
|
|
430
|
-
authToken: apiKey,
|
|
431
|
-
baseURL: model.baseUrl,
|
|
432
|
-
defaultHeaders,
|
|
433
|
-
dangerouslyAllowBrowser: true,
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
return { client, isOAuthToken: true };
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
const defaultHeaders = {
|
|
440
|
-
accept: "application/json",
|
|
441
|
-
"anthropic-dangerous-direct-browser-access": "true",
|
|
442
|
-
"anthropic-beta": betaHeader,
|
|
443
|
-
"user-agent": `claude-cli/${claudeCodeVersion} (external, cli)`,
|
|
444
|
-
"x-app": "cli",
|
|
445
|
-
...claudeCodeHeaders,
|
|
446
|
-
...(model.headers || {}),
|
|
447
|
-
};
|
|
448
|
-
// Ensure our beta header takes precedence
|
|
449
|
-
(defaultHeaders as Record<string, string>)["anthropic-beta"] = betaHeader;
|
|
450
|
-
|
|
451
|
-
const client = new Anthropic({
|
|
435
|
+
const defaultHeadersBase = buildAnthropicHeaders({
|
|
452
436
|
apiKey,
|
|
437
|
+
baseUrl: model.baseUrl,
|
|
438
|
+
isOAuth: oauthToken,
|
|
439
|
+
extraBetas: mergedBetas,
|
|
440
|
+
stream,
|
|
441
|
+
modelHeaders: model.headers,
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
const clientOptions: ConstructorParameters<typeof Anthropic>[0] = {
|
|
453
445
|
baseURL: model.baseUrl,
|
|
454
446
|
dangerouslyAllowBrowser: true,
|
|
455
|
-
defaultHeaders,
|
|
456
|
-
}
|
|
447
|
+
defaultHeaders: defaultHeadersBase,
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
if (oauthToken || !isAnthropicBaseUrl(model.baseUrl)) {
|
|
451
|
+
clientOptions.apiKey = null;
|
|
452
|
+
clientOptions.authToken = apiKey;
|
|
453
|
+
} else {
|
|
454
|
+
clientOptions.apiKey = apiKey;
|
|
455
|
+
}
|
|
457
456
|
|
|
458
|
-
|
|
457
|
+
const client = new Anthropic(clientOptions);
|
|
458
|
+
|
|
459
|
+
return { client, isOAuthToken: oauthToken };
|
|
459
460
|
}
|
|
460
461
|
|
|
461
462
|
export type AnthropicSystemBlock = {
|
|
@@ -464,6 +465,14 @@ export type AnthropicSystemBlock = {
|
|
|
464
465
|
cache_control?: { type: "ephemeral" };
|
|
465
466
|
};
|
|
466
467
|
|
|
468
|
+
type CacheControlBlock = {
|
|
469
|
+
cache_control?: { type: "ephemeral" };
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
type CacheControlMode = "none" | "toolBlocks" | "userText";
|
|
473
|
+
|
|
474
|
+
const cacheControlEphemeral = { type: "ephemeral" as const };
|
|
475
|
+
|
|
467
476
|
type SystemBlockOptions = {
|
|
468
477
|
includeClaudeCodeInstruction?: boolean;
|
|
469
478
|
includeCacheControl?: boolean;
|
|
@@ -537,9 +546,11 @@ function buildParams(
|
|
|
537
546
|
isOAuthToken: boolean,
|
|
538
547
|
options?: AnthropicOptions,
|
|
539
548
|
): MessageCreateParamsStreaming {
|
|
549
|
+
const hasTools = Boolean(context.tools?.length);
|
|
550
|
+
const cacheControlMode = resolveCacheControlMode(context.messages, hasTools && isOAuthToken);
|
|
540
551
|
const params: MessageCreateParamsStreaming = {
|
|
541
552
|
model: model.id,
|
|
542
|
-
messages: convertMessages(context.messages, model, isOAuthToken),
|
|
553
|
+
messages: convertMessages(context.messages, model, isOAuthToken, cacheControlMode),
|
|
543
554
|
max_tokens: options?.maxTokens || (model.maxTokens / 3) | 0,
|
|
544
555
|
stream: true,
|
|
545
556
|
};
|
|
@@ -547,7 +558,7 @@ function buildParams(
|
|
|
547
558
|
const includeClaudeCodeSystem = !model.id.startsWith("claude-3-5-haiku");
|
|
548
559
|
const systemBlocks = buildAnthropicSystemBlocks(context.systemPrompt, {
|
|
549
560
|
includeClaudeCodeInstruction: includeClaudeCodeSystem,
|
|
550
|
-
includeCacheControl:
|
|
561
|
+
includeCacheControl: cacheControlMode !== "none",
|
|
551
562
|
});
|
|
552
563
|
if (systemBlocks) {
|
|
553
564
|
params.system = systemBlocks;
|
|
@@ -594,12 +605,33 @@ function sanitizeToolCallId(id: string): string {
|
|
|
594
605
|
return id.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
595
606
|
}
|
|
596
607
|
|
|
608
|
+
function resolveCacheControlMode(messages: Message[], includeCacheControl: boolean): CacheControlMode {
|
|
609
|
+
if (!includeCacheControl) return "none";
|
|
610
|
+
|
|
611
|
+
for (const message of messages) {
|
|
612
|
+
if (message.role === "toolResult") return "toolBlocks";
|
|
613
|
+
if (message.role === "assistant") {
|
|
614
|
+
const hasToolCall = message.content.some((block) => block.type === "toolCall");
|
|
615
|
+
if (hasToolCall) return "toolBlocks";
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return "userText";
|
|
620
|
+
}
|
|
621
|
+
|
|
597
622
|
function convertMessages(
|
|
598
623
|
messages: Message[],
|
|
599
624
|
model: Model<"anthropic-messages">,
|
|
600
625
|
isOAuthToken: boolean,
|
|
626
|
+
cacheControlMode: CacheControlMode,
|
|
601
627
|
): MessageParam[] {
|
|
602
628
|
const params: MessageParam[] = [];
|
|
629
|
+
const applyToolCacheControl = cacheControlMode === "toolBlocks";
|
|
630
|
+
const applyUserTextCacheControl = cacheControlMode === "userText";
|
|
631
|
+
const withCacheControl = <T extends object>(block: T, enabled: boolean): T | (T & CacheControlBlock) => {
|
|
632
|
+
if (!enabled) return block;
|
|
633
|
+
return { ...block, cache_control: cacheControlEphemeral };
|
|
634
|
+
};
|
|
603
635
|
|
|
604
636
|
// Transform messages for cross-provider compatibility
|
|
605
637
|
const transformedMessages = transformMessages(messages, model);
|
|
@@ -610,28 +642,47 @@ function convertMessages(
|
|
|
610
642
|
if (msg.role === "user") {
|
|
611
643
|
if (typeof msg.content === "string") {
|
|
612
644
|
if (msg.content.trim().length > 0) {
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
645
|
+
const text = sanitizeSurrogates(msg.content);
|
|
646
|
+
if (applyUserTextCacheControl) {
|
|
647
|
+
const blocks: Array<ContentBlockParam & CacheControlBlock> = [
|
|
648
|
+
withCacheControl(
|
|
649
|
+
{
|
|
650
|
+
type: "text",
|
|
651
|
+
text,
|
|
652
|
+
},
|
|
653
|
+
true,
|
|
654
|
+
),
|
|
655
|
+
];
|
|
656
|
+
params.push({
|
|
657
|
+
role: "user",
|
|
658
|
+
content: blocks,
|
|
659
|
+
});
|
|
660
|
+
} else {
|
|
661
|
+
params.push({
|
|
662
|
+
role: "user",
|
|
663
|
+
content: text,
|
|
664
|
+
});
|
|
665
|
+
}
|
|
617
666
|
}
|
|
618
667
|
} else {
|
|
619
|
-
const blocks: ContentBlockParam
|
|
668
|
+
const blocks: Array<ContentBlockParam & CacheControlBlock> = msg.content.map((item) => {
|
|
620
669
|
if (item.type === "text") {
|
|
621
|
-
return
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
} else {
|
|
626
|
-
return {
|
|
627
|
-
type: "image",
|
|
628
|
-
source: {
|
|
629
|
-
type: "base64",
|
|
630
|
-
media_type: item.mimeType as "image/jpeg" | "image/png" | "image/gif" | "image/webp",
|
|
631
|
-
data: item.data,
|
|
670
|
+
return withCacheControl(
|
|
671
|
+
{
|
|
672
|
+
type: "text",
|
|
673
|
+
text: sanitizeSurrogates(item.text),
|
|
632
674
|
},
|
|
633
|
-
|
|
675
|
+
applyUserTextCacheControl,
|
|
676
|
+
);
|
|
634
677
|
}
|
|
678
|
+
return {
|
|
679
|
+
type: "image",
|
|
680
|
+
source: {
|
|
681
|
+
type: "base64",
|
|
682
|
+
media_type: item.mimeType as "image/jpeg" | "image/png" | "image/gif" | "image/webp",
|
|
683
|
+
data: item.data,
|
|
684
|
+
},
|
|
685
|
+
};
|
|
635
686
|
});
|
|
636
687
|
let filteredBlocks = !model?.input.includes("image") ? blocks.filter((b) => b.type !== "image") : blocks;
|
|
637
688
|
filteredBlocks = filteredBlocks.filter((b) => {
|
|
@@ -647,7 +698,7 @@ function convertMessages(
|
|
|
647
698
|
});
|
|
648
699
|
}
|
|
649
700
|
} else if (msg.role === "assistant") {
|
|
650
|
-
const blocks: ContentBlockParam
|
|
701
|
+
const blocks: Array<ContentBlockParam & CacheControlBlock> = [];
|
|
651
702
|
|
|
652
703
|
for (const block of msg.content) {
|
|
653
704
|
if (block.type === "text") {
|
|
@@ -674,12 +725,17 @@ function convertMessages(
|
|
|
674
725
|
});
|
|
675
726
|
}
|
|
676
727
|
} else if (block.type === "toolCall") {
|
|
677
|
-
blocks.push(
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
728
|
+
blocks.push(
|
|
729
|
+
withCacheControl(
|
|
730
|
+
{
|
|
731
|
+
type: "tool_use",
|
|
732
|
+
id: sanitizeToolCallId(block.id),
|
|
733
|
+
name: isOAuthToken ? toClaudeCodeName(block.name) : block.name,
|
|
734
|
+
input: block.arguments,
|
|
735
|
+
},
|
|
736
|
+
applyToolCacheControl,
|
|
737
|
+
),
|
|
738
|
+
);
|
|
683
739
|
}
|
|
684
740
|
}
|
|
685
741
|
if (blocks.length === 0) continue;
|
|
@@ -689,26 +745,36 @@ function convertMessages(
|
|
|
689
745
|
});
|
|
690
746
|
} else if (msg.role === "toolResult") {
|
|
691
747
|
// Collect all consecutive toolResult messages, needed for z.ai Anthropic endpoint
|
|
692
|
-
const toolResults: ContentBlockParam
|
|
748
|
+
const toolResults: Array<ContentBlockParam & CacheControlBlock> = [];
|
|
693
749
|
|
|
694
750
|
// Add the current tool result
|
|
695
|
-
toolResults.push(
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
751
|
+
toolResults.push(
|
|
752
|
+
withCacheControl(
|
|
753
|
+
{
|
|
754
|
+
type: "tool_result",
|
|
755
|
+
tool_use_id: sanitizeToolCallId(msg.toolCallId),
|
|
756
|
+
content: convertContentBlocks(msg.content),
|
|
757
|
+
is_error: msg.isError,
|
|
758
|
+
},
|
|
759
|
+
applyToolCacheControl,
|
|
760
|
+
),
|
|
761
|
+
);
|
|
701
762
|
|
|
702
763
|
// Look ahead for consecutive toolResult messages
|
|
703
764
|
let j = i + 1;
|
|
704
765
|
while (j < transformedMessages.length && transformedMessages[j].role === "toolResult") {
|
|
705
766
|
const nextMsg = transformedMessages[j] as ToolResultMessage; // We know it's a toolResult
|
|
706
|
-
toolResults.push(
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
767
|
+
toolResults.push(
|
|
768
|
+
withCacheControl(
|
|
769
|
+
{
|
|
770
|
+
type: "tool_result",
|
|
771
|
+
tool_use_id: sanitizeToolCallId(nextMsg.toolCallId),
|
|
772
|
+
content: convertContentBlocks(nextMsg.content),
|
|
773
|
+
is_error: nextMsg.isError,
|
|
774
|
+
},
|
|
775
|
+
applyToolCacheControl,
|
|
776
|
+
),
|
|
777
|
+
);
|
|
712
778
|
j++;
|
|
713
779
|
}
|
|
714
780
|
|
|
@@ -723,23 +789,6 @@ function convertMessages(
|
|
|
723
789
|
}
|
|
724
790
|
}
|
|
725
791
|
|
|
726
|
-
// Add cache_control to the last user message to cache conversation history
|
|
727
|
-
if (params.length > 0) {
|
|
728
|
-
const lastMessage = params[params.length - 1];
|
|
729
|
-
if (lastMessage.role === "user") {
|
|
730
|
-
// Add cache control to the last content block
|
|
731
|
-
if (Array.isArray(lastMessage.content)) {
|
|
732
|
-
const lastBlock = lastMessage.content[lastMessage.content.length - 1];
|
|
733
|
-
if (
|
|
734
|
-
lastBlock &&
|
|
735
|
-
(lastBlock.type === "text" || lastBlock.type === "image" || lastBlock.type === "tool_result")
|
|
736
|
-
) {
|
|
737
|
-
(lastBlock as any).cache_control = { type: "ephemeral" };
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
|
|
743
792
|
return params;
|
|
744
793
|
}
|
|
745
794
|
|
|
@@ -9,9 +9,17 @@ const decode = (s: string) => atob(s);
|
|
|
9
9
|
const CLIENT_ID = decode("OWQxYzI1MGEtZTYxYi00NGQ5LTg4ZWQtNTk0NGQxOTYyZjVl");
|
|
10
10
|
const AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
|
|
11
11
|
const TOKEN_URL = "https://console.anthropic.com/v1/oauth/token";
|
|
12
|
-
const REDIRECT_URI = "
|
|
12
|
+
const REDIRECT_URI = "http://localhost:54545/callback";
|
|
13
13
|
const SCOPES = "org:create_api_key user:profile user:inference";
|
|
14
14
|
|
|
15
|
+
function generateState(): string {
|
|
16
|
+
const bytes = new Uint8Array(16);
|
|
17
|
+
crypto.getRandomValues(bytes);
|
|
18
|
+
return Array.from(bytes)
|
|
19
|
+
.map((value) => value.toString(16).padStart(2, "0"))
|
|
20
|
+
.join("");
|
|
21
|
+
}
|
|
22
|
+
|
|
15
23
|
function parseAuthCode(input: string): { code: string; state?: string } {
|
|
16
24
|
const trimmed = input.trim();
|
|
17
25
|
if (!trimmed) return { code: "" };
|
|
@@ -47,6 +55,7 @@ export async function loginAnthropic(
|
|
|
47
55
|
onPromptCode: () => Promise<string>,
|
|
48
56
|
): Promise<OAuthCredentials> {
|
|
49
57
|
const { verifier, challenge } = await generatePKCE();
|
|
58
|
+
const state = generateState();
|
|
50
59
|
|
|
51
60
|
// Build authorization URL
|
|
52
61
|
const authParams = new URLSearchParams({
|
|
@@ -57,7 +66,7 @@ export async function loginAnthropic(
|
|
|
57
66
|
scope: SCOPES,
|
|
58
67
|
code_challenge: challenge,
|
|
59
68
|
code_challenge_method: "S256",
|
|
60
|
-
state
|
|
69
|
+
state,
|
|
61
70
|
});
|
|
62
71
|
|
|
63
72
|
const authUrl = `${AUTHORIZE_URL}?${authParams.toString()}`;
|
|
@@ -67,19 +76,21 @@ export async function loginAnthropic(
|
|
|
67
76
|
|
|
68
77
|
// Wait for user to paste authorization code (format: code#state)
|
|
69
78
|
const authCode = await onPromptCode();
|
|
70
|
-
const { code, state } = parseAuthCode(authCode);
|
|
79
|
+
const { code, state: parsedState } = parseAuthCode(authCode);
|
|
80
|
+
const requestState = parsedState ?? state;
|
|
71
81
|
|
|
72
82
|
// Exchange code for tokens
|
|
73
83
|
const tokenResponse = await fetch(TOKEN_URL, {
|
|
74
84
|
method: "POST",
|
|
75
85
|
headers: {
|
|
76
86
|
"Content-Type": "application/json",
|
|
87
|
+
Accept: "application/json",
|
|
77
88
|
},
|
|
78
89
|
body: JSON.stringify({
|
|
79
90
|
grant_type: "authorization_code",
|
|
80
91
|
client_id: CLIENT_ID,
|
|
81
92
|
code,
|
|
82
|
-
|
|
93
|
+
state: requestState,
|
|
83
94
|
redirect_uri: REDIRECT_URI,
|
|
84
95
|
code_verifier: verifier,
|
|
85
96
|
}),
|
|
@@ -113,7 +124,7 @@ export async function loginAnthropic(
|
|
|
113
124
|
export async function refreshAnthropicToken(refreshToken: string): Promise<OAuthCredentials> {
|
|
114
125
|
const response = await fetch(TOKEN_URL, {
|
|
115
126
|
method: "POST",
|
|
116
|
-
headers: { "Content-Type": "application/json" },
|
|
127
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
117
128
|
body: JSON.stringify({
|
|
118
129
|
grant_type: "refresh_token",
|
|
119
130
|
client_id: CLIENT_ID,
|
package/src/utils/oauth/pkce.ts
CHANGED
|
@@ -20,7 +20,7 @@ function base64urlEncode(bytes: Uint8Array): string {
|
|
|
20
20
|
*/
|
|
21
21
|
export async function generatePKCE(): Promise<{ verifier: string; challenge: string }> {
|
|
22
22
|
// Generate random verifier
|
|
23
|
-
const verifierBytes = new Uint8Array(
|
|
23
|
+
const verifierBytes = new Uint8Array(96);
|
|
24
24
|
crypto.getRandomValues(verifierBytes);
|
|
25
25
|
const verifier = base64urlEncode(verifierBytes);
|
|
26
26
|
|