@oh-my-pi/pi-ai 6.7.67 → 6.8.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 +31 -31
- package/package.json +2 -1
- package/src/cli.ts +114 -52
- package/src/providers/anthropic.ts +215 -166
- package/src/providers/google-gemini-cli.ts +4 -20
- package/src/providers/openai-codex/response-handler.ts +4 -43
- package/src/providers/openai-codex-responses.ts +2 -2
- package/src/storage.ts +185 -0
- package/src/utils/event-stream.ts +3 -3
- package/src/utils/oauth/anthropic.ts +70 -88
- package/src/utils/oauth/callback-server.ts +245 -0
- package/src/utils/oauth/cursor.ts +1 -5
- package/src/utils/oauth/github-copilot.ts +1 -23
- package/src/utils/oauth/google-antigravity.ts +73 -263
- package/src/utils/oauth/google-gemini-cli.ts +73 -281
- package/src/utils/oauth/oauth.html +199 -0
- package/src/utils/oauth/openai-codex.ts +131 -318
- package/src/utils/oauth/pkce.ts +1 -1
- package/src/utils/oauth/types.ts +8 -0
|
@@ -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
|
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
8
8
|
import type { Content, ThinkingConfig } from "@google/genai";
|
|
9
|
+
import { abortableSleep } from "@oh-my-pi/pi-utils";
|
|
9
10
|
import { calculateCost } from "../models";
|
|
10
11
|
import type {
|
|
11
12
|
Api,
|
|
@@ -301,23 +302,6 @@ function extractErrorMessage(errorText: string): string {
|
|
|
301
302
|
return errorText;
|
|
302
303
|
}
|
|
303
304
|
|
|
304
|
-
/**
|
|
305
|
-
* Sleep for a given number of milliseconds, respecting abort signal.
|
|
306
|
-
*/
|
|
307
|
-
function sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
308
|
-
return new Promise((resolve, reject) => {
|
|
309
|
-
if (signal?.aborted) {
|
|
310
|
-
reject(new Error("Request was aborted"));
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
const timeout = setTimeout(resolve, ms);
|
|
314
|
-
signal?.addEventListener("abort", () => {
|
|
315
|
-
clearTimeout(timeout);
|
|
316
|
-
reject(new Error("Request was aborted"));
|
|
317
|
-
});
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
|
|
321
305
|
interface CloudCodeAssistRequest {
|
|
322
306
|
project: string;
|
|
323
307
|
model: string;
|
|
@@ -468,7 +452,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
468
452
|
// Use server-provided delay or exponential backoff
|
|
469
453
|
const serverDelay = extractRetryDelay(errorText, response);
|
|
470
454
|
const delayMs = serverDelay ?? BASE_DELAY_MS * 2 ** attempt;
|
|
471
|
-
await
|
|
455
|
+
await abortableSleep(delayMs, options?.signal);
|
|
472
456
|
continue;
|
|
473
457
|
}
|
|
474
458
|
|
|
@@ -489,7 +473,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
489
473
|
// Network errors are retryable
|
|
490
474
|
if (attempt < MAX_RETRIES) {
|
|
491
475
|
const delayMs = BASE_DELAY_MS * 2 ** attempt;
|
|
492
|
-
await
|
|
476
|
+
await abortableSleep(delayMs, options?.signal);
|
|
493
477
|
continue;
|
|
494
478
|
}
|
|
495
479
|
throw lastError;
|
|
@@ -769,7 +753,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
|
|
|
769
753
|
|
|
770
754
|
if (emptyAttempt > 0) {
|
|
771
755
|
const backoffMs = EMPTY_STREAM_BASE_DELAY_MS * 2 ** (emptyAttempt - 1);
|
|
772
|
-
await
|
|
756
|
+
await abortableSleep(backoffMs, options?.signal);
|
|
773
757
|
|
|
774
758
|
if (!requestUrl) {
|
|
775
759
|
throw new Error("Missing request URL");
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readSseData } from "@oh-my-pi/pi-utils";
|
|
2
|
+
|
|
1
3
|
export type CodexRateLimit = {
|
|
2
4
|
used_percent?: number;
|
|
3
5
|
window_minutes?: number;
|
|
@@ -74,49 +76,8 @@ export async function* parseCodexSseStream(response: Response): AsyncGenerator<R
|
|
|
74
76
|
return;
|
|
75
77
|
}
|
|
76
78
|
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
let buffer = "";
|
|
80
|
-
|
|
81
|
-
while (true) {
|
|
82
|
-
const { done, value } = await reader.read();
|
|
83
|
-
if (done) break;
|
|
84
|
-
buffer += decoder.decode(value, { stream: true });
|
|
85
|
-
|
|
86
|
-
let index = buffer.indexOf("\n\n");
|
|
87
|
-
while (index !== -1) {
|
|
88
|
-
const chunk = buffer.slice(0, index);
|
|
89
|
-
buffer = buffer.slice(index + 2);
|
|
90
|
-
const event = parseSseChunk(chunk);
|
|
91
|
-
if (event) yield event;
|
|
92
|
-
index = buffer.indexOf("\n\n");
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (buffer.trim()) {
|
|
97
|
-
const event = parseSseChunk(buffer);
|
|
98
|
-
if (event) yield event;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function parseSseChunk(chunk: string): Record<string, unknown> | null {
|
|
103
|
-
const lines = chunk.split("\n");
|
|
104
|
-
const dataLines: string[] = [];
|
|
105
|
-
|
|
106
|
-
for (const line of lines) {
|
|
107
|
-
if (line.startsWith("data:")) {
|
|
108
|
-
dataLines.push(line.slice(5).trim());
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (dataLines.length === 0) return null;
|
|
113
|
-
const data = dataLines.join("\n").trim();
|
|
114
|
-
if (!data || data === "[DONE]") return null;
|
|
115
|
-
|
|
116
|
-
try {
|
|
117
|
-
return JSON.parse(data) as Record<string, unknown>;
|
|
118
|
-
} catch {
|
|
119
|
-
return null;
|
|
79
|
+
for await (const data of readSseData<Record<string, unknown>>(response.body)) {
|
|
80
|
+
yield data;
|
|
120
81
|
}
|
|
121
82
|
}
|
|
122
83
|
|
|
@@ -440,13 +440,13 @@ async function fetchWithRetry(url: string, init: RequestInit, signal?: AbortSign
|
|
|
440
440
|
}
|
|
441
441
|
if (signal?.aborted) return response;
|
|
442
442
|
const delay = getRetryDelayMs(response, attempt);
|
|
443
|
-
await
|
|
443
|
+
await Bun.sleep(delay);
|
|
444
444
|
} catch (error) {
|
|
445
445
|
if (attempt >= CODEX_MAX_RETRIES || signal?.aborted) {
|
|
446
446
|
throw error;
|
|
447
447
|
}
|
|
448
448
|
const delay = CODEX_RETRY_DELAY_MS * (attempt + 1);
|
|
449
|
-
await
|
|
449
|
+
await Bun.sleep(delay);
|
|
450
450
|
}
|
|
451
451
|
attempt += 1;
|
|
452
452
|
}
|