claudish 7.21.0 → 7.22.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.
Files changed (2) hide show
  1. package/dist/index.js +222 -7
  2. package/package.json +5 -5
package/dist/index.js CHANGED
@@ -651,7 +651,7 @@ var init_onepassword_config = __esm(() => {
651
651
  });
652
652
 
653
653
  // src/version.ts
654
- var VERSION = "7.21.0";
654
+ var VERSION = "7.22.0";
655
655
 
656
656
  // src/logger.ts
657
657
  var exports_logger = {};
@@ -37465,6 +37465,140 @@ var init_connection_error = __esm(() => {
37465
37465
  BUN_CONNECT_MESSAGE = /unable to connect\. is the computer able to access the url\?/i;
37466
37466
  });
37467
37467
 
37468
+ // src/handlers/shared/stream-head-sniffer.ts
37469
+ function isRetryableStreamError(code, type, message) {
37470
+ if (RETRYABLE_ERROR_CODES.has(code))
37471
+ return true;
37472
+ if (RETRYABLE_ERROR_TYPES.has(type))
37473
+ return true;
37474
+ return /overloaded|try again later|temporarily unavailable|please retry/i.test(message);
37475
+ }
37476
+ async function sniffResponsesStreamHead(response, opts = {}) {
37477
+ const budgetMs = opts.budgetMs ?? DEFAULT_SNIFF_BUDGET_MS;
37478
+ const logMsg = opts.log ?? (() => {});
37479
+ if (!response.body)
37480
+ return { kind: "clean", response };
37481
+ const reader = response.body.getReader();
37482
+ const decoder = new TextDecoder;
37483
+ const consumed = [];
37484
+ let pending = "";
37485
+ const deadline = Date.now() + budgetMs;
37486
+ const replayResponse = () => {
37487
+ const buffered = consumed.slice();
37488
+ const body = new ReadableStream({
37489
+ start: async (controller) => {
37490
+ try {
37491
+ for (const chunk of buffered)
37492
+ controller.enqueue(chunk);
37493
+ while (true) {
37494
+ const { done, value } = await reader.read();
37495
+ if (done)
37496
+ break;
37497
+ if (value)
37498
+ controller.enqueue(value);
37499
+ }
37500
+ controller.close();
37501
+ } catch (error46) {
37502
+ try {
37503
+ controller.error(error46);
37504
+ } catch {}
37505
+ }
37506
+ },
37507
+ cancel: () => {
37508
+ reader.cancel().catch(() => {});
37509
+ }
37510
+ });
37511
+ return new Response(body, {
37512
+ status: response.status,
37513
+ statusText: response.statusText,
37514
+ headers: response.headers
37515
+ });
37516
+ };
37517
+ try {
37518
+ while (true) {
37519
+ const remaining = deadline - Date.now();
37520
+ if (remaining <= 0) {
37521
+ logMsg(`[StreamSniff] budget ${budgetMs}ms elapsed with no verdict \u2014 streaming through`);
37522
+ return { kind: "clean", response: replayResponse() };
37523
+ }
37524
+ let timer;
37525
+ const timeout = new Promise((resolve) => {
37526
+ timer = setTimeout(() => resolve("timeout"), remaining);
37527
+ });
37528
+ let result;
37529
+ try {
37530
+ result = await Promise.race([reader.read(), timeout]);
37531
+ } finally {
37532
+ if (timer)
37533
+ clearTimeout(timer);
37534
+ }
37535
+ if (result === "timeout") {
37536
+ logMsg(`[StreamSniff] budget ${budgetMs}ms elapsed mid-read \u2014 streaming through`);
37537
+ return { kind: "clean", response: replayResponse() };
37538
+ }
37539
+ if (result.done)
37540
+ return { kind: "clean", response: replayResponse() };
37541
+ if (!result.value)
37542
+ continue;
37543
+ consumed.push(result.value);
37544
+ pending += decoder.decode(result.value, { stream: true });
37545
+ const lines = pending.split(`
37546
+ `);
37547
+ pending = lines.pop() ?? "";
37548
+ for (const line of lines) {
37549
+ const trimmed = line.trim();
37550
+ if (!trimmed.startsWith("data:"))
37551
+ continue;
37552
+ const payload = trimmed.slice(5).trim();
37553
+ if (!payload || payload === "[DONE]")
37554
+ continue;
37555
+ let event;
37556
+ try {
37557
+ event = JSON.parse(payload);
37558
+ } catch {
37559
+ continue;
37560
+ }
37561
+ const type = String(event?.type ?? "");
37562
+ if (PREAMBLE_EVENTS.has(type))
37563
+ continue;
37564
+ if (type === "error" || type === "response.failed") {
37565
+ const err = event.error ?? event.response?.error ?? {};
37566
+ const code = String(err.code ?? event.code ?? "");
37567
+ const errType = String(err.type ?? event.error_type ?? "");
37568
+ const message = String(err.message ?? event.message ?? "Unknown API error");
37569
+ if (isRetryableStreamError(code, errType, message)) {
37570
+ reader.cancel().catch(() => {});
37571
+ return { kind: "retryable", code: code || errType || "unknown", message };
37572
+ }
37573
+ return { kind: "clean", response: replayResponse() };
37574
+ }
37575
+ return { kind: "clean", response: replayResponse() };
37576
+ }
37577
+ }
37578
+ } catch (error46) {
37579
+ logMsg(`[StreamSniff] read failed (${error46}) \u2014 handing stream to parser`);
37580
+ return { kind: "clean", response: replayResponse() };
37581
+ }
37582
+ }
37583
+ var DEFAULT_SNIFF_BUDGET_MS = 12000, PREAMBLE_EVENTS, RETRYABLE_ERROR_CODES, RETRYABLE_ERROR_TYPES;
37584
+ var init_stream_head_sniffer = __esm(() => {
37585
+ PREAMBLE_EVENTS = new Set(["response.created", "response.in_progress"]);
37586
+ RETRYABLE_ERROR_CODES = new Set([
37587
+ "server_is_overloaded",
37588
+ "server_error",
37589
+ "internal_error",
37590
+ "internal_server_error",
37591
+ "rate_limit_exceeded",
37592
+ "overloaded",
37593
+ "slow_down"
37594
+ ]);
37595
+ RETRYABLE_ERROR_TYPES = new Set([
37596
+ "service_unavailable_error",
37597
+ "overloaded_error",
37598
+ "api_error"
37599
+ ]);
37600
+ });
37601
+
37468
37602
  // src/handlers/shared/stream-parsers/anthropic-sse.ts
37469
37603
  function createAnthropicPassthroughStream(c, response, opts) {
37470
37604
  const encoder = new TextEncoder;
@@ -38130,6 +38264,11 @@ data: ${JSON.stringify(data)}
38130
38264
  `));
38131
38265
  }
38132
38266
  };
38267
+ const safeClose = () => {
38268
+ try {
38269
+ controller.close();
38270
+ } catch {}
38271
+ };
38133
38272
  const closeReasoning = () => {
38134
38273
  if (reasoningIdx >= 0) {
38135
38274
  send("content_block_stop", { type: "content_block_stop", index: reasoningIdx });
@@ -38352,7 +38491,7 @@ data: ${JSON.stringify(data)}
38352
38491
  }
38353
38492
  if (opts.onTokenUpdate)
38354
38493
  opts.onTokenUpdate(inputTokens, outputTokens);
38355
- controller.close();
38494
+ safeClose();
38356
38495
  return;
38357
38496
  }
38358
38497
  } catch (parseError) {
@@ -38380,7 +38519,7 @@ data: ${JSON.stringify(data)}
38380
38519
  isClosed = true;
38381
38520
  if (opts.onTokenUpdate)
38382
38521
  opts.onTokenUpdate(inputTokens, outputTokens);
38383
- controller.close();
38522
+ safeClose();
38384
38523
  } catch (error46) {
38385
38524
  if (pingInterval) {
38386
38525
  clearInterval(pingInterval);
@@ -38416,9 +38555,7 @@ data: ${JSON.stringify(data)}
38416
38555
  isClosed = true;
38417
38556
  if (opts.onTokenUpdate)
38418
38557
  opts.onTokenUpdate(inputTokens, outputTokens);
38419
- try {
38420
- controller.close();
38421
- } catch {}
38558
+ safeClose();
38422
38559
  }
38423
38560
  }
38424
38561
  },
@@ -39031,6 +39168,35 @@ class ComposedHandler {
39031
39168
  if (droppedParams.length > 0) {
39032
39169
  c.header("X-Dropped-Params", droppedParams.join(", "));
39033
39170
  }
39171
+ if (this.resolveStreamFormat() === "openai-responses-sse") {
39172
+ const settled = await this.settleResponsesStreamHead(response, () => this.provider.enqueueRequest ? this.provider.enqueueRequest(doFetch) : doFetch());
39173
+ if (settled.kind === "exhausted") {
39174
+ const waited = STREAM_RETRY_DELAYS_MS.slice(0, settled.attempts).reduce((sum, ms) => sum + ms, 0);
39175
+ const surfaced = `${this.provider.displayName} is overloaded upstream (${settled.code}): ${settled.message} ` + `claudish retried ${settled.attempts}\xD7 over ${Math.round(waited / 1000)}s without success.`;
39176
+ logStderr(`Error: ${surfaced}`);
39177
+ try {
39178
+ recordStats({
39179
+ model_id: this.targetModel,
39180
+ provider_name: this.provider.name,
39181
+ stream_format: this.provider.streamFormat,
39182
+ latency_ms: Math.round(performance.now() - startTime),
39183
+ success: false,
39184
+ http_status: 503,
39185
+ error_class: "server_error",
39186
+ error_code: settled.code,
39187
+ token_strategy: this.options.tokenStrategy ?? "standard",
39188
+ adapter_name: this.getActiveAdapterName(),
39189
+ middleware_names: this.middlewareManager.getActiveNames(this.bareModelName),
39190
+ fallback_used: fallbackMeta !== undefined,
39191
+ fallback_chain: fallbackMeta?.chain,
39192
+ fallback_attempts: fallbackMeta?.attempts,
39193
+ invocation_mode: this.options.invocationMode ?? "auto-route"
39194
+ });
39195
+ } catch {}
39196
+ return c.json(wrapAnthropicError(503, surfaced, "overloaded_error"), 503);
39197
+ }
39198
+ response = settled.response;
39199
+ }
39034
39200
  latencyMs = Math.round(performance.now() - startTime);
39035
39201
  const httpStatus = response.status;
39036
39202
  let streamApiError = null;
@@ -39062,6 +39228,52 @@ class ComposedHandler {
39062
39228
  streamApiError = { code, message };
39063
39229
  });
39064
39230
  }
39231
+ async settleResponsesStreamHead(initial, reissue) {
39232
+ let response = initial;
39233
+ for (let attempt = 0;; attempt++) {
39234
+ const verdict = await sniffResponsesStreamHead(response, { log });
39235
+ if (verdict.kind === "clean")
39236
+ return { kind: "ok", response: verdict.response };
39237
+ const delayMs = STREAM_RETRY_DELAYS_MS[attempt];
39238
+ if (delayMs === undefined) {
39239
+ log(`[${this.provider.displayName}] in-stream ${verdict.code} persisted after ` + `${attempt} retries \u2014 surfacing 503 so the client can retry`);
39240
+ return {
39241
+ kind: "exhausted",
39242
+ code: verdict.code,
39243
+ message: verdict.message,
39244
+ attempts: attempt
39245
+ };
39246
+ }
39247
+ log(`[${this.provider.displayName}] in-stream ${verdict.code} before any output \u2014 ` + `retry ${attempt + 1}/${STREAM_RETRY_DELAYS_MS.length} in ${delayMs / 1000}s`);
39248
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
39249
+ let next;
39250
+ try {
39251
+ next = await reissue();
39252
+ } catch (error46) {
39253
+ log(`[${this.provider.displayName}] retry fetch failed: ${error46}`);
39254
+ return {
39255
+ kind: "exhausted",
39256
+ code: verdict.code,
39257
+ message: `${verdict.message} (retry could not reach the provider: ${error46})`,
39258
+ attempts: attempt + 1
39259
+ };
39260
+ }
39261
+ if (!next.ok) {
39262
+ const body = await next.text().catch(() => "");
39263
+ log(`[${this.provider.displayName}] retry returned HTTP ${next.status}`);
39264
+ return {
39265
+ kind: "exhausted",
39266
+ code: `http_${next.status}`,
39267
+ message: body.slice(0, 500) || `HTTP ${next.status}`,
39268
+ attempts: attempt + 1
39269
+ };
39270
+ }
39271
+ response = next;
39272
+ }
39273
+ }
39274
+ resolveStreamFormat() {
39275
+ return this.provider.overrideStreamFormat?.() ?? this.explicitAdapter?.getStreamFormat() ?? this.modelAdapter?.getStreamFormat() ?? this.getAdapter().getStreamFormat();
39276
+ }
39065
39277
  handleStream(c, response, adapter, claudeRequest, toolNameMap, onComplete, onApiError) {
39066
39278
  let pendingOnComplete = onComplete;
39067
39279
  const onTokenUpdate = (input, output) => {
@@ -39087,7 +39299,7 @@ class ComposedHandler {
39087
39299
  pendingOnComplete = undefined;
39088
39300
  }
39089
39301
  };
39090
- const streamFormat = this.provider.overrideStreamFormat?.() ?? this.explicitAdapter?.getStreamFormat() ?? this.modelAdapter?.getStreamFormat() ?? this.getAdapter().getStreamFormat();
39302
+ const streamFormat = this.resolveStreamFormat();
39091
39303
  const priorInputTokens = this.tokenTracker.getLastInputTokens();
39092
39304
  switch (streamFormat) {
39093
39305
  case "openai-sse":
@@ -39191,6 +39403,7 @@ function getRecoveryHint(status, errorText, providerName) {
39191
39403
  }
39192
39404
  return `Unexpected HTTP ${status} from ${providerName}.`;
39193
39405
  }
39406
+ var STREAM_RETRY_DELAYS_MS;
39194
39407
  var init_composed_handler = __esm(() => {
39195
39408
  init_dialect_manager();
39196
39409
  init_logger();
@@ -39203,6 +39416,7 @@ var init_composed_handler = __esm(() => {
39203
39416
  init_anthropic_error();
39204
39417
  init_connection_error();
39205
39418
  init_openai_compat();
39419
+ init_stream_head_sniffer();
39206
39420
  init_anthropic_sse();
39207
39421
  init_gemini_sse();
39208
39422
  init_ollama_jsonl();
@@ -39210,6 +39424,7 @@ var init_composed_handler = __esm(() => {
39210
39424
  init_openai_responses_sse();
39211
39425
  init_openai_sse();
39212
39426
  init_token_tracker();
39427
+ STREAM_RETRY_DELAYS_MS = [3000, 15000, 30000];
39213
39428
  });
39214
39429
 
39215
39430
  // src/handlers/fallback-handler.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudish",
3
- "version": "7.21.0",
3
+ "version": "7.22.0",
4
4
  "description": "Run Claude Code with any model - OpenRouter, Ollama, LM Studio & local models",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -60,10 +60,10 @@
60
60
  "ai"
61
61
  ],
62
62
  "optionalDependencies": {
63
- "@claudish/magmux-darwin-arm64": "7.21.0",
64
- "@claudish/magmux-darwin-x64": "7.21.0",
65
- "@claudish/magmux-linux-arm64": "7.21.0",
66
- "@claudish/magmux-linux-x64": "7.21.0"
63
+ "@claudish/magmux-darwin-arm64": "7.22.0",
64
+ "@claudish/magmux-darwin-x64": "7.22.0",
65
+ "@claudish/magmux-linux-arm64": "7.22.0",
66
+ "@claudish/magmux-linux-x64": "7.22.0"
67
67
  },
68
68
  "author": "Jack Rudenko <i@madappgang.com>",
69
69
  "license": "MIT",