@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lll9p/pi-anyrouter",
3
- "version": "0.4.1",
3
+ "version": "0.5.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",
@@ -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
- const errorType = payload?.error?.type || payload?.error?.code;
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 bodyText = JSON.stringify(requestBody);
389
- const maxRetries = Math.max(0, Number(process.env.PI_ANYROUTER_CC_MAX_RETRIES || options?.maxRetries || "10") || 0);
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
- let response: Response;
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
- // HTTP-level error → retry if retryable status
419
- if (!response.ok) {
420
- const raw = await response.text();
421
- const parsed = tryParseJson(raw) || { raw };
422
- const requestId = extractRequestId(parsed, response.headers);
423
- writeDebugFile("error", model.id, requestId, {
424
- status: response.status,
425
- statusText: response.statusText,
426
- requestId,
427
- headers: Object.fromEntries(response.headers.entries()),
428
- body: parsed,
429
- raw,
430
- transport: "sse",
431
- retryAttempt: attempt,
432
- maxRetries,
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
- if (options?.onResponse) {
453
- await options.onResponse({ status: response.status, headers: Object.fromEntries(response.headers.entries()) }, model);
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
- // Consume the SSE stream — retry on RetryableStreamError if no content was emitted
457
- const contentLenBefore = output.content.length;
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
- throw new Error("CC request failed after retries");
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 maxRetries = Math.max(0, Number(process.env.PI_ANYROUTER_CC_MAX_RETRIES || options?.maxRetries || "10") || 0);
496
- const bodyText = JSON.stringify(body);
497
- let lastErrorText = "";
498
-
499
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
500
- const headers = getClaudeCodeHeaders(apiKey, attempt, sessionId);
501
- if (attempt === 0) {
502
- writeDebugFile("request", modelId, undefined, {
503
- url,
504
- headers: redactHeaders(headers),
505
- body,
506
- });
507
- }
508
-
509
- let response: Response;
510
- try {
511
- response = await fetchWithProxy(url, {
512
- method: "POST",
513
- signal: options?.signal,
514
- headers,
515
- body: bodyText,
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
- writeDebugFile(response.ok ? "response" : "error", modelId, requestId, {
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
- if (response.ok) {
547
- if (options?.onResponse) {
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
- const errorType = payload.error?.type || payload.error?.code;
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
- const err = payload.response?.error;
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 bodyText = JSON.stringify(body);
354
- const maxRetries = Math.max(0, Number(process.env.PI_ANYROUTER_CC_MAX_RETRIES || options?.maxRetries || "10") || 0);
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
- let response: Response;
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
- if (!(response.headers.get("content-type") || "").includes("text/event-stream")) {
385
- throw new Error(`stream response was not SSE (content-type=${response.headers.get("content-type") || "<missing>"})`);
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
- if (options?.onResponse) {
389
- await options.onResponse({ status: response.status, headers: Object.fromEntries(response.headers.entries()) }, model);
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
- // Consume the SSE stream — retry on RetryableStreamError if no content was emitted
393
- const contentLenBefore = output.content.length;
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
- throw new Error("Codex request failed after retries");
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)) {