@lll9p/pi-anyrouter 0.3.2
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/LICENSE +22 -0
- package/README.md +251 -0
- package/index.ts +1221 -0
- package/package.json +53 -0
package/index.ts
ADDED
|
@@ -0,0 +1,1221 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
3
|
+
import { ProxyAgent, fetch as undiciFetch } from "undici";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import {
|
|
8
|
+
calculateCost,
|
|
9
|
+
createAssistantMessageEventStream,
|
|
10
|
+
type Api,
|
|
11
|
+
type AssistantMessage,
|
|
12
|
+
type AssistantMessageEventStream,
|
|
13
|
+
type Context,
|
|
14
|
+
type Message,
|
|
15
|
+
type Model,
|
|
16
|
+
type SimpleStreamOptions,
|
|
17
|
+
type StopReason,
|
|
18
|
+
type TextContent,
|
|
19
|
+
type ThinkingContent,
|
|
20
|
+
type Tool,
|
|
21
|
+
type ToolResultMessage,
|
|
22
|
+
type ImageContent,
|
|
23
|
+
} from "@earendil-works/pi-ai";
|
|
24
|
+
|
|
25
|
+
type Json = Record<string, any>;
|
|
26
|
+
type StreamMode = "off" | "auto" | "force";
|
|
27
|
+
type FetchInit = Parameters<typeof fetch>[1];
|
|
28
|
+
|
|
29
|
+
type ProviderModelConfig = {
|
|
30
|
+
id: string;
|
|
31
|
+
name?: string;
|
|
32
|
+
api?: string;
|
|
33
|
+
reasoning?: boolean;
|
|
34
|
+
input?: ("text" | "image")[];
|
|
35
|
+
cost?: { input?: number; output?: number; cacheRead?: number; cacheWrite?: number };
|
|
36
|
+
contextWindow?: number;
|
|
37
|
+
maxTokens?: number;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type ProviderConfigFile = {
|
|
41
|
+
baseUrl?: string;
|
|
42
|
+
apiKey?: string;
|
|
43
|
+
models?: ProviderModelConfig[];
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const DEFAULT_CONFIG_PATH = join(homedir(), ".pi", "agent", "anyrouter.json");
|
|
47
|
+
const CONFIG_PATH = process.env.PI_ANYROUTER_CC_CONFIG || DEFAULT_CONFIG_PATH;
|
|
48
|
+
const PROVIDER_NAME = "anyrouter";
|
|
49
|
+
// Keep this API id unique so pi uses this extension's streamSimple handler
|
|
50
|
+
// without touching the built-in anthropic-messages implementation.
|
|
51
|
+
const API_ID = "anyrouter-messages" as Api;
|
|
52
|
+
const DEBUG_ENABLED = process.env.PI_ANYROUTER_CC_DEBUG === "1";
|
|
53
|
+
const DEBUG_DIR = process.env.PI_ANYROUTER_CC_DEBUG_DIR || join(process.cwd(), ".pi", "anyrouter-cc-debug");
|
|
54
|
+
// Captured from the locally installed Claude Code on 2026-07-11.
|
|
55
|
+
const CLAUDE_CODE_VERSION = "2.1.206";
|
|
56
|
+
const CLAUDE_CODE_VERSION_BUILD = "2.1.206.3ee";
|
|
57
|
+
const STAINLESS_PACKAGE_VERSION = "0.94.0";
|
|
58
|
+
const STAINLESS_OS = "Linux";
|
|
59
|
+
const STAINLESS_ARCH = "x64";
|
|
60
|
+
const STAINLESS_RUNTIME = "node";
|
|
61
|
+
const STAINLESS_RUNTIME_VERSION = "v26.3.0";
|
|
62
|
+
const ANTHROPIC_BETA = "claude-code-20250219,context-1m-2025-08-07,interleaved-thinking-2025-05-14,thinking-token-count-2026-05-13,context-management-2025-06-27,prompt-caching-scope-2026-01-05,mid-conversation-system-2026-04-07,effort-2025-11-24";
|
|
63
|
+
const CLAUDE_DEVICE_ID = randomBytes(32).toString("hex");
|
|
64
|
+
const CODEX_VERSION = "0.153.4";
|
|
65
|
+
const CODEX_INSTALLATION_ID = randomUUID();
|
|
66
|
+
|
|
67
|
+
const NAME_MAP: Record<string, string> = {
|
|
68
|
+
read: "Read",
|
|
69
|
+
write: "Write",
|
|
70
|
+
edit: "Edit",
|
|
71
|
+
bash: "Bash",
|
|
72
|
+
grep: "Grep",
|
|
73
|
+
find: "Glob",
|
|
74
|
+
glob: "Glob",
|
|
75
|
+
ls: "LS",
|
|
76
|
+
todowrite: "TodoWrite",
|
|
77
|
+
webfetch: "WebFetch",
|
|
78
|
+
websearch: "WebSearch",
|
|
79
|
+
google_search: "Google_Search",
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
function toClaudeCodeName(name?: string | null) {
|
|
83
|
+
if (!name || typeof name !== "string") return name;
|
|
84
|
+
return NAME_MAP[name.toLowerCase()] ?? name.charAt(0).toUpperCase() + name.slice(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function fromClaudeCodeName(name?: string | null) {
|
|
88
|
+
if (!name || typeof name !== "string") return name;
|
|
89
|
+
const lower = name.toLowerCase();
|
|
90
|
+
for (const [from, to] of Object.entries(NAME_MAP)) {
|
|
91
|
+
if (to.toLowerCase() === lower) return from;
|
|
92
|
+
}
|
|
93
|
+
return name.charAt(0).toLowerCase() + name.slice(1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function sanitizeText(text: string) {
|
|
97
|
+
return text.replace(/[\uD800-\uDFFF]/g, "\uFFFD");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function resolveConfigValue(value?: string) {
|
|
101
|
+
if (!value) return "";
|
|
102
|
+
if (value.startsWith("!")) {
|
|
103
|
+
throw new Error("anyrouter does not support shell-command apiKey values. Use a literal key, env var name, or PI_ANYROUTER_CC_API_KEY.");
|
|
104
|
+
}
|
|
105
|
+
return process.env[value] || value;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function loadSourceProvider() {
|
|
109
|
+
let content = "";
|
|
110
|
+
try {
|
|
111
|
+
content = readFileSync(CONFIG_PATH, "utf8");
|
|
112
|
+
} catch {
|
|
113
|
+
throw new Error(`Config file not found: ${CONFIG_PATH}. Create ~/.pi/agent/anyrouter.json or set PI_ANYROUTER_CC_CONFIG.`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let parsed: ProviderConfigFile;
|
|
117
|
+
try {
|
|
118
|
+
parsed = JSON.parse(content) as ProviderConfigFile;
|
|
119
|
+
} catch (error) {
|
|
120
|
+
throw new Error(`Invalid JSON in ${CONFIG_PATH}: ${error instanceof Error ? error.message : String(error)}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const baseUrl = process.env.PI_ANYROUTER_CC_BASE_URL || parsed.baseUrl;
|
|
124
|
+
const apiKey = process.env.PI_ANYROUTER_CC_API_KEY || resolveConfigValue(parsed.apiKey);
|
|
125
|
+
const models = parsed.models || [];
|
|
126
|
+
|
|
127
|
+
if (!baseUrl) throw new Error(`Missing baseUrl in ${CONFIG_PATH}. You can also set PI_ANYROUTER_CC_BASE_URL.`);
|
|
128
|
+
if (!apiKey) throw new Error(`Missing apiKey in ${CONFIG_PATH}. You can also set PI_ANYROUTER_CC_API_KEY.`);
|
|
129
|
+
if (!models.length) throw new Error(`No models configured in ${CONFIG_PATH}. Add at least one model entry.`);
|
|
130
|
+
|
|
131
|
+
return { baseUrl, apiKey, models };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function convertContentBlocks(content: (TextContent | ImageContent)[]) {
|
|
135
|
+
const hasImages = content.some((c) => c.type === "image");
|
|
136
|
+
if (!hasImages) return sanitizeText(content.map((c) => (c as TextContent).text).join("\n"));
|
|
137
|
+
|
|
138
|
+
const blocks = content.map((block) => {
|
|
139
|
+
if (block.type === "text") return { type: "text", text: sanitizeText(block.text) };
|
|
140
|
+
return { type: "image", source: { type: "base64", media_type: block.mimeType, data: block.data } };
|
|
141
|
+
});
|
|
142
|
+
if (!blocks.some((b) => b.type === "text")) blocks.unshift({ type: "text", text: "(see attached image)" });
|
|
143
|
+
return blocks;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function convertMessages(messages: Message[]) {
|
|
147
|
+
const params: any[] = [];
|
|
148
|
+
for (let i = 0; i < messages.length; i++) {
|
|
149
|
+
const msg = messages[i];
|
|
150
|
+
if (msg.role === "user") {
|
|
151
|
+
if (typeof msg.content === "string") {
|
|
152
|
+
const text = sanitizeText(msg.content);
|
|
153
|
+
if (text.trim()) params.push({ role: "user", content: [{ type: "text", text }] });
|
|
154
|
+
} else {
|
|
155
|
+
const blocks = msg.content.map((item) =>
|
|
156
|
+
item.type === "text"
|
|
157
|
+
? { type: "text", text: sanitizeText(item.text) }
|
|
158
|
+
: { type: "image", source: { type: "base64", media_type: item.mimeType, data: item.data } },
|
|
159
|
+
);
|
|
160
|
+
if (blocks.length > 0) params.push({ role: "user", content: blocks });
|
|
161
|
+
}
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (msg.role === "assistant") {
|
|
166
|
+
const blocks: any[] = [];
|
|
167
|
+
for (const block of msg.content) {
|
|
168
|
+
if (block.type === "text" && block.text.trim()) blocks.push({ type: "text", text: sanitizeText(block.text) });
|
|
169
|
+
else if (block.type === "thinking" && block.thinking.trim()) {
|
|
170
|
+
if ((block as ThinkingContent).thinkingSignature) {
|
|
171
|
+
blocks.push({ type: "thinking", thinking: sanitizeText(block.thinking), signature: (block as ThinkingContent).thinkingSignature });
|
|
172
|
+
} else {
|
|
173
|
+
blocks.push({ type: "text", text: sanitizeText(block.thinking) });
|
|
174
|
+
}
|
|
175
|
+
} else if (block.type === "toolCall") {
|
|
176
|
+
blocks.push({ type: "tool_use", id: block.id, name: toClaudeCodeName(block.name), input: block.arguments });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (blocks.length > 0) params.push({ role: "assistant", content: blocks });
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (msg.role === "toolResult") {
|
|
184
|
+
const toolResults: any[] = [];
|
|
185
|
+
const pushToolResult = (toolMsg: ToolResultMessage) => {
|
|
186
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolMsg.toolCallId, content: convertContentBlocks(toolMsg.content), is_error: toolMsg.isError });
|
|
187
|
+
};
|
|
188
|
+
pushToolResult(msg as ToolResultMessage);
|
|
189
|
+
let j = i + 1;
|
|
190
|
+
while (j < messages.length && messages[j].role === "toolResult") {
|
|
191
|
+
pushToolResult(messages[j] as ToolResultMessage);
|
|
192
|
+
j++;
|
|
193
|
+
}
|
|
194
|
+
i = j - 1;
|
|
195
|
+
params.push({ role: "user", content: toolResults });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (params.length > 0) {
|
|
200
|
+
const last = params[params.length - 1];
|
|
201
|
+
if (last.role === "user" && Array.isArray(last.content) && last.content.length > 0) {
|
|
202
|
+
last.content[last.content.length - 1].cache_control = { type: "ephemeral" };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return params;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function convertTools(tools: Tool[]) {
|
|
209
|
+
return tools.map((tool) => ({
|
|
210
|
+
name: toClaudeCodeName(tool.name),
|
|
211
|
+
description: tool.description,
|
|
212
|
+
input_schema: {
|
|
213
|
+
type: "object",
|
|
214
|
+
properties: (tool.parameters as any).properties || {},
|
|
215
|
+
required: (tool.parameters as any).required || [],
|
|
216
|
+
},
|
|
217
|
+
}));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function mapReasoningEffort(level?: SimpleStreamOptions["reasoning"]) {
|
|
221
|
+
switch (level) {
|
|
222
|
+
case "minimal":
|
|
223
|
+
case "low": return "low";
|
|
224
|
+
case "medium": return "medium";
|
|
225
|
+
case "high": return "high";
|
|
226
|
+
case "xhigh": return "xhigh";
|
|
227
|
+
default: return "medium";
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function mapStopReason(reason: string): StopReason {
|
|
232
|
+
switch (reason) {
|
|
233
|
+
case "end_turn":
|
|
234
|
+
case "pause_turn":
|
|
235
|
+
case "stop_sequence": return "stop";
|
|
236
|
+
case "max_tokens": return "length";
|
|
237
|
+
case "tool_use": return "toolUse";
|
|
238
|
+
default: return "error";
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function getClaudeCodeHeaders(apiKey: string, retryCount = 0, sessionId: string) {
|
|
243
|
+
return {
|
|
244
|
+
"content-type": "application/json",
|
|
245
|
+
accept: "application/json",
|
|
246
|
+
authorization: `Bearer ${apiKey}`,
|
|
247
|
+
"anthropic-version": "2023-06-01",
|
|
248
|
+
"anthropic-dangerous-direct-browser-access": "true",
|
|
249
|
+
"anthropic-beta": ANTHROPIC_BETA,
|
|
250
|
+
"user-agent": `claude-cli/${CLAUDE_CODE_VERSION} (external, sdk-cli)`,
|
|
251
|
+
"x-app": "cli",
|
|
252
|
+
"x-claude-code-session-id": sessionId,
|
|
253
|
+
"x-stainless-retry-count": String(retryCount),
|
|
254
|
+
"x-stainless-timeout": "600",
|
|
255
|
+
"x-stainless-lang": "js",
|
|
256
|
+
"x-stainless-package-version": STAINLESS_PACKAGE_VERSION,
|
|
257
|
+
"x-stainless-os": STAINLESS_OS,
|
|
258
|
+
"x-stainless-arch": STAINLESS_ARCH,
|
|
259
|
+
"x-stainless-runtime": STAINLESS_RUNTIME,
|
|
260
|
+
"x-stainless-runtime-version": STAINLESS_RUNTIME_VERSION,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function createClaudeCodeMetadata(sessionId: string) {
|
|
265
|
+
return {
|
|
266
|
+
user_id: JSON.stringify({
|
|
267
|
+
device_id: CLAUDE_DEVICE_ID,
|
|
268
|
+
account_uuid: "",
|
|
269
|
+
session_id: sessionId,
|
|
270
|
+
}),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function createClaudeCodeSystem(systemPrompt: string) {
|
|
275
|
+
return [
|
|
276
|
+
{ type: "text", text: `x-anthropic-billing-header: cc_version=${CLAUDE_CODE_VERSION_BUILD}; cc_entrypoint=sdk-cli;` },
|
|
277
|
+
{ type: "text", text: "You are a Claude agent, built on Anthropic's Claude Agent SDK.", cache_control: { type: "ephemeral" } },
|
|
278
|
+
{ type: "text", text: sanitizeText(systemPrompt), cache_control: { type: "ephemeral" } },
|
|
279
|
+
];
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function redactHeaders(headers: Record<string, string>) {
|
|
283
|
+
const redacted = { ...headers };
|
|
284
|
+
if (redacted.authorization) redacted.authorization = "Bearer ***";
|
|
285
|
+
if (redacted["x-api-key"]) redacted["x-api-key"] = "***";
|
|
286
|
+
return redacted;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const PROXY_AGENTS = new Map<string, ProxyAgent>();
|
|
290
|
+
|
|
291
|
+
function hostMatchesNoProxy(hostname: string, pattern: string) {
|
|
292
|
+
const item = pattern.trim().toLowerCase();
|
|
293
|
+
if (!item) return false;
|
|
294
|
+
if (item === "*") return true;
|
|
295
|
+
const host = hostname.toLowerCase();
|
|
296
|
+
if (item.startsWith(".")) return host === item.slice(1) || host.endsWith(item);
|
|
297
|
+
return host === item || host.endsWith(`.${item}`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function getProxyUrl(url: string) {
|
|
301
|
+
const parsed = new URL(url);
|
|
302
|
+
const noProxy = process.env.NO_PROXY || process.env.no_proxy || "";
|
|
303
|
+
if (noProxy.split(",").some((item) => hostMatchesNoProxy(parsed.hostname, item))) return undefined;
|
|
304
|
+
if (parsed.protocol === "https:") return process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy;
|
|
305
|
+
return process.env.HTTP_PROXY || process.env.http_proxy;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function getProxyAgent(proxyUrl: string) {
|
|
309
|
+
let agent = PROXY_AGENTS.get(proxyUrl);
|
|
310
|
+
if (!agent) {
|
|
311
|
+
agent = new ProxyAgent(proxyUrl);
|
|
312
|
+
PROXY_AGENTS.set(proxyUrl, agent);
|
|
313
|
+
}
|
|
314
|
+
return agent;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function fetchWithProxy(url: string, init: FetchInit) {
|
|
318
|
+
const proxyUrl = getProxyUrl(url);
|
|
319
|
+
if (!proxyUrl) return fetch(url, init);
|
|
320
|
+
return undiciFetch(url, { ...init, dispatcher: getProxyAgent(proxyUrl) } as any) as unknown as Promise<Response>;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function writeDebugFile(kind: "request" | "response" | "error", modelId: string, requestId: string | undefined, payload: Json) {
|
|
324
|
+
if (!DEBUG_ENABLED) return;
|
|
325
|
+
mkdirSync(DEBUG_DIR, { recursive: true });
|
|
326
|
+
const safeModel = modelId.replace(/[^a-zA-Z0-9._-]+/g, "_");
|
|
327
|
+
const safeRequestId = (requestId || "no-request-id").replace(/[^a-zA-Z0-9._-]+/g, "_");
|
|
328
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
329
|
+
const path = join(DEBUG_DIR, `${timestamp}-${safeModel}-${safeRequestId}-${kind}.json`);
|
|
330
|
+
writeFileSync(path, JSON.stringify(payload, null, 2), "utf8");
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function delay(ms: number) {
|
|
334
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function isRetryableStatus(status: number) {
|
|
338
|
+
return [408, 409, 429, 500, 502, 503, 504, 520, 522, 524].includes(status);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function parseRetryAfterMs(value: string | null) {
|
|
342
|
+
if (!value) return undefined;
|
|
343
|
+
const seconds = Number(value);
|
|
344
|
+
if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1000;
|
|
345
|
+
const at = Date.parse(value);
|
|
346
|
+
if (Number.isFinite(at)) {
|
|
347
|
+
const delta = at - Date.now();
|
|
348
|
+
return delta > 0 ? delta : 0;
|
|
349
|
+
}
|
|
350
|
+
return undefined;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function getRetryDelayMs(attempt: number, retryAfterMs?: number) {
|
|
354
|
+
if (typeof retryAfterMs === "number") return Math.max(0, Math.min(retryAfterMs, 30_000));
|
|
355
|
+
const base = Math.min(1000 * (2 ** attempt), 15_000);
|
|
356
|
+
const jitter = Math.floor(Math.random() * 250);
|
|
357
|
+
return base + jitter;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function getStreamMode(): StreamMode {
|
|
361
|
+
// AnyRouter's Claude Code subscription route is SSE-first. Keep the exact
|
|
362
|
+
// transport by default instead of falling back to a generic JSON request.
|
|
363
|
+
const value = String(process.env.PI_ANYROUTER_CC_STREAM_MODE || "force").trim().toLowerCase();
|
|
364
|
+
if (["1", "true", "on", "auto"].includes(value)) return "auto";
|
|
365
|
+
if (["force", "only"].includes(value)) return "force";
|
|
366
|
+
return "off";
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function createEmptyUsage() {
|
|
370
|
+
return {
|
|
371
|
+
input: 0,
|
|
372
|
+
output: 0,
|
|
373
|
+
cacheRead: 0,
|
|
374
|
+
cacheWrite: 0,
|
|
375
|
+
totalTokens: 0,
|
|
376
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function tryParseJson(text: string) {
|
|
381
|
+
try {
|
|
382
|
+
return text ? JSON.parse(text) : {};
|
|
383
|
+
} catch {
|
|
384
|
+
return undefined;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function extractRequestId(parsed: any, headers: Headers) {
|
|
389
|
+
return parsed?.error?.message?.match(/request id:\s*([^\)]+)/i)?.[1]
|
|
390
|
+
|| headers.get("x-oneapi-request-id")
|
|
391
|
+
|| undefined;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function updateUsageFromAnthropic(output: AssistantMessage, usage: any, model: Model<Api>) {
|
|
395
|
+
if (usage?.input_tokens != null) output.usage.input = usage.input_tokens;
|
|
396
|
+
if (usage?.output_tokens != null) output.usage.output = usage.output_tokens;
|
|
397
|
+
if (usage?.cache_read_input_tokens != null) output.usage.cacheRead = usage.cache_read_input_tokens;
|
|
398
|
+
if (usage?.cache_creation_input_tokens != null) output.usage.cacheWrite = usage.cache_creation_input_tokens;
|
|
399
|
+
output.usage.totalTokens = output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite;
|
|
400
|
+
calculateCost(model, output.usage);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function resetOutputState(output: AssistantMessage) {
|
|
404
|
+
output.content = [];
|
|
405
|
+
output.usage = createEmptyUsage();
|
|
406
|
+
output.stopReason = "stop";
|
|
407
|
+
output.errorMessage = undefined;
|
|
408
|
+
output.responseId = undefined;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function isCodexModel(modelId: string, configuredApi?: string) {
|
|
412
|
+
if (configuredApi) return configuredApi === "openai-codex-responses";
|
|
413
|
+
return /(?:^|[-_.])(gpt|codex)(?:[-_.]|$)/i.test(modelId) || /^o\d(?:[-_.]|$)/i.test(modelId);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function getCodexResponsesUrl(baseUrl: string) {
|
|
417
|
+
const normalized = baseUrl.replace(/\/+$/, "");
|
|
418
|
+
if (normalized.endsWith("/responses")) return normalized;
|
|
419
|
+
if (normalized.endsWith("/v1")) return `${normalized}/responses`;
|
|
420
|
+
return `${normalized}/v1/responses`;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function convertCodexMessages(context: Context) {
|
|
424
|
+
const input: any[] = [];
|
|
425
|
+
if (context.systemPrompt) {
|
|
426
|
+
input.push({
|
|
427
|
+
type: "message",
|
|
428
|
+
role: "developer",
|
|
429
|
+
content: [{ type: "input_text", text: sanitizeText(context.systemPrompt) }],
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
for (const msg of context.messages) {
|
|
434
|
+
if (msg.role === "user") {
|
|
435
|
+
if (typeof msg.content === "string") {
|
|
436
|
+
if (msg.content.trim()) {
|
|
437
|
+
input.push({ type: "message", role: "user", content: [{ type: "input_text", text: sanitizeText(msg.content) }] });
|
|
438
|
+
}
|
|
439
|
+
} else {
|
|
440
|
+
const content = msg.content.map((item) => item.type === "text"
|
|
441
|
+
? { type: "input_text", text: sanitizeText(item.text) }
|
|
442
|
+
: { type: "input_image", detail: "auto", image_url: `data:${item.mimeType};base64,${item.data}` });
|
|
443
|
+
if (content.length) input.push({ type: "message", role: "user", content });
|
|
444
|
+
}
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (msg.role === "assistant") {
|
|
449
|
+
for (const block of msg.content) {
|
|
450
|
+
if (block.type === "thinking" && block.thinkingSignature) {
|
|
451
|
+
const reasoning = tryParseJson(block.thinkingSignature);
|
|
452
|
+
if (reasoning) input.push(reasoning);
|
|
453
|
+
} else if (block.type === "text" && block.text.trim()) {
|
|
454
|
+
input.push({
|
|
455
|
+
type: "message",
|
|
456
|
+
role: "assistant",
|
|
457
|
+
status: "completed",
|
|
458
|
+
content: [{ type: "output_text", text: sanitizeText(block.text), annotations: [] }],
|
|
459
|
+
});
|
|
460
|
+
} else if (block.type === "toolCall") {
|
|
461
|
+
const [callId, itemId] = block.id.split("|");
|
|
462
|
+
input.push({
|
|
463
|
+
type: "function_call",
|
|
464
|
+
...(itemId ? { id: itemId } : {}),
|
|
465
|
+
call_id: callId,
|
|
466
|
+
name: block.name,
|
|
467
|
+
arguments: JSON.stringify(block.arguments),
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (msg.role === "toolResult") {
|
|
475
|
+
const toolMsg = msg as ToolResultMessage;
|
|
476
|
+
const text = toolMsg.content.filter((item) => item.type === "text").map((item) => (item as TextContent).text).join("\n");
|
|
477
|
+
const images = toolMsg.content.filter((item) => item.type === "image") as ImageContent[];
|
|
478
|
+
const output = images.length
|
|
479
|
+
? [
|
|
480
|
+
...(text ? [{ type: "input_text", text: sanitizeText(text) }] : []),
|
|
481
|
+
...images.map((image) => ({ type: "input_image", detail: "auto", image_url: `data:${image.mimeType};base64,${image.data}` })),
|
|
482
|
+
]
|
|
483
|
+
: sanitizeText(text || (images.length ? "(see attached image)" : "(no tool output)"));
|
|
484
|
+
input.push({ type: "function_call_output", call_id: toolMsg.toolCallId.split("|")[0], output });
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return input;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function convertCodexTools(tools: Tool[]) {
|
|
491
|
+
return tools.map((tool) => ({
|
|
492
|
+
type: "function",
|
|
493
|
+
name: tool.name,
|
|
494
|
+
description: tool.description,
|
|
495
|
+
parameters: tool.parameters,
|
|
496
|
+
strict: false,
|
|
497
|
+
}));
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function createCodexMetadata(sessionId: string, turnId: string) {
|
|
501
|
+
const windowId = `${sessionId}:0`;
|
|
502
|
+
const turnMetadata = JSON.stringify({
|
|
503
|
+
installation_id: CODEX_INSTALLATION_ID,
|
|
504
|
+
session_id: sessionId,
|
|
505
|
+
thread_id: sessionId,
|
|
506
|
+
turn_id: turnId,
|
|
507
|
+
window_id: windowId,
|
|
508
|
+
request_kind: "turn",
|
|
509
|
+
thread_source: "user",
|
|
510
|
+
turn_started_at_unix_ms: Date.now(),
|
|
511
|
+
});
|
|
512
|
+
return {
|
|
513
|
+
windowId,
|
|
514
|
+
turnMetadata,
|
|
515
|
+
clientMetadata: {
|
|
516
|
+
session_id: sessionId,
|
|
517
|
+
thread_id: sessionId,
|
|
518
|
+
turn_id: turnId,
|
|
519
|
+
"x-codex-installation-id": CODEX_INSTALLATION_ID,
|
|
520
|
+
"x-codex-window-id": windowId,
|
|
521
|
+
"x-codex-turn-metadata": turnMetadata,
|
|
522
|
+
},
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function createCodexHeaders(apiKey: string, sessionId: string, metadata: ReturnType<typeof createCodexMetadata>) {
|
|
527
|
+
return {
|
|
528
|
+
authorization: `Bearer ${apiKey}`,
|
|
529
|
+
accept: "text/event-stream",
|
|
530
|
+
"content-type": "application/json",
|
|
531
|
+
originator: "codex_exec",
|
|
532
|
+
"user-agent": `codex_exec/${CODEX_VERSION} (Linux; x86_64) (codex_exec; ${CODEX_VERSION})`,
|
|
533
|
+
"x-openai-internal-codex-responses-lite": "true",
|
|
534
|
+
"x-codex-beta-features": "remote_compaction_v2",
|
|
535
|
+
"x-codex-window-id": metadata.windowId,
|
|
536
|
+
"x-codex-turn-metadata": metadata.turnMetadata,
|
|
537
|
+
"x-client-request-id": sessionId,
|
|
538
|
+
"session-id": sessionId,
|
|
539
|
+
"thread-id": sessionId,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function buildCodexRequestBody(model: Model<Api>, context: Context, options: SimpleStreamOptions | undefined, sessionId: string, metadata: ReturnType<typeof createCodexMetadata>) {
|
|
544
|
+
const body: Json = {
|
|
545
|
+
model: model.id,
|
|
546
|
+
input: convertCodexMessages(context),
|
|
547
|
+
tool_choice: "auto",
|
|
548
|
+
parallel_tool_calls: false,
|
|
549
|
+
reasoning: {
|
|
550
|
+
effort: mapReasoningEffort(options?.reasoning),
|
|
551
|
+
context: "all_turns",
|
|
552
|
+
},
|
|
553
|
+
store: false,
|
|
554
|
+
stream: true,
|
|
555
|
+
text: { verbosity: "low" },
|
|
556
|
+
max_output_tokens: options?.maxTokens || model.maxTokens,
|
|
557
|
+
include: ["reasoning.encrypted_content"],
|
|
558
|
+
prompt_cache_key: sessionId,
|
|
559
|
+
client_metadata: metadata.clientMetadata,
|
|
560
|
+
};
|
|
561
|
+
if (context.tools?.length) body.tools = convertCodexTools(context.tools);
|
|
562
|
+
return body;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function applyCodexUsage(output: AssistantMessage, response: any, model: Model<Api>) {
|
|
566
|
+
const usage = response?.usage;
|
|
567
|
+
if (!usage) return;
|
|
568
|
+
const cached = usage.input_tokens_details?.cached_tokens || 0;
|
|
569
|
+
const cacheWrite = usage.input_tokens_details?.cache_write_tokens || 0;
|
|
570
|
+
output.usage.input = Math.max(0, (usage.input_tokens || 0) - cached - cacheWrite);
|
|
571
|
+
output.usage.output = usage.output_tokens || 0;
|
|
572
|
+
output.usage.cacheRead = cached;
|
|
573
|
+
output.usage.cacheWrite = cacheWrite;
|
|
574
|
+
output.usage.totalTokens = usage.total_tokens || output.usage.input + output.usage.output + cached + cacheWrite;
|
|
575
|
+
calculateCost(model, output.usage);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function applyCodexSsePayload(payload: any, output: AssistantMessage, stream: AssistantMessageEventStream, model: Model<Api>, slots: Map<number, any>) {
|
|
579
|
+
const type = payload?.type;
|
|
580
|
+
if (!type || type === "response.in_progress" || type === "response.metadata") return;
|
|
581
|
+
if (type === "error") throw new Error(payload.message || JSON.stringify(payload));
|
|
582
|
+
if (type === "response.failed") throw new Error(payload.response?.error?.message || "Codex response failed");
|
|
583
|
+
|
|
584
|
+
if (type === "response.created") {
|
|
585
|
+
output.responseId = payload.response?.id || output.responseId;
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (type === "response.output_item.added") {
|
|
590
|
+
const item = payload.item;
|
|
591
|
+
if (item?.type === "message") {
|
|
592
|
+
const block = { type: "text", text: "" };
|
|
593
|
+
output.content.push(block as any);
|
|
594
|
+
const contentIndex = output.content.length - 1;
|
|
595
|
+
slots.set(payload.output_index, { type: "text", block, contentIndex });
|
|
596
|
+
stream.push({ type: "text_start", contentIndex, partial: output });
|
|
597
|
+
} else if (item?.type === "reasoning") {
|
|
598
|
+
const block = { type: "thinking", thinking: "", thinkingSignature: "" };
|
|
599
|
+
output.content.push(block as any);
|
|
600
|
+
const contentIndex = output.content.length - 1;
|
|
601
|
+
slots.set(payload.output_index, { type: "thinking", block, contentIndex });
|
|
602
|
+
stream.push({ type: "thinking_start", contentIndex, partial: output });
|
|
603
|
+
} else if (item?.type === "function_call") {
|
|
604
|
+
const block = { type: "toolCall", id: `${item.call_id}|${item.id}`, name: item.name, arguments: {}, partialJson: item.arguments || "" };
|
|
605
|
+
output.content.push(block as any);
|
|
606
|
+
const contentIndex = output.content.length - 1;
|
|
607
|
+
slots.set(payload.output_index, { type: "toolCall", block, contentIndex });
|
|
608
|
+
stream.push({ type: "toolcall_start", contentIndex, partial: output });
|
|
609
|
+
}
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const slot = slots.get(payload.output_index);
|
|
614
|
+
if (type === "response.output_text.delta" && slot?.type === "text") {
|
|
615
|
+
slot.block.text += String(payload.delta || "");
|
|
616
|
+
stream.push({ type: "text_delta", contentIndex: slot.contentIndex, delta: String(payload.delta || ""), partial: output });
|
|
617
|
+
} else if ((type === "response.reasoning_summary_text.delta" || type === "response.reasoning_text.delta") && slot?.type === "thinking") {
|
|
618
|
+
slot.block.thinking += String(payload.delta || "");
|
|
619
|
+
stream.push({ type: "thinking_delta", contentIndex: slot.contentIndex, delta: String(payload.delta || ""), partial: output });
|
|
620
|
+
} else if (type === "response.function_call_arguments.delta" && slot?.type === "toolCall") {
|
|
621
|
+
slot.block.partialJson += String(payload.delta || "");
|
|
622
|
+
const parsed = tryParseJson(slot.block.partialJson);
|
|
623
|
+
if (parsed !== undefined) slot.block.arguments = parsed;
|
|
624
|
+
stream.push({ type: "toolcall_delta", contentIndex: slot.contentIndex, delta: String(payload.delta || ""), partial: output });
|
|
625
|
+
} else if (type === "response.function_call_arguments.done" && slot?.type === "toolCall") {
|
|
626
|
+
slot.block.partialJson = String(payload.arguments || slot.block.partialJson);
|
|
627
|
+
slot.block.arguments = tryParseJson(slot.block.partialJson) || {};
|
|
628
|
+
} else if (type === "response.output_item.done") {
|
|
629
|
+
const item = payload.item;
|
|
630
|
+
if (slot?.type === "text" && item?.type === "message") {
|
|
631
|
+
slot.block.text = item.content?.map((part: any) => part.text || part.refusal || "").join("") || slot.block.text;
|
|
632
|
+
stream.push({ type: "text_end", contentIndex: slot.contentIndex, content: slot.block.text, partial: output });
|
|
633
|
+
} else if (slot?.type === "thinking" && item?.type === "reasoning") {
|
|
634
|
+
slot.block.thinking = item.summary?.map((part: any) => part.text).join("\n\n") || item.content?.map((part: any) => part.text).join("\n\n") || slot.block.thinking;
|
|
635
|
+
slot.block.thinkingSignature = JSON.stringify(item);
|
|
636
|
+
stream.push({ type: "thinking_end", contentIndex: slot.contentIndex, content: slot.block.thinking, partial: output });
|
|
637
|
+
} else if (slot?.type === "toolCall" && item?.type === "function_call") {
|
|
638
|
+
slot.block.arguments = tryParseJson(item.arguments || slot.block.partialJson) || {};
|
|
639
|
+
delete slot.block.partialJson;
|
|
640
|
+
stream.push({ type: "toolcall_end", contentIndex: slot.contentIndex, toolCall: slot.block, partial: output });
|
|
641
|
+
}
|
|
642
|
+
slots.delete(payload.output_index);
|
|
643
|
+
} else if (type === "response.completed" || type === "response.incomplete") {
|
|
644
|
+
output.responseId = payload.response?.id || output.responseId;
|
|
645
|
+
applyCodexUsage(output, payload.response, model);
|
|
646
|
+
output.stopReason = type === "response.incomplete" ? "length" : output.content.some((block) => block.type === "toolCall") ? "toolUse" : "stop";
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
async function tryStreamAnyRouterCodex(url: string, body: Json, apiKey: string, model: Model<Api>, output: AssistantMessage, stream: AssistantMessageEventStream, sessionId: string, metadata: ReturnType<typeof createCodexMetadata>, signal?: AbortSignal) {
|
|
651
|
+
const bodyText = JSON.stringify(body);
|
|
652
|
+
const maxRetries = Math.max(0, Number(process.env.PI_ANYROUTER_CC_MAX_RETRIES || "10") || 0);
|
|
653
|
+
let response: Response | undefined;
|
|
654
|
+
|
|
655
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
656
|
+
const headers = createCodexHeaders(apiKey, sessionId, metadata);
|
|
657
|
+
if (attempt === 0) writeDebugFile("request", model.id, undefined, { url, headers: redactHeaders(headers), body, transport: "codex-sse" });
|
|
658
|
+
try {
|
|
659
|
+
response = await fetchWithProxy(url, { method: "POST", signal, headers, body: bodyText });
|
|
660
|
+
} catch (error) {
|
|
661
|
+
if (attempt < maxRetries && !signal?.aborted) {
|
|
662
|
+
await delay(getRetryDelayMs(attempt));
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
throw error;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
if (response.ok && (response.headers.get("content-type") || "").includes("text/event-stream")) break;
|
|
669
|
+
const raw = await response.text();
|
|
670
|
+
const parsed = tryParseJson(raw) || { raw };
|
|
671
|
+
const requestId = extractRequestId(parsed, response.headers);
|
|
672
|
+
writeDebugFile("error", model.id, requestId, { status: response.status, requestId, body: parsed, raw, transport: "codex-sse", retryAttempt: attempt });
|
|
673
|
+
if (!response.ok && attempt < maxRetries && isRetryableStatus(response.status)) {
|
|
674
|
+
await delay(getRetryDelayMs(attempt, parseRetryAfterMs(response.headers.get("retry-after"))));
|
|
675
|
+
response = undefined;
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
throw new Error(raw || `HTTP ${response.status}`);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (!response?.body) throw new Error("Codex stream response body missing");
|
|
682
|
+
const slots = new Map<number, any>();
|
|
683
|
+
const reader = response.body.getReader();
|
|
684
|
+
const decoder = new TextDecoder();
|
|
685
|
+
let buffer = "";
|
|
686
|
+
let terminal = false;
|
|
687
|
+
while (true) {
|
|
688
|
+
const { value, done } = await reader.read();
|
|
689
|
+
buffer += decoder.decode(value || new Uint8Array(), { stream: !done });
|
|
690
|
+
let parsedChunk = nextSseChunk(buffer);
|
|
691
|
+
while (parsedChunk) {
|
|
692
|
+
buffer = parsedChunk.rest;
|
|
693
|
+
const event = parseSseEvent(parsedChunk.chunk);
|
|
694
|
+
if (event.data && event.data !== "[DONE]") {
|
|
695
|
+
const payload = tryParseJson(event.data);
|
|
696
|
+
if (!payload) throw new Error(`invalid Codex SSE payload: ${event.data.slice(0, 200)}`);
|
|
697
|
+
applyCodexSsePayload(payload, output, stream, model, slots);
|
|
698
|
+
if (payload.type === "response.completed" || payload.type === "response.incomplete") terminal = true;
|
|
699
|
+
}
|
|
700
|
+
parsedChunk = nextSseChunk(buffer);
|
|
701
|
+
}
|
|
702
|
+
if (done) break;
|
|
703
|
+
}
|
|
704
|
+
const tail = buffer.trim();
|
|
705
|
+
if (tail) {
|
|
706
|
+
const event = parseSseEvent(tail);
|
|
707
|
+
if (event.data && event.data !== "[DONE]") {
|
|
708
|
+
const payload = tryParseJson(event.data);
|
|
709
|
+
if (!payload) throw new Error(`invalid Codex SSE payload: ${event.data.slice(0, 200)}`);
|
|
710
|
+
applyCodexSsePayload(payload, output, stream, model, slots);
|
|
711
|
+
if (payload.type === "response.completed" || payload.type === "response.incomplete") terminal = true;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
if (!terminal) throw new Error("Codex stream ended before a terminal response event");
|
|
715
|
+
writeDebugFile("response", model.id, response.headers.get("x-oneapi-request-id") || undefined, {
|
|
716
|
+
status: response.status,
|
|
717
|
+
responseId: output.responseId,
|
|
718
|
+
stopReason: output.stopReason,
|
|
719
|
+
usage: output.usage,
|
|
720
|
+
transport: "codex-sse",
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function applyJsonResponseToOutput(response: any, output: AssistantMessage, stream: AssistantMessageEventStream, model: Model<Api>) {
|
|
725
|
+
updateUsageFromAnthropic(output, response?.usage || {}, model);
|
|
726
|
+
output.stopReason = mapStopReason(response?.stop_reason || "end_turn");
|
|
727
|
+
|
|
728
|
+
const content = Array.isArray(response?.content) ? response.content : [];
|
|
729
|
+
for (const block of content) {
|
|
730
|
+
if (block?.type === "text") {
|
|
731
|
+
output.content.push({ type: "text", text: "" });
|
|
732
|
+
const contentIndex = output.content.length - 1;
|
|
733
|
+
stream.push({ type: "text_start", contentIndex, partial: output });
|
|
734
|
+
const text = String(block.text || "");
|
|
735
|
+
(output.content[contentIndex] as any).text = text;
|
|
736
|
+
if (text) stream.push({ type: "text_delta", contentIndex, delta: text, partial: output });
|
|
737
|
+
stream.push({ type: "text_end", contentIndex, content: text, partial: output });
|
|
738
|
+
} else if (block?.type === "thinking") {
|
|
739
|
+
output.content.push({ type: "thinking", thinking: String(block.thinking || ""), thinkingSignature: block.signature || "" } as any);
|
|
740
|
+
const contentIndex = output.content.length - 1;
|
|
741
|
+
stream.push({ type: "thinking_start", contentIndex, partial: output });
|
|
742
|
+
if (block.thinking) stream.push({ type: "thinking_delta", contentIndex, delta: String(block.thinking), partial: output });
|
|
743
|
+
stream.push({ type: "thinking_end", contentIndex, content: String(block.thinking || ""), partial: output });
|
|
744
|
+
} else if (block?.type === "tool_use") {
|
|
745
|
+
const toolCall = { type: "toolCall" as const, id: block.id, name: fromClaudeCodeName(block.name), arguments: block.input || {} };
|
|
746
|
+
output.content.push(toolCall as any);
|
|
747
|
+
const contentIndex = output.content.length - 1;
|
|
748
|
+
stream.push({ type: "toolcall_start", contentIndex, partial: output });
|
|
749
|
+
stream.push({ type: "toolcall_delta", contentIndex, delta: JSON.stringify(toolCall.arguments), partial: output });
|
|
750
|
+
stream.push({ type: "toolcall_end", contentIndex, toolCall, partial: output });
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function parseSseEvent(chunk: string) {
|
|
756
|
+
let event = "message";
|
|
757
|
+
const data: string[] = [];
|
|
758
|
+
for (const line of chunk.split(/\r?\n/)) {
|
|
759
|
+
if (!line || line.startsWith(":")) continue;
|
|
760
|
+
if (line.startsWith("event:")) event = line.slice(6).trim();
|
|
761
|
+
else if (line.startsWith("data:")) data.push(line.slice(5).trimStart());
|
|
762
|
+
}
|
|
763
|
+
return { event, data: data.join("\n") };
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function nextSseChunk(buffer: string) {
|
|
767
|
+
const unix = buffer.indexOf("\n\n");
|
|
768
|
+
const dos = buffer.indexOf("\r\n\r\n");
|
|
769
|
+
if (unix === -1 && dos === -1) return undefined;
|
|
770
|
+
if (dos !== -1 && (unix === -1 || dos < unix)) {
|
|
771
|
+
return { chunk: buffer.slice(0, dos), rest: buffer.slice(dos + 4) };
|
|
772
|
+
}
|
|
773
|
+
return { chunk: buffer.slice(0, unix), rest: buffer.slice(unix + 2) };
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function applySsePayloadEvent(payload: any, output: AssistantMessage, stream: AssistantMessageEventStream, model: Model<Api>, blockIndexByEventIndex: Map<number, number>) {
|
|
777
|
+
if (!payload?.type || payload.type === "ping" || payload.type === "message_stop") return;
|
|
778
|
+
|
|
779
|
+
if (payload.type === "error") {
|
|
780
|
+
const errorText = payload?.error?.message || payload?.error || payload?.message || JSON.stringify(payload);
|
|
781
|
+
throw new Error(String(errorText));
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
if (payload.type === "message_start") {
|
|
785
|
+
output.responseId = payload.message?.id || output.responseId;
|
|
786
|
+
updateUsageFromAnthropic(output, payload.message?.usage || {}, model);
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
if (payload.type === "content_block_start") {
|
|
791
|
+
const block = payload.content_block;
|
|
792
|
+
if (block?.type === "text") {
|
|
793
|
+
output.content.push({ type: "text", text: "", eventIndex: payload.index } as any);
|
|
794
|
+
const contentIndex = output.content.length - 1;
|
|
795
|
+
blockIndexByEventIndex.set(payload.index, contentIndex);
|
|
796
|
+
stream.push({ type: "text_start", contentIndex, partial: output });
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
if (block?.type === "thinking" || block?.type === "redacted_thinking") {
|
|
800
|
+
output.content.push({
|
|
801
|
+
type: "thinking",
|
|
802
|
+
thinking: block.type === "redacted_thinking" ? "[Reasoning redacted]" : "",
|
|
803
|
+
thinkingSignature: block.type === "redacted_thinking" ? String(block.data || "") : "",
|
|
804
|
+
redacted: block.type === "redacted_thinking" ? true : undefined,
|
|
805
|
+
eventIndex: payload.index,
|
|
806
|
+
} as any);
|
|
807
|
+
const contentIndex = output.content.length - 1;
|
|
808
|
+
blockIndexByEventIndex.set(payload.index, contentIndex);
|
|
809
|
+
stream.push({ type: "thinking_start", contentIndex, partial: output });
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
if (block?.type === "tool_use") {
|
|
813
|
+
const toolCall = {
|
|
814
|
+
type: "toolCall" as const,
|
|
815
|
+
id: block.id,
|
|
816
|
+
name: fromClaudeCodeName(block.name),
|
|
817
|
+
arguments: (block.input as Json) || {},
|
|
818
|
+
partialJson: "",
|
|
819
|
+
eventIndex: payload.index,
|
|
820
|
+
};
|
|
821
|
+
output.content.push(toolCall as any);
|
|
822
|
+
const contentIndex = output.content.length - 1;
|
|
823
|
+
blockIndexByEventIndex.set(payload.index, contentIndex);
|
|
824
|
+
stream.push({ type: "toolcall_start", contentIndex, partial: output });
|
|
825
|
+
}
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
if (payload.type === "content_block_delta") {
|
|
830
|
+
const contentIndex = blockIndexByEventIndex.get(payload.index);
|
|
831
|
+
if (contentIndex == null) return;
|
|
832
|
+
const block = output.content[contentIndex] as any;
|
|
833
|
+
if (!block) return;
|
|
834
|
+
|
|
835
|
+
if (payload.delta?.type === "text_delta" && block.type === "text") {
|
|
836
|
+
block.text += String(payload.delta.text || "");
|
|
837
|
+
stream.push({ type: "text_delta", contentIndex, delta: String(payload.delta.text || ""), partial: output });
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
if (payload.delta?.type === "thinking_delta" && block.type === "thinking") {
|
|
841
|
+
block.thinking += String(payload.delta.thinking || "");
|
|
842
|
+
stream.push({ type: "thinking_delta", contentIndex, delta: String(payload.delta.thinking || ""), partial: output });
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
if (payload.delta?.type === "input_json_delta" && block.type === "toolCall") {
|
|
846
|
+
block.partialJson += String(payload.delta.partial_json || "");
|
|
847
|
+
try {
|
|
848
|
+
block.arguments = JSON.parse(block.partialJson);
|
|
849
|
+
} catch {
|
|
850
|
+
// partial json is expected during streaming
|
|
851
|
+
}
|
|
852
|
+
stream.push({ type: "toolcall_delta", contentIndex, delta: String(payload.delta.partial_json || ""), partial: output });
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
if (payload.delta?.type === "signature_delta" && block.type === "thinking") {
|
|
856
|
+
block.thinkingSignature = `${block.thinkingSignature || ""}${String(payload.delta.signature || "")}`;
|
|
857
|
+
}
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
if (payload.type === "content_block_stop") {
|
|
862
|
+
const contentIndex = blockIndexByEventIndex.get(payload.index);
|
|
863
|
+
if (contentIndex == null) return;
|
|
864
|
+
const block = output.content[contentIndex] as any;
|
|
865
|
+
if (!block) return;
|
|
866
|
+
|
|
867
|
+
delete block.eventIndex;
|
|
868
|
+
blockIndexByEventIndex.delete(payload.index);
|
|
869
|
+
|
|
870
|
+
if (block.type === "text") {
|
|
871
|
+
stream.push({ type: "text_end", contentIndex, content: block.text, partial: output });
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
if (block.type === "thinking") {
|
|
875
|
+
stream.push({ type: "thinking_end", contentIndex, content: block.thinking, partial: output });
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
if (block.type === "toolCall") {
|
|
879
|
+
if (block.partialJson) {
|
|
880
|
+
try {
|
|
881
|
+
block.arguments = JSON.parse(block.partialJson);
|
|
882
|
+
} catch {
|
|
883
|
+
block.arguments = block.arguments || {};
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
delete block.partialJson;
|
|
887
|
+
stream.push({ type: "toolcall_end", contentIndex, toolCall: block, partial: output });
|
|
888
|
+
}
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
if (payload.type === "message_delta") {
|
|
893
|
+
if (payload.delta?.stop_reason) output.stopReason = mapStopReason(payload.delta.stop_reason);
|
|
894
|
+
updateUsageFromAnthropic(output, payload.usage || {}, model);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
async function tryStreamAnyRouterCc(url: string, body: Json, apiKey: string, model: Model<Api>, output: AssistantMessage, stream: AssistantMessageEventStream, sessionId: string, signal?: AbortSignal) {
|
|
899
|
+
const requestBody = { ...body, stream: true };
|
|
900
|
+
const bodyText = JSON.stringify(requestBody);
|
|
901
|
+
const maxRetries = Math.max(0, Number(process.env.PI_ANYROUTER_CC_MAX_RETRIES || "10") || 0);
|
|
902
|
+
let response: Response | undefined;
|
|
903
|
+
|
|
904
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
905
|
+
// Real Claude Code keeps this at zero across its application-level retries.
|
|
906
|
+
const headers = getClaudeCodeHeaders(apiKey, 0, sessionId);
|
|
907
|
+
if (attempt === 0) {
|
|
908
|
+
writeDebugFile("request", model.id, undefined, {
|
|
909
|
+
url,
|
|
910
|
+
headers: redactHeaders(headers),
|
|
911
|
+
body: requestBody,
|
|
912
|
+
transport: "sse",
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
try {
|
|
917
|
+
response = await fetchWithProxy(url, {
|
|
918
|
+
method: "POST",
|
|
919
|
+
signal,
|
|
920
|
+
headers,
|
|
921
|
+
body: bodyText,
|
|
922
|
+
});
|
|
923
|
+
} catch (error) {
|
|
924
|
+
if (attempt < maxRetries && !signal?.aborted) {
|
|
925
|
+
await delay(getRetryDelayMs(attempt));
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
928
|
+
throw error;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
const contentType = response.headers.get("content-type") || "";
|
|
932
|
+
if (response.ok && contentType.includes("text/event-stream")) break;
|
|
933
|
+
|
|
934
|
+
const raw = await response.text();
|
|
935
|
+
const parsed = tryParseJson(raw) || { raw };
|
|
936
|
+
const requestId = extractRequestId(parsed, response.headers);
|
|
937
|
+
writeDebugFile(response.ok ? "response" : "error", model.id, requestId, {
|
|
938
|
+
status: response.status,
|
|
939
|
+
statusText: response.statusText,
|
|
940
|
+
requestId,
|
|
941
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
942
|
+
body: parsed,
|
|
943
|
+
raw,
|
|
944
|
+
transport: "sse",
|
|
945
|
+
retryAttempt: attempt,
|
|
946
|
+
maxRetries,
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
if (!response.ok && attempt < maxRetries && isRetryableStatus(response.status)) {
|
|
950
|
+
// Push visible retry feedback so pi's UI shows activity instead of a frozen "working" status.
|
|
951
|
+
const retryBlockIndex = output.content.length;
|
|
952
|
+
const retryText = `⏳ ${response.status} — retrying (${attempt + 1}/${maxRetries})…`;
|
|
953
|
+
output.content.push({ type: "text", text: retryText } as any);
|
|
954
|
+
stream.push({ type: "text_start", contentIndex: retryBlockIndex, partial: output });
|
|
955
|
+
stream.push({ type: "text_delta", contentIndex: retryBlockIndex, delta: retryText, partial: output });
|
|
956
|
+
stream.push({ type: "text_end", contentIndex: retryBlockIndex, content: retryText, partial: output });
|
|
957
|
+
await delay(getRetryDelayMs(attempt, parseRetryAfterMs(response.headers.get("retry-after"))));
|
|
958
|
+
response = undefined;
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
if (response.ok) throw new Error(`stream response was not SSE (content-type=${contentType || "<missing>"})`);
|
|
962
|
+
throw new Error(raw || `HTTP ${response.status}`);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
if (!response?.body) throw new Error("stream response body missing");
|
|
966
|
+
|
|
967
|
+
const blockIndexByEventIndex = new Map<number, number>();
|
|
968
|
+
const reader = response.body.getReader();
|
|
969
|
+
const decoder = new TextDecoder();
|
|
970
|
+
let buffer = "";
|
|
971
|
+
|
|
972
|
+
while (true) {
|
|
973
|
+
const { value, done } = await reader.read();
|
|
974
|
+
buffer += decoder.decode(value || new Uint8Array(), { stream: !done });
|
|
975
|
+
|
|
976
|
+
let parsedChunk = nextSseChunk(buffer);
|
|
977
|
+
while (parsedChunk) {
|
|
978
|
+
buffer = parsedChunk.rest;
|
|
979
|
+
const event = parseSseEvent(parsedChunk.chunk);
|
|
980
|
+
if (event.data) {
|
|
981
|
+
const payload = tryParseJson(event.data);
|
|
982
|
+
if (!payload && event.data !== "[DONE]") throw new Error(`invalid SSE payload: ${event.data.slice(0, 200)}`);
|
|
983
|
+
if (payload) applySsePayloadEvent(payload, output, stream, model, blockIndexByEventIndex);
|
|
984
|
+
}
|
|
985
|
+
parsedChunk = nextSseChunk(buffer);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
if (done) break;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
const tail = buffer.trim();
|
|
992
|
+
if (tail) {
|
|
993
|
+
const event = parseSseEvent(tail);
|
|
994
|
+
if (event.data && event.data !== "[DONE]") {
|
|
995
|
+
const payload = tryParseJson(event.data);
|
|
996
|
+
if (!payload) throw new Error(`invalid SSE payload: ${event.data.slice(0, 200)}`);
|
|
997
|
+
applySsePayloadEvent(payload, output, stream, model, blockIndexByEventIndex);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
writeDebugFile("response", model.id, response.headers.get("x-oneapi-request-id") || undefined, {
|
|
1002
|
+
status: response.status,
|
|
1003
|
+
statusText: response.statusText,
|
|
1004
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
1005
|
+
body: {
|
|
1006
|
+
responseId: output.responseId,
|
|
1007
|
+
stopReason: output.stopReason,
|
|
1008
|
+
usage: output.usage,
|
|
1009
|
+
contentBlocks: output.content.length,
|
|
1010
|
+
},
|
|
1011
|
+
transport: "sse",
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
async function postJson(url: string, body: Json, apiKey: string, modelId: string, sessionId: string, signal?: AbortSignal) {
|
|
1016
|
+
const maxRetries = Math.max(0, Number(process.env.PI_ANYROUTER_CC_MAX_RETRIES || "10") || 0);
|
|
1017
|
+
const bodyText = JSON.stringify(body);
|
|
1018
|
+
let lastErrorText = "";
|
|
1019
|
+
|
|
1020
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1021
|
+
const headers = getClaudeCodeHeaders(apiKey, attempt, sessionId);
|
|
1022
|
+
if (attempt === 0) {
|
|
1023
|
+
writeDebugFile("request", modelId, undefined, {
|
|
1024
|
+
url,
|
|
1025
|
+
headers: redactHeaders(headers),
|
|
1026
|
+
body,
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
let response: Response;
|
|
1031
|
+
try {
|
|
1032
|
+
response = await fetchWithProxy(url, {
|
|
1033
|
+
method: "POST",
|
|
1034
|
+
signal,
|
|
1035
|
+
headers,
|
|
1036
|
+
body: bodyText,
|
|
1037
|
+
});
|
|
1038
|
+
} catch (error) {
|
|
1039
|
+
if (attempt < maxRetries) {
|
|
1040
|
+
await delay(getRetryDelayMs(attempt));
|
|
1041
|
+
continue;
|
|
1042
|
+
}
|
|
1043
|
+
throw error;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
const text = await response.text();
|
|
1047
|
+
lastErrorText = text;
|
|
1048
|
+
let parsed: any = {};
|
|
1049
|
+
try {
|
|
1050
|
+
parsed = text ? JSON.parse(text) : {};
|
|
1051
|
+
} catch {
|
|
1052
|
+
parsed = { raw: text };
|
|
1053
|
+
}
|
|
1054
|
+
const requestId = parsed?.error?.message?.match(/request id:\s*([^\)]+)/i)?.[1]
|
|
1055
|
+
|| response.headers.get("x-oneapi-request-id")
|
|
1056
|
+
|| undefined;
|
|
1057
|
+
|
|
1058
|
+
writeDebugFile(response.ok ? "response" : "error", modelId, requestId, {
|
|
1059
|
+
status: response.status,
|
|
1060
|
+
statusText: response.statusText,
|
|
1061
|
+
requestId,
|
|
1062
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
1063
|
+
body: parsed,
|
|
1064
|
+
raw: text,
|
|
1065
|
+
retryAttempt: attempt,
|
|
1066
|
+
maxRetries,
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
if (response.ok) return parsed;
|
|
1070
|
+
if (attempt < maxRetries && isRetryableStatus(response.status)) {
|
|
1071
|
+
await delay(getRetryDelayMs(attempt, parseRetryAfterMs(response.headers.get("retry-after"))));
|
|
1072
|
+
continue;
|
|
1073
|
+
}
|
|
1074
|
+
throw new Error(text || `HTTP ${response.status}`);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
throw new Error(lastErrorText || "HTTP request failed after retries");
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
function streamAnyRouterCc(model: Model<Api>, context: Context, options?: SimpleStreamOptions): AssistantMessageEventStream {
|
|
1081
|
+
const stream = createAssistantMessageEventStream();
|
|
1082
|
+
(async () => {
|
|
1083
|
+
const output: AssistantMessage = {
|
|
1084
|
+
role: "assistant",
|
|
1085
|
+
content: [],
|
|
1086
|
+
api: model.api,
|
|
1087
|
+
provider: model.provider,
|
|
1088
|
+
model: model.id,
|
|
1089
|
+
usage: createEmptyUsage(),
|
|
1090
|
+
stopReason: "stop",
|
|
1091
|
+
timestamp: Date.now(),
|
|
1092
|
+
};
|
|
1093
|
+
|
|
1094
|
+
try {
|
|
1095
|
+
const source = loadSourceProvider();
|
|
1096
|
+
const sessionId = randomUUID();
|
|
1097
|
+
|
|
1098
|
+
const configuredModel = source.models.find((item) => item.id === model.id);
|
|
1099
|
+
if (isCodexModel(model.id, configuredModel?.api)) {
|
|
1100
|
+
const turnId = randomUUID();
|
|
1101
|
+
const metadata = createCodexMetadata(sessionId, turnId);
|
|
1102
|
+
const codexBody = buildCodexRequestBody(model, context, options, sessionId, metadata);
|
|
1103
|
+
stream.push({ type: "start", partial: output });
|
|
1104
|
+
await tryStreamAnyRouterCodex(
|
|
1105
|
+
getCodexResponsesUrl(source.baseUrl),
|
|
1106
|
+
codexBody,
|
|
1107
|
+
source.apiKey,
|
|
1108
|
+
model,
|
|
1109
|
+
output,
|
|
1110
|
+
stream,
|
|
1111
|
+
sessionId,
|
|
1112
|
+
metadata,
|
|
1113
|
+
options?.signal,
|
|
1114
|
+
);
|
|
1115
|
+
if (options?.signal?.aborted) throw new Error("Request was aborted");
|
|
1116
|
+
stream.push({ type: "done", reason: output.stopReason as "stop" | "length" | "toolUse", message: output });
|
|
1117
|
+
stream.end();
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
const url = `${source.baseUrl.replace(/\/$/, "")}/v1/messages?beta=true`;
|
|
1122
|
+
const requestBody: Json = {
|
|
1123
|
+
model: model.id,
|
|
1124
|
+
messages: convertMessages(context.messages),
|
|
1125
|
+
max_tokens: options?.maxTokens || model.maxTokens || 32000,
|
|
1126
|
+
stream: false,
|
|
1127
|
+
metadata: createClaudeCodeMetadata(sessionId),
|
|
1128
|
+
system: createClaudeCodeSystem(context.systemPrompt || "You are an expert coding assistant operating inside pi."),
|
|
1129
|
+
context_management: {
|
|
1130
|
+
edits: [{ type: "clear_thinking_20251015", keep: "all" }],
|
|
1131
|
+
},
|
|
1132
|
+
};
|
|
1133
|
+
if (context.tools?.length) requestBody.tools = convertTools(context.tools);
|
|
1134
|
+
if (options?.reasoning && model.reasoning) {
|
|
1135
|
+
requestBody.thinking = { type: "adaptive", display: "omitted" };
|
|
1136
|
+
requestBody.output_config = { effort: mapReasoningEffort(options.reasoning) };
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
stream.push({ type: "start", partial: output });
|
|
1140
|
+
|
|
1141
|
+
const streamMode = getStreamMode();
|
|
1142
|
+
if (streamMode !== "off") {
|
|
1143
|
+
try {
|
|
1144
|
+
await tryStreamAnyRouterCc(url, requestBody, source.apiKey, model, output, stream, sessionId, options?.signal);
|
|
1145
|
+
if (options?.signal?.aborted) throw new Error("Request was aborted");
|
|
1146
|
+
stream.push({ type: "done", reason: output.stopReason as "stop" | "length" | "toolUse", message: output });
|
|
1147
|
+
stream.end();
|
|
1148
|
+
return;
|
|
1149
|
+
} catch (streamError) {
|
|
1150
|
+
if (streamMode === "force" || output.content.length > 0) {
|
|
1151
|
+
// All retries exhausted. Push a text block with the error so pi
|
|
1152
|
+
// shows it, then end with done/stop to prevent pi from auto-retrying
|
|
1153
|
+
// the entire provider stream.
|
|
1154
|
+
const errText = `[anyrouter] ${streamError instanceof Error ? streamError.message : String(streamError)}`;
|
|
1155
|
+
const contentIndex = output.content.length;
|
|
1156
|
+
output.content.push({ type: "text", text: errText } as any);
|
|
1157
|
+
output.stopReason = "stop";
|
|
1158
|
+
output.errorMessage = errText;
|
|
1159
|
+
stream.push({ type: "text_start", contentIndex, partial: output });
|
|
1160
|
+
stream.push({ type: "text_delta", contentIndex, delta: errText, partial: output });
|
|
1161
|
+
stream.push({ type: "text_end", contentIndex, content: errText, partial: output });
|
|
1162
|
+
stream.push({ type: "done", reason: "stop", message: output });
|
|
1163
|
+
stream.end();
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
writeDebugFile("error", model.id, undefined, {
|
|
1167
|
+
phase: "stream-fallback",
|
|
1168
|
+
errorMessage: streamError instanceof Error ? streamError.message : String(streamError),
|
|
1169
|
+
});
|
|
1170
|
+
resetOutputState(output);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
const response = await postJson(url, requestBody, source.apiKey, model.id, sessionId, options?.signal);
|
|
1175
|
+
applyJsonResponseToOutput(response, output, stream, model);
|
|
1176
|
+
stream.push({ type: "done", reason: output.stopReason as "stop" | "length" | "toolUse", message: output });
|
|
1177
|
+
stream.end();
|
|
1178
|
+
} catch (error) {
|
|
1179
|
+
output.stopReason = options?.signal?.aborted ? "aborted" : "error";
|
|
1180
|
+
output.errorMessage = error instanceof Error ? `[anyrouter] ${error.message}` : String(error);
|
|
1181
|
+
writeDebugFile("error", model.id, undefined, {
|
|
1182
|
+
stopReason: output.stopReason,
|
|
1183
|
+
errorMessage: output.errorMessage,
|
|
1184
|
+
});
|
|
1185
|
+
stream.push({ type: "error", reason: output.stopReason, error: output });
|
|
1186
|
+
stream.end();
|
|
1187
|
+
}
|
|
1188
|
+
})();
|
|
1189
|
+
return stream;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
export default function (pi: ExtensionAPI) {
|
|
1193
|
+
try {
|
|
1194
|
+
const source = loadSourceProvider();
|
|
1195
|
+
pi.registerProvider(PROVIDER_NAME, {
|
|
1196
|
+
baseUrl: source.baseUrl,
|
|
1197
|
+
apiKey: source.apiKey,
|
|
1198
|
+
api: API_ID,
|
|
1199
|
+
models: source.models.map((model) => ({
|
|
1200
|
+
id: model.id,
|
|
1201
|
+
name: model.name ? `${model.name} (AnyRouter)` : `${model.id} (AnyRouter)`,
|
|
1202
|
+
api: API_ID,
|
|
1203
|
+
reasoning: model.reasoning ?? true,
|
|
1204
|
+
input: model.input ?? ["text"],
|
|
1205
|
+
cost: {
|
|
1206
|
+
input: model.cost?.input ?? 0,
|
|
1207
|
+
output: model.cost?.output ?? 0,
|
|
1208
|
+
cacheRead: model.cost?.cacheRead ?? 0,
|
|
1209
|
+
cacheWrite: model.cost?.cacheWrite ?? 0,
|
|
1210
|
+
},
|
|
1211
|
+
contextWindow: model.contextWindow ?? 200000,
|
|
1212
|
+
maxTokens: model.maxTokens ?? 32000,
|
|
1213
|
+
})),
|
|
1214
|
+
streamSimple: streamAnyRouterCc,
|
|
1215
|
+
});
|
|
1216
|
+
} catch (error) {
|
|
1217
|
+
console.error(`[anyrouter] Failed to register provider: ${error instanceof Error ? error.message : String(error)}`);
|
|
1218
|
+
console.error(`[anyrouter] Config path: ${CONFIG_PATH}`);
|
|
1219
|
+
console.error(`[anyrouter] You can override with PI_ANYROUTER_CC_CONFIG, PI_ANYROUTER_CC_BASE_URL, PI_ANYROUTER_CC_API_KEY`);
|
|
1220
|
+
}
|
|
1221
|
+
}
|