@lll9p/pi-anyrouter 0.4.1 → 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 +64 -176
- package/src/codex.ts +26 -78
- package/src/http.ts +0 -46
- package/src/index.ts +1 -1
package/package.json
CHANGED
package/src/claude-code.ts
CHANGED
|
@@ -11,19 +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
|
-
isRetryableErrorType,
|
|
19
|
-
isRetryableStatus,
|
|
20
|
-
nextSseChunk,
|
|
21
|
-
parseRetryAfterMs,
|
|
22
|
-
parseSseEvent,
|
|
23
|
-
RetryableStreamError,
|
|
24
|
-
redactHeaders,
|
|
25
|
-
writeDebugFile,
|
|
26
|
-
} from "./http.js";
|
|
14
|
+
import { fetchWithProxy, nextSseChunk, parseSseEvent, redactHeaders, writeDebugFile } from "./http.js";
|
|
27
15
|
import {
|
|
28
16
|
ANTHROPIC_BETA,
|
|
29
17
|
CLAUDE_CODE_VERSION,
|
|
@@ -211,11 +199,7 @@ function applySsePayloadEvent(
|
|
|
211
199
|
if (!payload?.type || payload.type === "ping" || payload.type === "message_stop") return;
|
|
212
200
|
|
|
213
201
|
if (payload.type === "error") {
|
|
214
|
-
|
|
215
|
-
const errorText = payload?.error?.message || payload?.error || payload?.message || JSON.stringify(payload);
|
|
216
|
-
const message = String(errorText);
|
|
217
|
-
if (isRetryableErrorType(errorType)) throw new RetryableStreamError(message);
|
|
218
|
-
throw new Error(message);
|
|
202
|
+
throw new Error(String(payload?.error?.message || payload?.error || payload?.message || JSON.stringify(payload)));
|
|
219
203
|
}
|
|
220
204
|
|
|
221
205
|
if (payload.type === "message_start") {
|
|
@@ -385,176 +369,80 @@ export async function tryStreamAnyRouterCc(
|
|
|
385
369
|
options?: SimpleStreamOptions,
|
|
386
370
|
) {
|
|
387
371
|
const requestBody = { ...body, stream: true };
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
392
|
-
const headers = getClaudeCodeHeaders(apiKey, 0, sessionId);
|
|
393
|
-
if (attempt === 0) {
|
|
394
|
-
writeDebugFile("request", model.id, undefined, {
|
|
395
|
-
url,
|
|
396
|
-
headers: redactHeaders(headers),
|
|
397
|
-
body: requestBody,
|
|
398
|
-
transport: "sse",
|
|
399
|
-
});
|
|
400
|
-
}
|
|
372
|
+
const headers = getClaudeCodeHeaders(apiKey, 0, sessionId);
|
|
373
|
+
writeDebugFile("request", model.id, undefined, { url, headers: redactHeaders(headers), body: requestBody, transport: "sse" });
|
|
401
374
|
|
|
402
|
-
|
|
403
|
-
try {
|
|
404
|
-
response = await fetchWithProxy(url, {
|
|
405
|
-
method: "POST",
|
|
406
|
-
signal: options?.signal,
|
|
407
|
-
headers,
|
|
408
|
-
body: bodyText,
|
|
409
|
-
});
|
|
410
|
-
} catch (error) {
|
|
411
|
-
if (attempt < maxRetries && !options?.signal?.aborted) {
|
|
412
|
-
await delay(getRetryDelayMs(attempt));
|
|
413
|
-
continue;
|
|
414
|
-
}
|
|
415
|
-
throw error;
|
|
416
|
-
}
|
|
375
|
+
const response = await fetchWithProxy(url, { method: "POST", signal: options?.signal, headers, body: JSON.stringify(requestBody) });
|
|
417
376
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
});
|
|
434
|
-
if (attempt < maxRetries && isRetryableStatus(response.status)) {
|
|
435
|
-
const retryBlockIndex = output.content.length;
|
|
436
|
-
const retryText = `⏳ ${response.status} — retrying (${attempt + 1}/${maxRetries})…`;
|
|
437
|
-
output.content.push({ type: "text", text: retryText } as any);
|
|
438
|
-
stream.push({ type: "text_start", contentIndex: retryBlockIndex, partial: output });
|
|
439
|
-
stream.push({ type: "text_delta", contentIndex: retryBlockIndex, delta: retryText, partial: output });
|
|
440
|
-
stream.push({ type: "text_end", contentIndex: retryBlockIndex, content: retryText, partial: output });
|
|
441
|
-
await delay(getRetryDelayMs(attempt, parseRetryAfterMs(response.headers.get("retry-after"))));
|
|
442
|
-
continue;
|
|
443
|
-
}
|
|
444
|
-
throw new Error(raw || `HTTP ${response.status}`);
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
const contentType = response.headers.get("content-type") || "";
|
|
448
|
-
if (!contentType.includes("text/event-stream")) {
|
|
449
|
-
if (response.ok) throw new Error(`stream response was not SSE (content-type=${contentType || "<missing>"})`);
|
|
450
|
-
}
|
|
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
|
+
}
|
|
451
392
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
}
|
|
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
|
+
}
|
|
455
397
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
try {
|
|
459
|
-
const resp = await consumeCcStream(response, output, stream, model);
|
|
460
|
-
writeDebugFile("response", model.id, resp.headers.get("x-oneapi-request-id") || undefined, {
|
|
461
|
-
status: resp.status,
|
|
462
|
-
statusText: resp.statusText,
|
|
463
|
-
headers: Object.fromEntries(resp.headers.entries()),
|
|
464
|
-
body: {
|
|
465
|
-
responseId: output.responseId,
|
|
466
|
-
stopReason: output.stopReason,
|
|
467
|
-
usage: output.usage,
|
|
468
|
-
contentBlocks: output.content.length,
|
|
469
|
-
},
|
|
470
|
-
transport: "sse",
|
|
471
|
-
});
|
|
472
|
-
return;
|
|
473
|
-
} catch (error) {
|
|
474
|
-
if (error instanceof RetryableStreamError && output.content.length === contentLenBefore && attempt < maxRetries && !options?.signal?.aborted) {
|
|
475
|
-
writeDebugFile("error", model.id, undefined, { phase: "sse-stream-retry", errorMessage: error.message, retryAttempt: attempt, transport: "sse" });
|
|
476
|
-
const retryBlockIndex = output.content.length;
|
|
477
|
-
const retryText = `⏳ SSE error — retrying (${attempt + 1}/${maxRetries})…`;
|
|
478
|
-
output.content.push({ type: "text", text: retryText } as any);
|
|
479
|
-
stream.push({ type: "text_start", contentIndex: retryBlockIndex, partial: output });
|
|
480
|
-
stream.push({ type: "text_delta", contentIndex: retryBlockIndex, delta: retryText, partial: output });
|
|
481
|
-
stream.push({ type: "text_end", contentIndex: retryBlockIndex, content: retryText, partial: output });
|
|
482
|
-
await delay(getRetryDelayMs(attempt, error.retryAfterMs));
|
|
483
|
-
continue;
|
|
484
|
-
}
|
|
485
|
-
throw error;
|
|
486
|
-
}
|
|
398
|
+
if (options?.onResponse) {
|
|
399
|
+
await options.onResponse({ status: response.status, headers: Object.fromEntries(response.headers.entries()) }, model);
|
|
487
400
|
}
|
|
488
401
|
|
|
489
|
-
|
|
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()),
|
|
407
|
+
body: {
|
|
408
|
+
responseId: output.responseId,
|
|
409
|
+
stopReason: output.stopReason,
|
|
410
|
+
usage: output.usage,
|
|
411
|
+
contentBlocks: output.content.length,
|
|
412
|
+
},
|
|
413
|
+
transport: "sse",
|
|
414
|
+
});
|
|
490
415
|
}
|
|
491
416
|
|
|
492
417
|
// ── JSON request ────────────────────────────────────────────────────────────
|
|
493
418
|
|
|
494
419
|
export async function postJson(url: string, body: Json, apiKey: string, modelId: string, sessionId: string, model: Model<Api>, options?: SimpleStreamOptions) {
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
});
|
|
517
|
-
} catch (error) {
|
|
518
|
-
if (attempt < maxRetries) {
|
|
519
|
-
await delay(getRetryDelayMs(attempt));
|
|
520
|
-
continue;
|
|
521
|
-
}
|
|
522
|
-
throw error;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
const text = await response.text();
|
|
526
|
-
lastErrorText = text;
|
|
527
|
-
let parsed: any = {};
|
|
528
|
-
try {
|
|
529
|
-
parsed = text ? JSON.parse(text) : {};
|
|
530
|
-
} catch {
|
|
531
|
-
parsed = { raw: text };
|
|
532
|
-
}
|
|
533
|
-
const requestId = parsed?.error?.message?.match(/request id:\s*([^)]+)/i)?.[1] || response.headers.get("x-oneapi-request-id") || undefined;
|
|
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;
|
|
432
|
+
|
|
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
|
+
});
|
|
534
441
|
|
|
535
|
-
|
|
536
|
-
status: response.status,
|
|
537
|
-
statusText: response.statusText,
|
|
538
|
-
requestId,
|
|
539
|
-
headers: Object.fromEntries(response.headers.entries()),
|
|
540
|
-
body: parsed,
|
|
541
|
-
raw: text,
|
|
542
|
-
retryAttempt: attempt,
|
|
543
|
-
maxRetries,
|
|
544
|
-
});
|
|
442
|
+
if (!response.ok) throw new Error(text || `HTTP ${response.status}`);
|
|
545
443
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
await options.onResponse({ status: response.status, headers: Object.fromEntries(response.headers.entries()) }, model);
|
|
549
|
-
}
|
|
550
|
-
return parsed;
|
|
551
|
-
}
|
|
552
|
-
if (attempt < maxRetries && isRetryableStatus(response.status)) {
|
|
553
|
-
await delay(getRetryDelayMs(attempt, parseRetryAfterMs(response.headers.get("retry-after"))));
|
|
554
|
-
continue;
|
|
555
|
-
}
|
|
556
|
-
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);
|
|
557
446
|
}
|
|
558
|
-
|
|
559
|
-
throw new Error(lastErrorText || "HTTP request failed after retries");
|
|
447
|
+
return parsed;
|
|
560
448
|
}
|
package/src/codex.ts
CHANGED
|
@@ -11,19 +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
|
-
isRetryableErrorType,
|
|
19
|
-
isRetryableStatus,
|
|
20
|
-
nextSseChunk,
|
|
21
|
-
parseRetryAfterMs,
|
|
22
|
-
parseSseEvent,
|
|
23
|
-
RetryableStreamError,
|
|
24
|
-
redactHeaders,
|
|
25
|
-
writeDebugFile,
|
|
26
|
-
} from "./http.js";
|
|
14
|
+
import { fetchWithProxy, nextSseChunk, parseSseEvent, redactHeaders, writeDebugFile } from "./http.js";
|
|
27
15
|
import { CODEX_INSTALLATION_ID, CODEX_VERSION, type Json } from "./types.js";
|
|
28
16
|
import { extractRequestId, mapReasoningEffort, sanitizeText, tryParseJson } from "./utils.js";
|
|
29
17
|
|
|
@@ -217,17 +205,10 @@ function applyCodexSsePayload(payload: any, output: AssistantMessage, stream: As
|
|
|
217
205
|
const type = payload?.type;
|
|
218
206
|
if (!type || type === "response.in_progress" || type === "response.metadata") return;
|
|
219
207
|
if (type === "error") {
|
|
220
|
-
|
|
221
|
-
const message = payload.error?.message || payload.message || JSON.stringify(payload);
|
|
222
|
-
if (isRetryableErrorType(errorType)) throw new RetryableStreamError(message);
|
|
223
|
-
throw new Error(message);
|
|
208
|
+
throw new Error(payload.error?.message || payload.message || JSON.stringify(payload));
|
|
224
209
|
}
|
|
225
210
|
if (type === "response.failed") {
|
|
226
|
-
|
|
227
|
-
const message = err?.message || "Codex response failed";
|
|
228
|
-
const errorType = err?.type || err?.code;
|
|
229
|
-
if (isRetryableErrorType(errorType)) throw new RetryableStreamError(message);
|
|
230
|
-
throw new Error(message);
|
|
211
|
+
throw new Error(payload.response?.error?.message || "Codex response failed");
|
|
231
212
|
}
|
|
232
213
|
|
|
233
214
|
if (type === "response.created") {
|
|
@@ -350,66 +331,33 @@ export async function tryStreamAnyRouterCodex(
|
|
|
350
331
|
metadata: ReturnType<typeof createCodexMetadata>,
|
|
351
332
|
options?: SimpleStreamOptions,
|
|
352
333
|
) {
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
357
|
-
const headers = createCodexHeaders(apiKey, sessionId, metadata);
|
|
358
|
-
if (attempt === 0) writeDebugFile("request", model.id, undefined, { url, headers: redactHeaders(headers), body, transport: "codex-sse" });
|
|
334
|
+
const headers = createCodexHeaders(apiKey, sessionId, metadata);
|
|
335
|
+
writeDebugFile("request", model.id, undefined, { url, headers: redactHeaders(headers), body, transport: "codex-sse" });
|
|
359
336
|
|
|
360
|
-
|
|
361
|
-
try {
|
|
362
|
-
response = await fetchWithProxy(url, { method: "POST", signal: options?.signal, headers, body: bodyText });
|
|
363
|
-
} catch (error) {
|
|
364
|
-
if (attempt < maxRetries && !options?.signal?.aborted) {
|
|
365
|
-
await delay(getRetryDelayMs(attempt));
|
|
366
|
-
continue;
|
|
367
|
-
}
|
|
368
|
-
throw error;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// HTTP-level error → retry if retryable status
|
|
372
|
-
if (!response.ok) {
|
|
373
|
-
const raw = await response.text();
|
|
374
|
-
const parsed = tryParseJson(raw) || { raw };
|
|
375
|
-
const requestId = extractRequestId(parsed, response.headers);
|
|
376
|
-
writeDebugFile("error", model.id, requestId, { status: response.status, requestId, body: parsed, raw, transport: "codex-sse", retryAttempt: attempt });
|
|
377
|
-
if (attempt < maxRetries && isRetryableStatus(response.status)) {
|
|
378
|
-
await delay(getRetryDelayMs(attempt, parseRetryAfterMs(response.headers.get("retry-after"))));
|
|
379
|
-
continue;
|
|
380
|
-
}
|
|
381
|
-
throw new Error(raw || `HTTP ${response.status}`);
|
|
382
|
-
}
|
|
337
|
+
const response = await fetchWithProxy(url, { method: "POST", signal: options?.signal, headers, body: JSON.stringify(body) });
|
|
383
338
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
}
|
|
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
|
+
}
|
|
387
346
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
+
}
|
|
391
350
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
try {
|
|
395
|
-
const resp = await consumeCodexStream(response, output, stream, model);
|
|
396
|
-
writeDebugFile("response", model.id, resp.headers.get("x-oneapi-request-id") || undefined, {
|
|
397
|
-
status: resp.status,
|
|
398
|
-
responseId: output.responseId,
|
|
399
|
-
stopReason: output.stopReason,
|
|
400
|
-
usage: output.usage,
|
|
401
|
-
transport: "codex-sse",
|
|
402
|
-
});
|
|
403
|
-
return;
|
|
404
|
-
} catch (error) {
|
|
405
|
-
if (error instanceof RetryableStreamError && output.content.length === contentLenBefore && attempt < maxRetries && !options?.signal?.aborted) {
|
|
406
|
-
writeDebugFile("error", model.id, undefined, { phase: "sse-stream-retry", errorMessage: error.message, retryAttempt: attempt, transport: "codex-sse" });
|
|
407
|
-
await delay(getRetryDelayMs(attempt, error.retryAfterMs));
|
|
408
|
-
continue;
|
|
409
|
-
}
|
|
410
|
-
throw error;
|
|
411
|
-
}
|
|
351
|
+
if (options?.onResponse) {
|
|
352
|
+
await options.onResponse({ status: response.status, headers: Object.fromEntries(response.headers.entries()) }, model);
|
|
412
353
|
}
|
|
413
354
|
|
|
414
|
-
|
|
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,
|
|
358
|
+
responseId: output.responseId,
|
|
359
|
+
stopReason: output.stopReason,
|
|
360
|
+
usage: output.usage,
|
|
361
|
+
transport: "codex-sse",
|
|
362
|
+
});
|
|
415
363
|
}
|
package/src/http.ts
CHANGED
|
@@ -60,52 +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
|
-
/** Thrown when an SSE stream delivers a retryable error (e.g. rate_limit) before any content was emitted. */
|
|
66
|
-
export class RetryableStreamError extends Error {
|
|
67
|
-
constructor(
|
|
68
|
-
message: string,
|
|
69
|
-
public readonly retryAfterMs?: number,
|
|
70
|
-
) {
|
|
71
|
-
super(message);
|
|
72
|
-
this.name = "RetryableStreamError";
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const RETRYABLE_ERROR_TYPES = new Set(["too_many_requests", "rate_limit_exceeded", "server_error", "overloaded_error", "api_error"]);
|
|
77
|
-
|
|
78
|
-
export function isRetryableErrorType(errorType: string | undefined): boolean {
|
|
79
|
-
return !!errorType && RETRYABLE_ERROR_TYPES.has(errorType);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export function delay(ms: number) {
|
|
83
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function isRetryableStatus(status: number) {
|
|
87
|
-
return [408, 409, 429, 500, 502, 503, 504, 520, 522, 524].includes(status);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export function parseRetryAfterMs(value: string | null) {
|
|
91
|
-
if (!value) return undefined;
|
|
92
|
-
const seconds = Number(value);
|
|
93
|
-
if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1000;
|
|
94
|
-
const at = Date.parse(value);
|
|
95
|
-
if (Number.isFinite(at)) {
|
|
96
|
-
const delta = at - Date.now();
|
|
97
|
-
return delta > 0 ? delta : 0;
|
|
98
|
-
}
|
|
99
|
-
return undefined;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export function getRetryDelayMs(attempt: number, retryAfterMs?: number) {
|
|
103
|
-
if (typeof retryAfterMs === "number") return Math.max(0, Math.min(retryAfterMs, 30_000));
|
|
104
|
-
const base = Math.min(1000 * 2 ** attempt, 15_000);
|
|
105
|
-
const jitter = Math.floor(Math.random() * 250);
|
|
106
|
-
return base + jitter;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
63
|
// ── SSE parsing ─────────────────────────────────────────────────────────────
|
|
110
64
|
|
|
111
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)) {
|