@lll9p/pi-anyrouter 0.4.0 → 0.5.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/package.json +1 -1
- package/src/claude-code.ts +79 -162
- package/src/codex.ts +50 -63
- package/src/http.ts +0 -29
- package/src/index.ts +1 -1
package/package.json
CHANGED
package/src/claude-code.ts
CHANGED
|
@@ -11,17 +11,7 @@ import type {
|
|
|
11
11
|
Tool,
|
|
12
12
|
ToolResultMessage,
|
|
13
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";
|
|
14
|
+
import { fetchWithProxy, nextSseChunk, parseSseEvent, redactHeaders, writeDebugFile } from "./http.js";
|
|
25
15
|
import {
|
|
26
16
|
ANTHROPIC_BETA,
|
|
27
17
|
CLAUDE_CODE_VERSION,
|
|
@@ -209,8 +199,7 @@ function applySsePayloadEvent(
|
|
|
209
199
|
if (!payload?.type || payload.type === "ping" || payload.type === "message_stop") return;
|
|
210
200
|
|
|
211
201
|
if (payload.type === "error") {
|
|
212
|
-
|
|
213
|
-
throw new Error(String(errorText));
|
|
202
|
+
throw new Error(String(payload?.error?.message || payload?.error || payload?.message || JSON.stringify(payload)));
|
|
214
203
|
}
|
|
215
204
|
|
|
216
205
|
if (payload.type === "message_start") {
|
|
@@ -327,91 +316,10 @@ function applySsePayloadEvent(
|
|
|
327
316
|
}
|
|
328
317
|
}
|
|
329
318
|
|
|
330
|
-
// ──
|
|
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");
|
|
319
|
+
// ── Stream consumption ──────────────────────────────────────────────────────
|
|
414
320
|
|
|
321
|
+
async function consumeCcStream(response: Response, output: AssistantMessage, stream: AssistantMessageEventStream, model: Model<Api>) {
|
|
322
|
+
if (!response.body) throw new Error("stream response body missing");
|
|
415
323
|
const blockIndexByEventIndex = new Map<number, number>();
|
|
416
324
|
const reader = response.body.getReader();
|
|
417
325
|
const decoder = new TextDecoder();
|
|
@@ -445,11 +353,57 @@ export async function tryStreamAnyRouterCc(
|
|
|
445
353
|
applySsePayloadEvent(payload, output, stream, model, blockIndexByEventIndex);
|
|
446
354
|
}
|
|
447
355
|
}
|
|
356
|
+
return response;
|
|
357
|
+
}
|
|
448
358
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
359
|
+
// ── Streaming request ───────────────────────────────────────────────────────
|
|
360
|
+
|
|
361
|
+
export async function tryStreamAnyRouterCc(
|
|
362
|
+
url: string,
|
|
363
|
+
body: Json,
|
|
364
|
+
apiKey: string,
|
|
365
|
+
model: Model<Api>,
|
|
366
|
+
output: AssistantMessage,
|
|
367
|
+
stream: AssistantMessageEventStream,
|
|
368
|
+
sessionId: string,
|
|
369
|
+
options?: SimpleStreamOptions,
|
|
370
|
+
) {
|
|
371
|
+
const requestBody = { ...body, stream: true };
|
|
372
|
+
const headers = getClaudeCodeHeaders(apiKey, 0, sessionId);
|
|
373
|
+
writeDebugFile("request", model.id, undefined, { url, headers: redactHeaders(headers), body: requestBody, transport: "sse" });
|
|
374
|
+
|
|
375
|
+
const response = await fetchWithProxy(url, { method: "POST", signal: options?.signal, headers, body: JSON.stringify(requestBody) });
|
|
376
|
+
|
|
377
|
+
if (!response.ok) {
|
|
378
|
+
const raw = await response.text();
|
|
379
|
+
const parsed = tryParseJson(raw) || { raw };
|
|
380
|
+
const requestId = extractRequestId(parsed, response.headers);
|
|
381
|
+
writeDebugFile("error", model.id, requestId, {
|
|
382
|
+
status: response.status,
|
|
383
|
+
statusText: response.statusText,
|
|
384
|
+
requestId,
|
|
385
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
386
|
+
body: parsed,
|
|
387
|
+
raw,
|
|
388
|
+
transport: "sse",
|
|
389
|
+
});
|
|
390
|
+
throw new Error(raw || `HTTP ${response.status}`);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const contentType = response.headers.get("content-type") || "";
|
|
394
|
+
if (!contentType.includes("text/event-stream")) {
|
|
395
|
+
throw new Error(`stream response was not SSE (content-type=${contentType || "<missing>"})`);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (options?.onResponse) {
|
|
399
|
+
await options.onResponse({ status: response.status, headers: Object.fromEntries(response.headers.entries()) }, model);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const resp = await consumeCcStream(response, output, stream, model);
|
|
403
|
+
writeDebugFile("response", model.id, resp.headers.get("x-oneapi-request-id") || undefined, {
|
|
404
|
+
status: resp.status,
|
|
405
|
+
statusText: resp.statusText,
|
|
406
|
+
headers: Object.fromEntries(resp.headers.entries()),
|
|
453
407
|
body: {
|
|
454
408
|
responseId: output.responseId,
|
|
455
409
|
stopReason: output.stopReason,
|
|
@@ -463,69 +417,32 @@ export async function tryStreamAnyRouterCc(
|
|
|
463
417
|
// ── JSON request ────────────────────────────────────────────────────────────
|
|
464
418
|
|
|
465
419
|
export async function postJson(url: string, body: Json, apiKey: string, modelId: string, sessionId: string, model: Model<Api>, options?: SimpleStreamOptions) {
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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
|
-
}
|
|
420
|
+
const headers = getClaudeCodeHeaders(apiKey, 0, sessionId);
|
|
421
|
+
writeDebugFile("request", modelId, undefined, { url, headers: redactHeaders(headers), body });
|
|
422
|
+
|
|
423
|
+
const response = await fetchWithProxy(url, { method: "POST", signal: options?.signal, headers, body: JSON.stringify(body) });
|
|
424
|
+
const text = await response.text();
|
|
425
|
+
let parsed: any = {};
|
|
426
|
+
try {
|
|
427
|
+
parsed = text ? JSON.parse(text) : {};
|
|
428
|
+
} catch {
|
|
429
|
+
parsed = { raw: text };
|
|
430
|
+
}
|
|
431
|
+
const requestId = parsed?.error?.message?.match(/request id:\s*([^)]+)/i)?.[1] || response.headers.get("x-oneapi-request-id") || undefined;
|
|
495
432
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
const requestId = parsed?.error?.message?.match(/request id:\s*([^)]+)/i)?.[1] || response.headers.get("x-oneapi-request-id") || undefined;
|
|
433
|
+
writeDebugFile(response.ok ? "response" : "error", modelId, requestId, {
|
|
434
|
+
status: response.status,
|
|
435
|
+
statusText: response.statusText,
|
|
436
|
+
requestId,
|
|
437
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
438
|
+
body: parsed,
|
|
439
|
+
raw: text,
|
|
440
|
+
});
|
|
505
441
|
|
|
506
|
-
|
|
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
|
-
});
|
|
442
|
+
if (!response.ok) throw new Error(text || `HTTP ${response.status}`);
|
|
516
443
|
|
|
517
|
-
|
|
518
|
-
|
|
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}`);
|
|
444
|
+
if (options?.onResponse) {
|
|
445
|
+
await options.onResponse({ status: response.status, headers: Object.fromEntries(response.headers.entries()) }, model);
|
|
528
446
|
}
|
|
529
|
-
|
|
530
|
-
throw new Error(lastErrorText || "HTTP request failed after retries");
|
|
447
|
+
return parsed;
|
|
531
448
|
}
|
package/src/codex.ts
CHANGED
|
@@ -11,17 +11,7 @@ import {
|
|
|
11
11
|
type Tool,
|
|
12
12
|
type ToolResultMessage,
|
|
13
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";
|
|
14
|
+
import { fetchWithProxy, nextSseChunk, parseSseEvent, redactHeaders, writeDebugFile } from "./http.js";
|
|
25
15
|
import { CODEX_INSTALLATION_ID, CODEX_VERSION, type Json } from "./types.js";
|
|
26
16
|
import { extractRequestId, mapReasoningEffort, sanitizeText, tryParseJson } from "./utils.js";
|
|
27
17
|
|
|
@@ -214,8 +204,12 @@ function applyCodexUsage(output: AssistantMessage, response: any, model: Model<A
|
|
|
214
204
|
function applyCodexSsePayload(payload: any, output: AssistantMessage, stream: AssistantMessageEventStream, model: Model<Api>, slots: Map<number, any>) {
|
|
215
205
|
const type = payload?.type;
|
|
216
206
|
if (!type || type === "response.in_progress" || type === "response.metadata") return;
|
|
217
|
-
if (type === "error")
|
|
218
|
-
|
|
207
|
+
if (type === "error") {
|
|
208
|
+
throw new Error(payload.error?.message || payload.message || JSON.stringify(payload));
|
|
209
|
+
}
|
|
210
|
+
if (type === "response.failed") {
|
|
211
|
+
throw new Error(payload.response?.error?.message || "Codex response failed");
|
|
212
|
+
}
|
|
219
213
|
|
|
220
214
|
if (type === "response.created") {
|
|
221
215
|
output.responseId = payload.response?.id || output.responseId;
|
|
@@ -284,55 +278,10 @@ function applyCodexSsePayload(payload: any, output: AssistantMessage, stream: As
|
|
|
284
278
|
}
|
|
285
279
|
}
|
|
286
280
|
|
|
287
|
-
// ──
|
|
288
|
-
|
|
289
|
-
export async function tryStreamAnyRouterCodex(
|
|
290
|
-
url: string,
|
|
291
|
-
body: Json,
|
|
292
|
-
apiKey: string,
|
|
293
|
-
model: Model<Api>,
|
|
294
|
-
output: AssistantMessage,
|
|
295
|
-
stream: AssistantMessageEventStream,
|
|
296
|
-
sessionId: string,
|
|
297
|
-
metadata: ReturnType<typeof createCodexMetadata>,
|
|
298
|
-
options?: SimpleStreamOptions,
|
|
299
|
-
) {
|
|
300
|
-
const bodyText = JSON.stringify(body);
|
|
301
|
-
const maxRetries = Math.max(0, Number(process.env.PI_ANYROUTER_CC_MAX_RETRIES || options?.maxRetries || "10") || 0);
|
|
302
|
-
let response: Response | undefined;
|
|
303
|
-
|
|
304
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
305
|
-
const headers = createCodexHeaders(apiKey, sessionId, metadata);
|
|
306
|
-
if (attempt === 0) writeDebugFile("request", model.id, undefined, { url, headers: redactHeaders(headers), body, transport: "codex-sse" });
|
|
307
|
-
try {
|
|
308
|
-
response = await fetchWithProxy(url, { method: "POST", signal: options?.signal, headers, body: bodyText });
|
|
309
|
-
} catch (error) {
|
|
310
|
-
if (attempt < maxRetries && !options?.signal?.aborted) {
|
|
311
|
-
await delay(getRetryDelayMs(attempt));
|
|
312
|
-
continue;
|
|
313
|
-
}
|
|
314
|
-
throw error;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
if (response.ok && (response.headers.get("content-type") || "").includes("text/event-stream")) {
|
|
318
|
-
if (options?.onResponse) {
|
|
319
|
-
await options.onResponse({ status: response.status, headers: Object.fromEntries(response.headers.entries()) }, model);
|
|
320
|
-
}
|
|
321
|
-
break;
|
|
322
|
-
}
|
|
323
|
-
const raw = await response.text();
|
|
324
|
-
const parsed = tryParseJson(raw) || { raw };
|
|
325
|
-
const requestId = extractRequestId(parsed, response.headers);
|
|
326
|
-
writeDebugFile("error", model.id, requestId, { status: response.status, requestId, body: parsed, raw, transport: "codex-sse", retryAttempt: attempt });
|
|
327
|
-
if (!response.ok && attempt < maxRetries && isRetryableStatus(response.status)) {
|
|
328
|
-
await delay(getRetryDelayMs(attempt, parseRetryAfterMs(response.headers.get("retry-after"))));
|
|
329
|
-
response = undefined;
|
|
330
|
-
continue;
|
|
331
|
-
}
|
|
332
|
-
throw new Error(raw || `HTTP ${response.status}`);
|
|
333
|
-
}
|
|
281
|
+
// ── Stream consumption ──────────────────────────────────────────────────────
|
|
334
282
|
|
|
335
|
-
|
|
283
|
+
async function consumeCodexStream(response: Response, output: AssistantMessage, stream: AssistantMessageEventStream, model: Model<Api>) {
|
|
284
|
+
if (!response.body) throw new Error("Codex stream response body missing");
|
|
336
285
|
const slots = new Map<number, any>();
|
|
337
286
|
const reader = response.body.getReader();
|
|
338
287
|
const decoder = new TextDecoder();
|
|
@@ -366,8 +315,46 @@ export async function tryStreamAnyRouterCodex(
|
|
|
366
315
|
}
|
|
367
316
|
}
|
|
368
317
|
if (!terminal) throw new Error("Codex stream ended before a terminal response event");
|
|
369
|
-
|
|
370
|
-
|
|
318
|
+
return response;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ── Streaming request ───────────────────────────────────────────────────────
|
|
322
|
+
|
|
323
|
+
export async function tryStreamAnyRouterCodex(
|
|
324
|
+
url: string,
|
|
325
|
+
body: Json,
|
|
326
|
+
apiKey: string,
|
|
327
|
+
model: Model<Api>,
|
|
328
|
+
output: AssistantMessage,
|
|
329
|
+
stream: AssistantMessageEventStream,
|
|
330
|
+
sessionId: string,
|
|
331
|
+
metadata: ReturnType<typeof createCodexMetadata>,
|
|
332
|
+
options?: SimpleStreamOptions,
|
|
333
|
+
) {
|
|
334
|
+
const headers = createCodexHeaders(apiKey, sessionId, metadata);
|
|
335
|
+
writeDebugFile("request", model.id, undefined, { url, headers: redactHeaders(headers), body, transport: "codex-sse" });
|
|
336
|
+
|
|
337
|
+
const response = await fetchWithProxy(url, { method: "POST", signal: options?.signal, headers, body: JSON.stringify(body) });
|
|
338
|
+
|
|
339
|
+
if (!response.ok) {
|
|
340
|
+
const raw = await response.text();
|
|
341
|
+
const parsed = tryParseJson(raw) || { raw };
|
|
342
|
+
const requestId = extractRequestId(parsed, response.headers);
|
|
343
|
+
writeDebugFile("error", model.id, requestId, { status: response.status, requestId, body: parsed, raw, transport: "codex-sse" });
|
|
344
|
+
throw new Error(raw || `HTTP ${response.status}`);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (!(response.headers.get("content-type") || "").includes("text/event-stream")) {
|
|
348
|
+
throw new Error(`stream response was not SSE (content-type=${response.headers.get("content-type") || "<missing>"})`);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (options?.onResponse) {
|
|
352
|
+
await options.onResponse({ status: response.status, headers: Object.fromEntries(response.headers.entries()) }, model);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const resp = await consumeCodexStream(response, output, stream, model);
|
|
356
|
+
writeDebugFile("response", model.id, resp.headers.get("x-oneapi-request-id") || undefined, {
|
|
357
|
+
status: resp.status,
|
|
371
358
|
responseId: output.responseId,
|
|
372
359
|
stopReason: output.stopReason,
|
|
373
360
|
usage: output.usage,
|
package/src/http.ts
CHANGED
|
@@ -60,35 +60,6 @@ export function writeDebugFile(kind: "request" | "response" | "error", modelId:
|
|
|
60
60
|
writeFileSync(path, JSON.stringify(payload, null, 2), "utf8");
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
// ── Retry ───────────────────────────────────────────────────────────────────
|
|
64
|
-
|
|
65
|
-
export function delay(ms: number) {
|
|
66
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function isRetryableStatus(status: number) {
|
|
70
|
-
return [408, 409, 429, 500, 502, 503, 504, 520, 522, 524].includes(status);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function parseRetryAfterMs(value: string | null) {
|
|
74
|
-
if (!value) return undefined;
|
|
75
|
-
const seconds = Number(value);
|
|
76
|
-
if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1000;
|
|
77
|
-
const at = Date.parse(value);
|
|
78
|
-
if (Number.isFinite(at)) {
|
|
79
|
-
const delta = at - Date.now();
|
|
80
|
-
return delta > 0 ? delta : 0;
|
|
81
|
-
}
|
|
82
|
-
return undefined;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function getRetryDelayMs(attempt: number, retryAfterMs?: number) {
|
|
86
|
-
if (typeof retryAfterMs === "number") return Math.max(0, Math.min(retryAfterMs, 30_000));
|
|
87
|
-
const base = Math.min(1000 * 2 ** attempt, 15_000);
|
|
88
|
-
const jitter = Math.floor(Math.random() * 250);
|
|
89
|
-
return base + jitter;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
63
|
// ── SSE parsing ─────────────────────────────────────────────────────────────
|
|
93
64
|
|
|
94
65
|
export function parseSseEvent(chunk: string) {
|
package/src/index.ts
CHANGED
|
@@ -41,7 +41,7 @@ function streamAnyRouterCc(model: Model<Api>, context: Context, options?: Simple
|
|
|
41
41
|
try {
|
|
42
42
|
const source = loadSourceProvider();
|
|
43
43
|
const apiKey = options?.apiKey || source.apiKey;
|
|
44
|
-
const sessionId = randomUUID();
|
|
44
|
+
const sessionId = options?.sessionId || randomUUID();
|
|
45
45
|
|
|
46
46
|
const configuredModel = source.models.find((item) => item.id === model.id);
|
|
47
47
|
if (isCodexModel(model.id, configuredModel?.api)) {
|