@lll9p/pi-anyrouter 0.3.2 → 0.4.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 +5 -5
- package/package.json +15 -4
- package/src/claude-code.ts +531 -0
- package/src/codex.ts +376 -0
- package/src/config.ts +36 -0
- package/src/http.ts +113 -0
- package/src/index.ts +166 -0
- package/src/types.ts +65 -0
- package/src/utils.ts +114 -0
- package/index.ts +0 -1221
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ This package selects an adapter by model family:
|
|
|
50
50
|
Clone into a normal directory:
|
|
51
51
|
|
|
52
52
|
```bash
|
|
53
|
-
git clone https://github.com/
|
|
53
|
+
git clone https://github.com/lll9p/pi-anyrouter.git
|
|
54
54
|
cd pi-anyrouter
|
|
55
55
|
```
|
|
56
56
|
|
|
@@ -71,13 +71,13 @@ pi install /absolute/path/to/pi-anyrouter
|
|
|
71
71
|
Once pushed to GitHub, install with:
|
|
72
72
|
|
|
73
73
|
```bash
|
|
74
|
-
pi install git:github.com/
|
|
74
|
+
pi install git:github.com/lll9p/pi-anyrouter
|
|
75
75
|
```
|
|
76
76
|
|
|
77
77
|
Or pin a ref/tag:
|
|
78
78
|
|
|
79
79
|
```bash
|
|
80
|
-
pi install git:github.com/
|
|
80
|
+
pi install git:github.com/lll9p/pi-anyrouter@<tag>
|
|
81
81
|
```
|
|
82
82
|
|
|
83
83
|
## Option C: manual extension placement
|
|
@@ -204,7 +204,7 @@ This repo is already structured as a pi package through `package.json`:
|
|
|
204
204
|
That means users can install it with:
|
|
205
205
|
|
|
206
206
|
```bash
|
|
207
|
-
pi install git:github.com/
|
|
207
|
+
pi install git:github.com/lll9p/pi-anyrouter
|
|
208
208
|
```
|
|
209
209
|
|
|
210
210
|
## Publish checklist
|
|
@@ -216,7 +216,7 @@ git init
|
|
|
216
216
|
git add .
|
|
217
217
|
git commit -m "feat: add AnyRouter Claude and Codex adapters"
|
|
218
218
|
git branch -M main
|
|
219
|
-
git remote add origin git@github.com:
|
|
219
|
+
git remote add origin git@github.com:lll9p/pi-anyrouter.git
|
|
220
220
|
git push -u origin main
|
|
221
221
|
```
|
|
222
222
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lll9p/pi-anyrouter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Unofficial pi provider extension for AnyRouter Claude Code and Codex Responses Lite routes",
|
|
@@ -24,19 +24,24 @@
|
|
|
24
24
|
"url": "https://github.com/lll9p/pi-anyrouter/issues"
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
|
27
|
-
"
|
|
27
|
+
"src/",
|
|
28
28
|
"README.md"
|
|
29
29
|
],
|
|
30
30
|
"pi": {
|
|
31
31
|
"extensions": [
|
|
32
|
-
"./index.ts"
|
|
32
|
+
"./src/index.ts"
|
|
33
33
|
]
|
|
34
34
|
},
|
|
35
35
|
"scripts": {
|
|
36
|
+
"typecheck": "tsc --noEmit",
|
|
37
|
+
"lint": "biome check src/",
|
|
38
|
+
"lint:fix": "biome check --write src/",
|
|
39
|
+
"format": "biome format --write src/",
|
|
40
|
+
"check": "tsc --noEmit && biome check src/",
|
|
36
41
|
"pack:dry-run": "npm pack --dry-run"
|
|
37
42
|
},
|
|
38
43
|
"dependencies": {
|
|
39
|
-
"undici": "^8.
|
|
44
|
+
"undici": "^8.10.2"
|
|
40
45
|
},
|
|
41
46
|
"peerDependencies": {
|
|
42
47
|
"@earendil-works/pi-ai": "*",
|
|
@@ -49,5 +54,11 @@
|
|
|
49
54
|
"@earendil-works/pi-coding-agent": {
|
|
50
55
|
"optional": true
|
|
51
56
|
}
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@biomejs/biome": "^2.5.12",
|
|
60
|
+
"@earendil-works/pi-ai": "^0.85.1",
|
|
61
|
+
"@earendil-works/pi-coding-agent": "^0.85.1",
|
|
62
|
+
"@types/node": "^26.5.0"
|
|
52
63
|
}
|
|
53
64
|
}
|
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Api,
|
|
3
|
+
AssistantMessage,
|
|
4
|
+
AssistantMessageEventStream,
|
|
5
|
+
ImageContent,
|
|
6
|
+
Message,
|
|
7
|
+
Model,
|
|
8
|
+
SimpleStreamOptions,
|
|
9
|
+
TextContent,
|
|
10
|
+
ThinkingContent,
|
|
11
|
+
Tool,
|
|
12
|
+
ToolResultMessage,
|
|
13
|
+
} from "@earendil-works/pi-ai";
|
|
14
|
+
import {
|
|
15
|
+
delay,
|
|
16
|
+
fetchWithProxy,
|
|
17
|
+
getRetryDelayMs,
|
|
18
|
+
isRetryableStatus,
|
|
19
|
+
nextSseChunk,
|
|
20
|
+
parseRetryAfterMs,
|
|
21
|
+
parseSseEvent,
|
|
22
|
+
redactHeaders,
|
|
23
|
+
writeDebugFile,
|
|
24
|
+
} from "./http.js";
|
|
25
|
+
import {
|
|
26
|
+
ANTHROPIC_BETA,
|
|
27
|
+
CLAUDE_CODE_VERSION,
|
|
28
|
+
CLAUDE_CODE_VERSION_BUILD,
|
|
29
|
+
CLAUDE_DEVICE_ID,
|
|
30
|
+
type Json,
|
|
31
|
+
STAINLESS_ARCH,
|
|
32
|
+
STAINLESS_OS,
|
|
33
|
+
STAINLESS_PACKAGE_VERSION,
|
|
34
|
+
STAINLESS_RUNTIME,
|
|
35
|
+
STAINLESS_RUNTIME_VERSION,
|
|
36
|
+
} from "./types.js";
|
|
37
|
+
import { extractRequestId, fromClaudeCodeName, mapStopReason, sanitizeText, toClaudeCodeName, tryParseJson, updateUsageFromAnthropic } from "./utils.js";
|
|
38
|
+
|
|
39
|
+
// ── Message / tool conversion ───────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
function convertContentBlocks(content: (TextContent | ImageContent)[]) {
|
|
42
|
+
const hasImages = content.some((c) => c.type === "image");
|
|
43
|
+
if (!hasImages) return sanitizeText(content.map((c) => (c as TextContent).text).join("\n"));
|
|
44
|
+
|
|
45
|
+
const blocks = content.map((block) => {
|
|
46
|
+
if (block.type === "text") return { type: "text", text: sanitizeText(block.text) };
|
|
47
|
+
return { type: "image", source: { type: "base64", media_type: block.mimeType, data: block.data } };
|
|
48
|
+
});
|
|
49
|
+
if (!blocks.some((b) => b.type === "text")) blocks.unshift({ type: "text", text: "(see attached image)" });
|
|
50
|
+
return blocks;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function convertMessages(messages: Message[]) {
|
|
54
|
+
const params: any[] = [];
|
|
55
|
+
for (let i = 0; i < messages.length; i++) {
|
|
56
|
+
const msg = messages[i];
|
|
57
|
+
if (msg.role === "user") {
|
|
58
|
+
if (typeof msg.content === "string") {
|
|
59
|
+
const text = sanitizeText(msg.content);
|
|
60
|
+
if (text.trim()) params.push({ role: "user", content: [{ type: "text", text }] });
|
|
61
|
+
} else {
|
|
62
|
+
const blocks = msg.content.map((item) =>
|
|
63
|
+
item.type === "text"
|
|
64
|
+
? { type: "text", text: sanitizeText(item.text) }
|
|
65
|
+
: { type: "image", source: { type: "base64", media_type: item.mimeType, data: item.data } },
|
|
66
|
+
);
|
|
67
|
+
if (blocks.length > 0) params.push({ role: "user", content: blocks });
|
|
68
|
+
}
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (msg.role === "assistant") {
|
|
73
|
+
const blocks: any[] = [];
|
|
74
|
+
for (const block of msg.content) {
|
|
75
|
+
if (block.type === "text" && block.text.trim()) blocks.push({ type: "text", text: sanitizeText(block.text) });
|
|
76
|
+
else if (block.type === "thinking" && block.thinking.trim()) {
|
|
77
|
+
if ((block as ThinkingContent).thinkingSignature) {
|
|
78
|
+
blocks.push({ type: "thinking", thinking: sanitizeText(block.thinking), signature: (block as ThinkingContent).thinkingSignature });
|
|
79
|
+
} else {
|
|
80
|
+
blocks.push({ type: "text", text: sanitizeText(block.thinking) });
|
|
81
|
+
}
|
|
82
|
+
} else if (block.type === "toolCall") {
|
|
83
|
+
blocks.push({ type: "tool_use", id: block.id, name: toClaudeCodeName(block.name), input: block.arguments });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (blocks.length > 0) params.push({ role: "assistant", content: blocks });
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (msg.role === "toolResult") {
|
|
91
|
+
const toolResults: any[] = [];
|
|
92
|
+
const pushToolResult = (toolMsg: ToolResultMessage) => {
|
|
93
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolMsg.toolCallId, content: convertContentBlocks(toolMsg.content), is_error: toolMsg.isError });
|
|
94
|
+
};
|
|
95
|
+
pushToolResult(msg as ToolResultMessage);
|
|
96
|
+
let j = i + 1;
|
|
97
|
+
while (j < messages.length && messages[j].role === "toolResult") {
|
|
98
|
+
pushToolResult(messages[j] as ToolResultMessage);
|
|
99
|
+
j++;
|
|
100
|
+
}
|
|
101
|
+
i = j - 1;
|
|
102
|
+
params.push({ role: "user", content: toolResults });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (params.length > 0) {
|
|
107
|
+
const last = params[params.length - 1];
|
|
108
|
+
if (last.role === "user" && Array.isArray(last.content) && last.content.length > 0) {
|
|
109
|
+
last.content[last.content.length - 1].cache_control = { type: "ephemeral" };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return params;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function convertTools(tools: Tool[]) {
|
|
116
|
+
return tools.map((tool) => ({
|
|
117
|
+
name: toClaudeCodeName(tool.name),
|
|
118
|
+
description: tool.description,
|
|
119
|
+
input_schema: {
|
|
120
|
+
type: "object",
|
|
121
|
+
properties: (tool.parameters as any).properties || {},
|
|
122
|
+
required: (tool.parameters as any).required || [],
|
|
123
|
+
},
|
|
124
|
+
}));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── Headers / metadata ──────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
export function getClaudeCodeHeaders(apiKey: string, retryCount = 0, sessionId: string) {
|
|
130
|
+
return {
|
|
131
|
+
"content-type": "application/json",
|
|
132
|
+
accept: "application/json",
|
|
133
|
+
authorization: `Bearer ${apiKey}`,
|
|
134
|
+
"anthropic-version": "2023-06-01",
|
|
135
|
+
"anthropic-dangerous-direct-browser-access": "true",
|
|
136
|
+
"anthropic-beta": ANTHROPIC_BETA,
|
|
137
|
+
"user-agent": `claude-cli/${CLAUDE_CODE_VERSION} (external, sdk-cli)`,
|
|
138
|
+
"x-app": "cli",
|
|
139
|
+
"x-claude-code-session-id": sessionId,
|
|
140
|
+
"x-stainless-retry-count": String(retryCount),
|
|
141
|
+
"x-stainless-timeout": "600",
|
|
142
|
+
"x-stainless-lang": "js",
|
|
143
|
+
"x-stainless-package-version": STAINLESS_PACKAGE_VERSION,
|
|
144
|
+
"x-stainless-os": STAINLESS_OS,
|
|
145
|
+
"x-stainless-arch": STAINLESS_ARCH,
|
|
146
|
+
"x-stainless-runtime": STAINLESS_RUNTIME,
|
|
147
|
+
"x-stainless-runtime-version": STAINLESS_RUNTIME_VERSION,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function createClaudeCodeMetadata(sessionId: string) {
|
|
152
|
+
return {
|
|
153
|
+
user_id: JSON.stringify({
|
|
154
|
+
device_id: CLAUDE_DEVICE_ID,
|
|
155
|
+
account_uuid: "",
|
|
156
|
+
session_id: sessionId,
|
|
157
|
+
}),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function createClaudeCodeSystem(systemPrompt: string) {
|
|
162
|
+
return [
|
|
163
|
+
{ type: "text", text: `x-anthropic-billing-header: cc_version=${CLAUDE_CODE_VERSION_BUILD}; cc_entrypoint=sdk-cli;` },
|
|
164
|
+
{ type: "text", text: "You are a Claude agent, built on Anthropic's Claude Agent SDK.", cache_control: { type: "ephemeral" } },
|
|
165
|
+
{ type: "text", text: sanitizeText(systemPrompt), cache_control: { type: "ephemeral" } },
|
|
166
|
+
];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── SSE payload processing ──────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
export function applyJsonResponseToOutput(response: any, output: AssistantMessage, stream: AssistantMessageEventStream, model: Model<Api>) {
|
|
172
|
+
updateUsageFromAnthropic(output, response?.usage || {}, model);
|
|
173
|
+
output.stopReason = mapStopReason(response?.stop_reason || "end_turn");
|
|
174
|
+
|
|
175
|
+
const content = Array.isArray(response?.content) ? response.content : [];
|
|
176
|
+
for (const block of content) {
|
|
177
|
+
if (block?.type === "text") {
|
|
178
|
+
output.content.push({ type: "text", text: "" });
|
|
179
|
+
const contentIndex = output.content.length - 1;
|
|
180
|
+
stream.push({ type: "text_start", contentIndex, partial: output });
|
|
181
|
+
const text = String(block.text || "");
|
|
182
|
+
(output.content[contentIndex] as any).text = text;
|
|
183
|
+
if (text) stream.push({ type: "text_delta", contentIndex, delta: text, partial: output });
|
|
184
|
+
stream.push({ type: "text_end", contentIndex, content: text, partial: output });
|
|
185
|
+
} else if (block?.type === "thinking") {
|
|
186
|
+
output.content.push({ type: "thinking", thinking: String(block.thinking || ""), thinkingSignature: block.signature || "" } as any);
|
|
187
|
+
const contentIndex = output.content.length - 1;
|
|
188
|
+
stream.push({ type: "thinking_start", contentIndex, partial: output });
|
|
189
|
+
if (block.thinking) stream.push({ type: "thinking_delta", contentIndex, delta: String(block.thinking), partial: output });
|
|
190
|
+
stream.push({ type: "thinking_end", contentIndex, content: String(block.thinking || ""), partial: output });
|
|
191
|
+
} else if (block?.type === "tool_use") {
|
|
192
|
+
const toolCall = { type: "toolCall" as const, id: block.id, name: fromClaudeCodeName(block.name), arguments: block.input || {} };
|
|
193
|
+
output.content.push(toolCall as any);
|
|
194
|
+
const contentIndex = output.content.length - 1;
|
|
195
|
+
stream.push({ type: "toolcall_start", contentIndex, partial: output });
|
|
196
|
+
stream.push({ type: "toolcall_delta", contentIndex, delta: JSON.stringify(toolCall.arguments), partial: output });
|
|
197
|
+
stream.push({ type: "toolcall_end", contentIndex, toolCall, partial: output });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function applySsePayloadEvent(
|
|
203
|
+
payload: any,
|
|
204
|
+
output: AssistantMessage,
|
|
205
|
+
stream: AssistantMessageEventStream,
|
|
206
|
+
model: Model<Api>,
|
|
207
|
+
blockIndexByEventIndex: Map<number, number>,
|
|
208
|
+
) {
|
|
209
|
+
if (!payload?.type || payload.type === "ping" || payload.type === "message_stop") return;
|
|
210
|
+
|
|
211
|
+
if (payload.type === "error") {
|
|
212
|
+
const errorText = payload?.error?.message || payload?.error || payload?.message || JSON.stringify(payload);
|
|
213
|
+
throw new Error(String(errorText));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (payload.type === "message_start") {
|
|
217
|
+
output.responseId = payload.message?.id || output.responseId;
|
|
218
|
+
updateUsageFromAnthropic(output, payload.message?.usage || {}, model);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (payload.type === "content_block_start") {
|
|
223
|
+
const block = payload.content_block;
|
|
224
|
+
if (block?.type === "text") {
|
|
225
|
+
output.content.push({ type: "text", text: "", eventIndex: payload.index } as any);
|
|
226
|
+
const contentIndex = output.content.length - 1;
|
|
227
|
+
blockIndexByEventIndex.set(payload.index, contentIndex);
|
|
228
|
+
stream.push({ type: "text_start", contentIndex, partial: output });
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (block?.type === "thinking" || block?.type === "redacted_thinking") {
|
|
232
|
+
output.content.push({
|
|
233
|
+
type: "thinking",
|
|
234
|
+
thinking: block.type === "redacted_thinking" ? "[Reasoning redacted]" : "",
|
|
235
|
+
thinkingSignature: block.type === "redacted_thinking" ? String(block.data || "") : "",
|
|
236
|
+
redacted: block.type === "redacted_thinking" ? true : undefined,
|
|
237
|
+
eventIndex: payload.index,
|
|
238
|
+
} as any);
|
|
239
|
+
const contentIndex = output.content.length - 1;
|
|
240
|
+
blockIndexByEventIndex.set(payload.index, contentIndex);
|
|
241
|
+
stream.push({ type: "thinking_start", contentIndex, partial: output });
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (block?.type === "tool_use") {
|
|
245
|
+
const toolCall = {
|
|
246
|
+
type: "toolCall" as const,
|
|
247
|
+
id: block.id,
|
|
248
|
+
name: fromClaudeCodeName(block.name),
|
|
249
|
+
arguments: (block.input as Json) || {},
|
|
250
|
+
partialJson: "",
|
|
251
|
+
eventIndex: payload.index,
|
|
252
|
+
};
|
|
253
|
+
output.content.push(toolCall as any);
|
|
254
|
+
const contentIndex = output.content.length - 1;
|
|
255
|
+
blockIndexByEventIndex.set(payload.index, contentIndex);
|
|
256
|
+
stream.push({ type: "toolcall_start", contentIndex, partial: output });
|
|
257
|
+
}
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (payload.type === "content_block_delta") {
|
|
262
|
+
const contentIndex = blockIndexByEventIndex.get(payload.index);
|
|
263
|
+
if (contentIndex == null) return;
|
|
264
|
+
const block = output.content[contentIndex] as any;
|
|
265
|
+
if (!block) return;
|
|
266
|
+
|
|
267
|
+
if (payload.delta?.type === "text_delta" && block.type === "text") {
|
|
268
|
+
block.text += String(payload.delta.text || "");
|
|
269
|
+
stream.push({ type: "text_delta", contentIndex, delta: String(payload.delta.text || ""), partial: output });
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (payload.delta?.type === "thinking_delta" && block.type === "thinking") {
|
|
273
|
+
block.thinking += String(payload.delta.thinking || "");
|
|
274
|
+
stream.push({ type: "thinking_delta", contentIndex, delta: String(payload.delta.thinking || ""), partial: output });
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (payload.delta?.type === "input_json_delta" && block.type === "toolCall") {
|
|
278
|
+
block.partialJson += String(payload.delta.partial_json || "");
|
|
279
|
+
try {
|
|
280
|
+
block.arguments = JSON.parse(block.partialJson);
|
|
281
|
+
} catch {
|
|
282
|
+
// partial json is expected during streaming
|
|
283
|
+
}
|
|
284
|
+
stream.push({ type: "toolcall_delta", contentIndex, delta: String(payload.delta.partial_json || ""), partial: output });
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (payload.delta?.type === "signature_delta" && block.type === "thinking") {
|
|
288
|
+
block.thinkingSignature = `${block.thinkingSignature || ""}${String(payload.delta.signature || "")}`;
|
|
289
|
+
}
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (payload.type === "content_block_stop") {
|
|
294
|
+
const contentIndex = blockIndexByEventIndex.get(payload.index);
|
|
295
|
+
if (contentIndex == null) return;
|
|
296
|
+
const block = output.content[contentIndex] as any;
|
|
297
|
+
if (!block) return;
|
|
298
|
+
|
|
299
|
+
delete block.eventIndex;
|
|
300
|
+
blockIndexByEventIndex.delete(payload.index);
|
|
301
|
+
|
|
302
|
+
if (block.type === "text") {
|
|
303
|
+
stream.push({ type: "text_end", contentIndex, content: block.text, partial: output });
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (block.type === "thinking") {
|
|
307
|
+
stream.push({ type: "thinking_end", contentIndex, content: block.thinking, partial: output });
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (block.type === "toolCall") {
|
|
311
|
+
if (block.partialJson) {
|
|
312
|
+
try {
|
|
313
|
+
block.arguments = JSON.parse(block.partialJson);
|
|
314
|
+
} catch {
|
|
315
|
+
block.arguments = block.arguments || {};
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
delete block.partialJson;
|
|
319
|
+
stream.push({ type: "toolcall_end", contentIndex, toolCall: block, partial: output });
|
|
320
|
+
}
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (payload.type === "message_delta") {
|
|
325
|
+
if (payload.delta?.stop_reason) output.stopReason = mapStopReason(payload.delta.stop_reason);
|
|
326
|
+
updateUsageFromAnthropic(output, payload.usage || {}, model);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ── Streaming request ───────────────────────────────────────────────────────
|
|
331
|
+
|
|
332
|
+
export async function tryStreamAnyRouterCc(
|
|
333
|
+
url: string,
|
|
334
|
+
body: Json,
|
|
335
|
+
apiKey: string,
|
|
336
|
+
model: Model<Api>,
|
|
337
|
+
output: AssistantMessage,
|
|
338
|
+
stream: AssistantMessageEventStream,
|
|
339
|
+
sessionId: string,
|
|
340
|
+
options?: SimpleStreamOptions,
|
|
341
|
+
) {
|
|
342
|
+
const requestBody = { ...body, stream: true };
|
|
343
|
+
const bodyText = JSON.stringify(requestBody);
|
|
344
|
+
const maxRetries = Math.max(0, Number(process.env.PI_ANYROUTER_CC_MAX_RETRIES || options?.maxRetries || "10") || 0);
|
|
345
|
+
let response: Response | undefined;
|
|
346
|
+
|
|
347
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
348
|
+
// Real Claude Code keeps this at zero across its application-level retries.
|
|
349
|
+
const headers = getClaudeCodeHeaders(apiKey, 0, sessionId);
|
|
350
|
+
if (attempt === 0) {
|
|
351
|
+
writeDebugFile("request", model.id, undefined, {
|
|
352
|
+
url,
|
|
353
|
+
headers: redactHeaders(headers),
|
|
354
|
+
body: requestBody,
|
|
355
|
+
transport: "sse",
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
response = await fetchWithProxy(url, {
|
|
361
|
+
method: "POST",
|
|
362
|
+
signal: options?.signal,
|
|
363
|
+
headers,
|
|
364
|
+
body: bodyText,
|
|
365
|
+
});
|
|
366
|
+
} catch (error) {
|
|
367
|
+
if (attempt < maxRetries && !options?.signal?.aborted) {
|
|
368
|
+
await delay(getRetryDelayMs(attempt));
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
throw error;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const contentType = response.headers.get("content-type") || "";
|
|
375
|
+
if (response.ok && contentType.includes("text/event-stream")) {
|
|
376
|
+
if (options?.onResponse) {
|
|
377
|
+
await options.onResponse({ status: response.status, headers: Object.fromEntries(response.headers.entries()) }, model);
|
|
378
|
+
}
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const raw = await response.text();
|
|
383
|
+
const parsed = tryParseJson(raw) || { raw };
|
|
384
|
+
const requestId = extractRequestId(parsed, response.headers);
|
|
385
|
+
writeDebugFile(response.ok ? "response" : "error", model.id, requestId, {
|
|
386
|
+
status: response.status,
|
|
387
|
+
statusText: response.statusText,
|
|
388
|
+
requestId,
|
|
389
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
390
|
+
body: parsed,
|
|
391
|
+
raw,
|
|
392
|
+
transport: "sse",
|
|
393
|
+
retryAttempt: attempt,
|
|
394
|
+
maxRetries,
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
if (!response.ok && attempt < maxRetries && isRetryableStatus(response.status)) {
|
|
398
|
+
// Push visible retry feedback so pi's UI shows activity instead of a frozen "working" status.
|
|
399
|
+
const retryBlockIndex = output.content.length;
|
|
400
|
+
const retryText = `⏳ ${response.status} — retrying (${attempt + 1}/${maxRetries})…`;
|
|
401
|
+
output.content.push({ type: "text", text: retryText } as any);
|
|
402
|
+
stream.push({ type: "text_start", contentIndex: retryBlockIndex, partial: output });
|
|
403
|
+
stream.push({ type: "text_delta", contentIndex: retryBlockIndex, delta: retryText, partial: output });
|
|
404
|
+
stream.push({ type: "text_end", contentIndex: retryBlockIndex, content: retryText, partial: output });
|
|
405
|
+
await delay(getRetryDelayMs(attempt, parseRetryAfterMs(response.headers.get("retry-after"))));
|
|
406
|
+
response = undefined;
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
if (response.ok) throw new Error(`stream response was not SSE (content-type=${contentType || "<missing>"})`);
|
|
410
|
+
throw new Error(raw || `HTTP ${response.status}`);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (!response?.body) throw new Error("stream response body missing");
|
|
414
|
+
|
|
415
|
+
const blockIndexByEventIndex = new Map<number, number>();
|
|
416
|
+
const reader = response.body.getReader();
|
|
417
|
+
const decoder = new TextDecoder();
|
|
418
|
+
let buffer = "";
|
|
419
|
+
|
|
420
|
+
while (true) {
|
|
421
|
+
const { value, done } = await reader.read();
|
|
422
|
+
buffer += decoder.decode(value || new Uint8Array(), { stream: !done });
|
|
423
|
+
|
|
424
|
+
let parsedChunk = nextSseChunk(buffer);
|
|
425
|
+
while (parsedChunk) {
|
|
426
|
+
buffer = parsedChunk.rest;
|
|
427
|
+
const event = parseSseEvent(parsedChunk.chunk);
|
|
428
|
+
if (event.data) {
|
|
429
|
+
const payload = tryParseJson(event.data);
|
|
430
|
+
if (!payload && event.data !== "[DONE]") throw new Error(`invalid SSE payload: ${event.data.slice(0, 200)}`);
|
|
431
|
+
if (payload) applySsePayloadEvent(payload, output, stream, model, blockIndexByEventIndex);
|
|
432
|
+
}
|
|
433
|
+
parsedChunk = nextSseChunk(buffer);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (done) break;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const tail = buffer.trim();
|
|
440
|
+
if (tail) {
|
|
441
|
+
const event = parseSseEvent(tail);
|
|
442
|
+
if (event.data && event.data !== "[DONE]") {
|
|
443
|
+
const payload = tryParseJson(event.data);
|
|
444
|
+
if (!payload) throw new Error(`invalid SSE payload: ${event.data.slice(0, 200)}`);
|
|
445
|
+
applySsePayloadEvent(payload, output, stream, model, blockIndexByEventIndex);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
writeDebugFile("response", model.id, response.headers.get("x-oneapi-request-id") || undefined, {
|
|
450
|
+
status: response.status,
|
|
451
|
+
statusText: response.statusText,
|
|
452
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
453
|
+
body: {
|
|
454
|
+
responseId: output.responseId,
|
|
455
|
+
stopReason: output.stopReason,
|
|
456
|
+
usage: output.usage,
|
|
457
|
+
contentBlocks: output.content.length,
|
|
458
|
+
},
|
|
459
|
+
transport: "sse",
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// ── JSON request ────────────────────────────────────────────────────────────
|
|
464
|
+
|
|
465
|
+
export async function postJson(url: string, body: Json, apiKey: string, modelId: string, sessionId: string, model: Model<Api>, options?: SimpleStreamOptions) {
|
|
466
|
+
const maxRetries = Math.max(0, Number(process.env.PI_ANYROUTER_CC_MAX_RETRIES || options?.maxRetries || "10") || 0);
|
|
467
|
+
const bodyText = JSON.stringify(body);
|
|
468
|
+
let lastErrorText = "";
|
|
469
|
+
|
|
470
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
471
|
+
const headers = getClaudeCodeHeaders(apiKey, attempt, sessionId);
|
|
472
|
+
if (attempt === 0) {
|
|
473
|
+
writeDebugFile("request", modelId, undefined, {
|
|
474
|
+
url,
|
|
475
|
+
headers: redactHeaders(headers),
|
|
476
|
+
body,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
let response: Response;
|
|
481
|
+
try {
|
|
482
|
+
response = await fetchWithProxy(url, {
|
|
483
|
+
method: "POST",
|
|
484
|
+
signal: options?.signal,
|
|
485
|
+
headers,
|
|
486
|
+
body: bodyText,
|
|
487
|
+
});
|
|
488
|
+
} catch (error) {
|
|
489
|
+
if (attempt < maxRetries) {
|
|
490
|
+
await delay(getRetryDelayMs(attempt));
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
throw error;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const text = await response.text();
|
|
497
|
+
lastErrorText = text;
|
|
498
|
+
let parsed: any = {};
|
|
499
|
+
try {
|
|
500
|
+
parsed = text ? JSON.parse(text) : {};
|
|
501
|
+
} catch {
|
|
502
|
+
parsed = { raw: text };
|
|
503
|
+
}
|
|
504
|
+
const requestId = parsed?.error?.message?.match(/request id:\s*([^)]+)/i)?.[1] || response.headers.get("x-oneapi-request-id") || undefined;
|
|
505
|
+
|
|
506
|
+
writeDebugFile(response.ok ? "response" : "error", modelId, requestId, {
|
|
507
|
+
status: response.status,
|
|
508
|
+
statusText: response.statusText,
|
|
509
|
+
requestId,
|
|
510
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
511
|
+
body: parsed,
|
|
512
|
+
raw: text,
|
|
513
|
+
retryAttempt: attempt,
|
|
514
|
+
maxRetries,
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
if (response.ok) {
|
|
518
|
+
if (options?.onResponse) {
|
|
519
|
+
await options.onResponse({ status: response.status, headers: Object.fromEntries(response.headers.entries()) }, model);
|
|
520
|
+
}
|
|
521
|
+
return parsed;
|
|
522
|
+
}
|
|
523
|
+
if (attempt < maxRetries && isRetryableStatus(response.status)) {
|
|
524
|
+
await delay(getRetryDelayMs(attempt, parseRetryAfterMs(response.headers.get("retry-after"))));
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
throw new Error(text || `HTTP ${response.status}`);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
throw new Error(lastErrorText || "HTTP request failed after retries");
|
|
531
|
+
}
|