claudish 3.7.8 → 3.8.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 +482 -27
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -32119,6 +32119,250 @@ var init_openai_compat = __esm(() => {
32119
32119
  init_tool_call_recovery();
32120
32120
  });
32121
32121
 
32122
+ // ../core/dist/handlers/shared/openrouter-queue.js
32123
+ class OpenRouterRequestQueue {
32124
+ static instance = null;
32125
+ queue = [];
32126
+ processing = false;
32127
+ rateLimitState = {
32128
+ limitRequests: null,
32129
+ limitTokens: null,
32130
+ remainingRequests: null,
32131
+ remainingTokens: null,
32132
+ resetTime: null,
32133
+ lastRequestTime: 0,
32134
+ consecutiveErrors: 0,
32135
+ currentDelayMs: 1000,
32136
+ totalProcessed: 0,
32137
+ totalErrors: 0,
32138
+ total429Errors: 0
32139
+ };
32140
+ baseDelayMs = 1000;
32141
+ maxDelayMs = 1e4;
32142
+ maxQueueSize = 100;
32143
+ constructor() {
32144
+ if (getLogLevel() === "debug") {
32145
+ log("[OpenRouterQueue] Queue initialized with baseDelay=1000ms, maxQueueSize=100");
32146
+ }
32147
+ }
32148
+ static getInstance() {
32149
+ if (!OpenRouterRequestQueue.instance) {
32150
+ OpenRouterRequestQueue.instance = new OpenRouterRequestQueue;
32151
+ }
32152
+ return OpenRouterRequestQueue.instance;
32153
+ }
32154
+ async enqueue(fetchFn) {
32155
+ if (this.queue.length >= this.maxQueueSize) {
32156
+ if (getLogLevel() === "debug") {
32157
+ log(`[OpenRouterQueue] Queue full (${this.queue.length}/${this.maxQueueSize}), rejecting request`);
32158
+ }
32159
+ throw new Error(`OpenRouter request queue full (${this.queue.length}/${this.maxQueueSize}). The API is rate-limited. Please wait and try again.`);
32160
+ }
32161
+ return new Promise((resolve, reject) => {
32162
+ const queuedRequest = {
32163
+ fetchFn,
32164
+ resolve,
32165
+ reject
32166
+ };
32167
+ this.queue.push(queuedRequest);
32168
+ if (getLogLevel() === "debug") {
32169
+ log(`[OpenRouterQueue] Request enqueued (queue length: ${this.queue.length})`);
32170
+ }
32171
+ if (!this.processing) {
32172
+ this.processQueue();
32173
+ }
32174
+ });
32175
+ }
32176
+ async processQueue() {
32177
+ if (this.processing) {
32178
+ return;
32179
+ }
32180
+ this.processing = true;
32181
+ if (getLogLevel() === "debug") {
32182
+ log("[OpenRouterQueue] Worker started");
32183
+ }
32184
+ while (this.queue.length > 0) {
32185
+ const request = this.queue.shift();
32186
+ if (!request)
32187
+ break;
32188
+ if (getLogLevel() === "debug") {
32189
+ log(`[OpenRouterQueue] Processing request (${this.queue.length} remaining in queue)`);
32190
+ }
32191
+ try {
32192
+ await this.waitForNextSlot();
32193
+ const response = await request.fetchFn();
32194
+ this.rateLimitState.lastRequestTime = Date.now();
32195
+ this.parseRateLimitHeaders(response);
32196
+ if (response.status === 429) {
32197
+ this.rateLimitState.totalErrors++;
32198
+ this.rateLimitState.total429Errors++;
32199
+ await this.handleRateLimitError(response);
32200
+ if (getLogLevel() === "debug") {
32201
+ log(`[OpenRouterQueue] Rate limit hit (429), adjusted delay to ${this.rateLimitState.currentDelayMs}ms`);
32202
+ }
32203
+ } else {
32204
+ this.handleSuccessResponse();
32205
+ }
32206
+ this.rateLimitState.totalProcessed++;
32207
+ request.resolve(response);
32208
+ } catch (error46) {
32209
+ this.rateLimitState.totalErrors++;
32210
+ this.rateLimitState.consecutiveErrors++;
32211
+ if (getLogLevel() === "debug") {
32212
+ log(`[OpenRouterQueue] Request failed with error: ${error46}`);
32213
+ }
32214
+ request.reject(error46 instanceof Error ? error46 : new Error(String(error46)));
32215
+ }
32216
+ }
32217
+ this.processing = false;
32218
+ if (getLogLevel() === "debug") {
32219
+ log("[OpenRouterQueue] Worker stopped (queue empty)");
32220
+ }
32221
+ }
32222
+ async waitForNextSlot() {
32223
+ const now = Date.now();
32224
+ const timeSinceLastRequest = now - this.rateLimitState.lastRequestTime;
32225
+ const delayMs = this.calculateDelay();
32226
+ this.rateLimitState.currentDelayMs = delayMs;
32227
+ if (timeSinceLastRequest < delayMs) {
32228
+ const waitMs = delayMs - timeSinceLastRequest;
32229
+ if (getLogLevel() === "debug") {
32230
+ log(`[OpenRouterQueue] Waiting ${waitMs}ms before next request`);
32231
+ }
32232
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
32233
+ }
32234
+ }
32235
+ calculateDelay() {
32236
+ let delayMs = this.baseDelayMs;
32237
+ if (this.rateLimitState.remainingRequests !== null && this.rateLimitState.limitRequests !== null && this.rateLimitState.limitRequests > 0) {
32238
+ const quotaPercent = this.rateLimitState.remainingRequests / this.rateLimitState.limitRequests;
32239
+ if (quotaPercent < 0.2) {
32240
+ delayMs = Math.max(delayMs, 3000);
32241
+ if (getLogLevel() === "debug") {
32242
+ log(`[OpenRouterQueue] Low quota (${(quotaPercent * 100).toFixed(1)}%), increasing delay to ${delayMs}ms`);
32243
+ }
32244
+ } else if (quotaPercent < 0.5) {
32245
+ delayMs = Math.max(delayMs, 2000);
32246
+ if (getLogLevel() === "debug") {
32247
+ log(`[OpenRouterQueue] Medium quota (${(quotaPercent * 100).toFixed(1)}%), increasing delay to ${delayMs}ms`);
32248
+ }
32249
+ }
32250
+ }
32251
+ if (this.rateLimitState.resetTime !== null && this.rateLimitState.remainingRequests !== null) {
32252
+ const now = Date.now() / 1000;
32253
+ const timeUntilReset = this.rateLimitState.resetTime - now;
32254
+ if (timeUntilReset > 0 && this.rateLimitState.remainingRequests > 0) {
32255
+ const optimalDelay = timeUntilReset * 1000 / Math.max(this.rateLimitState.remainingRequests, 1);
32256
+ delayMs = Math.max(delayMs, Math.min(optimalDelay, this.maxDelayMs));
32257
+ if (getLogLevel() === "debug") {
32258
+ log(`[OpenRouterQueue] Spreading ${this.rateLimitState.remainingRequests} requests ` + `over ${timeUntilReset.toFixed(1)}s, optimal delay: ${optimalDelay.toFixed(0)}ms`);
32259
+ }
32260
+ }
32261
+ }
32262
+ if (this.rateLimitState.consecutiveErrors > 0) {
32263
+ const backoffMultiplier = 1 + this.rateLimitState.consecutiveErrors * 0.5;
32264
+ delayMs = delayMs * backoffMultiplier;
32265
+ if (getLogLevel() === "debug") {
32266
+ log(`[OpenRouterQueue] Applying backoff (${this.rateLimitState.consecutiveErrors} errors): ${delayMs.toFixed(0)}ms`);
32267
+ }
32268
+ }
32269
+ return Math.min(delayMs, this.maxDelayMs);
32270
+ }
32271
+ parseRateLimitHeaders(response) {
32272
+ const limitRequests = response.headers.get("X-RateLimit-Limit-Requests");
32273
+ if (limitRequests) {
32274
+ this.rateLimitState.limitRequests = Number.parseInt(limitRequests, 10);
32275
+ }
32276
+ const remainingRequests = response.headers.get("X-RateLimit-Remaining-Requests");
32277
+ if (remainingRequests) {
32278
+ this.rateLimitState.remainingRequests = Number.parseInt(remainingRequests, 10);
32279
+ }
32280
+ const resetRequests = response.headers.get("X-RateLimit-Reset-Requests");
32281
+ if (resetRequests) {
32282
+ this.rateLimitState.resetTime = Number.parseFloat(resetRequests);
32283
+ }
32284
+ const limitTokens = response.headers.get("X-RateLimit-Limit-Tokens");
32285
+ if (limitTokens) {
32286
+ this.rateLimitState.limitTokens = Number.parseInt(limitTokens, 10);
32287
+ }
32288
+ const remainingTokens = response.headers.get("X-RateLimit-Remaining-Tokens");
32289
+ if (remainingTokens) {
32290
+ this.rateLimitState.remainingTokens = Number.parseInt(remainingTokens, 10);
32291
+ }
32292
+ if (getLogLevel() === "debug") {
32293
+ const headers = {
32294
+ limitRequests: this.rateLimitState.limitRequests,
32295
+ remainingRequests: this.rateLimitState.remainingRequests,
32296
+ resetTime: this.rateLimitState.resetTime ? new Date(this.rateLimitState.resetTime * 1000).toISOString() : null,
32297
+ limitTokens: this.rateLimitState.limitTokens,
32298
+ remainingTokens: this.rateLimitState.remainingTokens
32299
+ };
32300
+ log(`[OpenRouterQueue] Rate limit headers: ${JSON.stringify(headers)}`);
32301
+ }
32302
+ }
32303
+ async handleRateLimitError(response) {
32304
+ this.rateLimitState.consecutiveErrors++;
32305
+ this.rateLimitState.remainingRequests = 0;
32306
+ const retryAfter = response.headers.get("Retry-After");
32307
+ if (retryAfter) {
32308
+ const retryAfterSeconds = Number.parseInt(retryAfter, 10);
32309
+ if (!Number.isNaN(retryAfterSeconds)) {
32310
+ const retryAfterMs = retryAfterSeconds * 1000;
32311
+ this.rateLimitState.currentDelayMs = Math.min(retryAfterMs, this.maxDelayMs);
32312
+ if (getLogLevel() === "debug") {
32313
+ log(`[OpenRouterQueue] Retry-After header: ${retryAfterSeconds}s (${retryAfterMs}ms)`);
32314
+ }
32315
+ }
32316
+ }
32317
+ try {
32318
+ const errorText = await response.clone().text();
32319
+ const errorData = JSON.parse(errorText);
32320
+ if (errorData?.error?.message) {
32321
+ if (getLogLevel() === "debug") {
32322
+ log(`[OpenRouterQueue] 429 error message: ${errorData.error.message}`);
32323
+ }
32324
+ }
32325
+ } catch {}
32326
+ const backoffMultiplier = 1 + this.rateLimitState.consecutiveErrors * 0.5;
32327
+ const backoffDelay = Math.min(this.baseDelayMs * backoffMultiplier, this.maxDelayMs);
32328
+ this.rateLimitState.currentDelayMs = Math.max(this.rateLimitState.currentDelayMs, backoffDelay);
32329
+ if (getLogLevel() === "debug") {
32330
+ log(`[OpenRouterQueue] Applied exponential backoff: ${this.rateLimitState.currentDelayMs}ms ` + `(${this.rateLimitState.consecutiveErrors} consecutive errors)`);
32331
+ }
32332
+ }
32333
+ handleSuccessResponse() {
32334
+ if (this.rateLimitState.consecutiveErrors > 0) {
32335
+ if (getLogLevel() === "debug") {
32336
+ log(`[OpenRouterQueue] Success after ${this.rateLimitState.consecutiveErrors} errors, resetting counter`);
32337
+ }
32338
+ this.rateLimitState.consecutiveErrors = 0;
32339
+ }
32340
+ if (this.rateLimitState.currentDelayMs > this.baseDelayMs) {
32341
+ this.rateLimitState.currentDelayMs = Math.max(this.baseDelayMs, this.rateLimitState.currentDelayMs * 0.9);
32342
+ if (getLogLevel() === "debug") {
32343
+ log(`[OpenRouterQueue] Reducing delay to ${this.rateLimitState.currentDelayMs}ms`);
32344
+ }
32345
+ }
32346
+ }
32347
+ getStats() {
32348
+ return {
32349
+ queueLength: this.queue.length,
32350
+ processing: this.processing,
32351
+ consecutiveErrors: this.rateLimitState.consecutiveErrors,
32352
+ currentDelayMs: this.rateLimitState.currentDelayMs,
32353
+ totalProcessed: this.rateLimitState.totalProcessed,
32354
+ totalErrors: this.rateLimitState.totalErrors,
32355
+ total429Errors: this.rateLimitState.total429Errors,
32356
+ remainingRequests: this.rateLimitState.remainingRequests,
32357
+ remainingTokens: this.rateLimitState.remainingTokens,
32358
+ resetTime: this.rateLimitState.resetTime
32359
+ };
32360
+ }
32361
+ }
32362
+ var init_openrouter_queue = __esm(() => {
32363
+ init_logger();
32364
+ });
32365
+
32122
32366
  // ../core/dist/handlers/openrouter-handler.js
32123
32367
  import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync2 } from "node:fs";
32124
32368
  import { homedir } from "node:os";
@@ -32133,6 +32377,7 @@ class OpenRouterHandler {
32133
32377
  port;
32134
32378
  sessionTotalCost = 0;
32135
32379
  CLAUDE_INTERNAL_CONTEXT_MAX = 200000;
32380
+ queue;
32136
32381
  constructor(targetModel, apiKey, port) {
32137
32382
  this.targetModel = targetModel;
32138
32383
  this.apiKey = apiKey;
@@ -32142,6 +32387,7 @@ class OpenRouterHandler {
32142
32387
  this.middlewareManager.register(new GeminiThoughtSignatureMiddleware);
32143
32388
  this.middlewareManager.initialize().catch((err) => log(`[Handler:${targetModel}] Middleware init error: ${err}`));
32144
32389
  this.fetchContextWindow(targetModel);
32390
+ this.queue = OpenRouterRequestQueue.getInstance();
32145
32391
  }
32146
32392
  async fetchContextWindow(model) {
32147
32393
  if (this.contextWindowCache.has(model))
@@ -32161,6 +32407,7 @@ class OpenRouterHandler {
32161
32407
  const limit = this.contextWindowCache.get(this.targetModel) || 200000;
32162
32408
  const leftPct = limit > 0 ? Math.max(0, Math.min(100, Math.round((limit - total) / limit * 100))) : 100;
32163
32409
  const displayModelName = this.targetModel.replace(/^(go|g|gemini|v|vertex|oai|mmax|mm|kimi|moonshot|glm|zhipu|oc|ollama|lmstudio|vllm|mlx)[\/:]/, "");
32410
+ const isFreeModel = this.targetModel.endsWith(":free") || this.sessionTotalCost === 0;
32164
32411
  const data = {
32165
32412
  input_tokens: input,
32166
32413
  output_tokens: output,
@@ -32170,7 +32417,8 @@ class OpenRouterHandler {
32170
32417
  context_left_percent: leftPct,
32171
32418
  provider_name: "OpenRouter",
32172
32419
  model_name: displayModelName,
32173
- updated_at: Date.now()
32420
+ updated_at: Date.now(),
32421
+ is_free: isFreeModel
32174
32422
  };
32175
32423
  const claudishDir = join4(homedir(), ".claudish");
32176
32424
  mkdirSync2(claudishDir, { recursive: true });
@@ -32232,7 +32480,7 @@ class OpenRouterHandler {
32232
32480
  await this.middlewareManager.beforeRequest({ modelId: target, messages, tools, stream: true });
32233
32481
  let response;
32234
32482
  try {
32235
- response = await fetch(OPENROUTER_API_URL2, {
32483
+ response = await this.queue.enqueue(() => fetch(OPENROUTER_API_URL2, {
32236
32484
  method: "POST",
32237
32485
  headers: {
32238
32486
  "Content-Type": "application/json",
@@ -32240,16 +32488,26 @@ class OpenRouterHandler {
32240
32488
  ...OPENROUTER_HEADERS2
32241
32489
  },
32242
32490
  body: JSON.stringify(openRouterPayload)
32243
- });
32491
+ }));
32244
32492
  } catch (fetchError) {
32245
32493
  log(`[OpenRouter] Fetch error: ${fetchError.message || fetchError}`);
32246
32494
  return this.createStreamingErrorResponse(c, target, `Network error: ${fetchError.message || "Connection failed"}`);
32247
32495
  }
32248
32496
  log(`[OpenRouter] Response status: ${response.status}`);
32497
+ if (getLogLevel() === "debug") {
32498
+ const rateLimitHeaders = {
32499
+ remaining: response.headers.get("X-RateLimit-Remaining-Requests"),
32500
+ limit: response.headers.get("X-RateLimit-Limit-Requests"),
32501
+ reset: response.headers.get("X-RateLimit-Reset-Requests"),
32502
+ remainingTokens: response.headers.get("X-RateLimit-Remaining-Tokens")
32503
+ };
32504
+ log(`[OpenRouter] Rate limit headers: ${JSON.stringify(rateLimitHeaders)}`);
32505
+ }
32249
32506
  if (!response.ok) {
32250
32507
  const errorText = await response.text().catch(() => "Unknown error");
32251
32508
  log(`[OpenRouter] API error ${response.status}: ${errorText}`);
32252
- return this.createStreamingErrorResponse(c, target, `OpenRouter API error (${response.status}): ${errorText}`);
32509
+ const friendlyMessage = this.formatErrorMessage(response.status, errorText, target);
32510
+ return this.createStreamingErrorResponse(c, target, friendlyMessage);
32253
32511
  }
32254
32512
  if (droppedParams.length > 0)
32255
32513
  c.header("X-Dropped-Params", droppedParams.join(", "));
@@ -32677,6 +32935,36 @@ data: ${JSON.stringify(d)}
32677
32935
  }
32678
32936
  });
32679
32937
  }
32938
+ formatErrorMessage(status, errorText, model) {
32939
+ try {
32940
+ const error46 = JSON.parse(errorText);
32941
+ const msg = error46?.error?.message || "";
32942
+ const metadata = error46?.error?.metadata || {};
32943
+ if (status === 429) {
32944
+ const provider = metadata.provider_name || "Provider";
32945
+ if (msg.includes("rate-limited")) {
32946
+ return `⏳ Rate limited by ${provider}. The model "${model}" is temporarily unavailable. Please wait a moment and try again, or try a different model.`;
32947
+ }
32948
+ return `⏳ Rate limit exceeded. Please wait and try again.`;
32949
+ }
32950
+ if (status === 404) {
32951
+ if (msg.includes("tool use")) {
32952
+ return `❌ Model "${model}" does not support tool calling, which is required by Claude Code. Try a different model with tool support.`;
32953
+ }
32954
+ return `❌ Model not found or unavailable: ${model}`;
32955
+ }
32956
+ if (status === 401 || status === 403) {
32957
+ return `\uD83D\uDD11 Authentication error. Please check your OPENROUTER_API_KEY.`;
32958
+ }
32959
+ if (status >= 500) {
32960
+ return `⚠️ OpenRouter server error (${status}). Please try again later.`;
32961
+ }
32962
+ if (msg) {
32963
+ return `OpenRouter error (${status}): ${msg}`;
32964
+ }
32965
+ } catch {}
32966
+ return `OpenRouter API error (${status}): ${errorText}`;
32967
+ }
32680
32968
  createStreamingErrorResponse(c, model, errorMessage) {
32681
32969
  const encoder = new TextEncoder;
32682
32970
  const msgId = `msg_${Date.now()}_${Math.random().toString(36).slice(2)}`;
@@ -32744,6 +33032,7 @@ var init_openrouter_handler = __esm(() => {
32744
33032
  init_logger();
32745
33033
  init_model_loader();
32746
33034
  init_openai_compat();
33035
+ init_openrouter_queue();
32747
33036
  OPENROUTER_HEADERS2 = {
32748
33037
  "HTTP-Referer": "https://claudish.com",
32749
33038
  "X-Title": "Claudish - OpenRouter Proxy"
@@ -53726,6 +54015,10 @@ function getModelPricing(provider, modelName) {
53726
54015
  case "oc":
53727
54016
  pricingTable = OLLAMACLOUD_PRICING;
53728
54017
  break;
54018
+ case "opencode-zen":
54019
+ case "zen":
54020
+ pricingTable = OPENCODE_ZEN_PRICING;
54021
+ break;
53729
54022
  default:
53730
54023
  return { inputCostPer1M: 1, outputCostPer1M: 4 };
53731
54024
  }
@@ -53745,7 +54038,7 @@ function calculateCost(provider, modelName, inputTokens, outputTokens) {
53745
54038
  const outputCost = outputTokens / 1e6 * pricing.outputCostPer1M;
53746
54039
  return inputCost + outputCost;
53747
54040
  }
53748
- var GEMINI_PRICING, OPENAI_PRICING, MINIMAX_PRICING, KIMI_PRICING, GLM_PRICING, OLLAMACLOUD_PRICING, VERTEX_PRICING;
54041
+ var GEMINI_PRICING, OPENAI_PRICING, MINIMAX_PRICING, KIMI_PRICING, GLM_PRICING, OLLAMACLOUD_PRICING, VERTEX_PRICING, OPENCODE_ZEN_PRICING;
53749
54042
  var init_remote_provider_types = __esm(() => {
53750
54043
  GEMINI_PRICING = {
53751
54044
  "gemini-2.5-flash": { inputCostPer1M: 0.15, outputCostPer1M: 0.6 },
@@ -53834,6 +54127,15 @@ var init_remote_provider_types = __esm(() => {
53834
54127
  "gemini-2.0-flash-thinking": { inputCostPer1M: 0.1, outputCostPer1M: 0.4 },
53835
54128
  default: { inputCostPer1M: 0.5, outputCostPer1M: 2, isEstimate: true }
53836
54129
  };
54130
+ OPENCODE_ZEN_PRICING = {
54131
+ "grok-code": { inputCostPer1M: 0, outputCostPer1M: 0, isFree: true },
54132
+ "grok-code-fast-1": { inputCostPer1M: 0, outputCostPer1M: 0, isFree: true },
54133
+ "glm-4.7-free": { inputCostPer1M: 0, outputCostPer1M: 0, isFree: true },
54134
+ "minimax-m2.1-free": { inputCostPer1M: 0, outputCostPer1M: 0, isFree: true },
54135
+ "big-pickle": { inputCostPer1M: 0, outputCostPer1M: 0, isFree: true },
54136
+ "gpt-5-nano": { inputCostPer1M: 0, outputCostPer1M: 0, isFree: true },
54137
+ default: { inputCostPer1M: 0, outputCostPer1M: 0, isFree: true }
54138
+ };
53837
54139
  });
53838
54140
 
53839
54141
  // ../core/dist/handlers/base-gemini-handler.js
@@ -55344,7 +55646,7 @@ class OpenAIHandler {
55344
55646
  }
55345
55647
  }
55346
55648
  getPricing() {
55347
- return getModelPricing("openai", this.modelName);
55649
+ return getModelPricing(this.provider.name, this.modelName);
55348
55650
  }
55349
55651
  getApiEndpoint() {
55350
55652
  if (this.isCodexModel()) {
@@ -55356,7 +55658,16 @@ class OpenAIHandler {
55356
55658
  try {
55357
55659
  const total = input + output;
55358
55660
  const leftPct = this.contextWindow > 0 ? Math.max(0, Math.min(100, Math.round((this.contextWindow - total) / this.contextWindow * 100))) : 100;
55359
- const displayModelName = this.modelName.replace(/^(go|g|gemini|v|vertex|oai|mmax|mm|kimi|moonshot|glm|zhipu|oc|ollama|lmstudio|vllm|mlx)[\/:]/, "");
55661
+ const displayModelName = this.modelName.replace(/^(go|g|gemini|v|vertex|oai|mmax|mm|kimi|moonshot|glm|zhipu|oc|zen|ollama|lmstudio|vllm|mlx)[\/:]/, "");
55662
+ const formatProviderName = (name) => {
55663
+ if (name === "opencode-zen")
55664
+ return "Zen";
55665
+ if (name === "glm")
55666
+ return "GLM";
55667
+ return name.charAt(0).toUpperCase() + name.slice(1);
55668
+ };
55669
+ const pricing = this.getPricing();
55670
+ const isFreeModel = pricing.isFree || pricing.inputCostPer1M === 0 && pricing.outputCostPer1M === 0;
55360
55671
  const data = {
55361
55672
  input_tokens: input,
55362
55673
  output_tokens: output,
@@ -55364,13 +55675,12 @@ class OpenAIHandler {
55364
55675
  total_cost: this.sessionTotalCost,
55365
55676
  context_window: this.contextWindow,
55366
55677
  context_left_percent: leftPct,
55367
- provider_name: "OpenAI",
55678
+ provider_name: formatProviderName(this.provider.name),
55368
55679
  model_name: displayModelName,
55369
- updated_at: Date.now()
55680
+ updated_at: Date.now(),
55681
+ is_free: isFreeModel,
55682
+ is_estimated: isEstimate || false
55370
55683
  };
55371
- if (isEstimate) {
55372
- data.cost_is_estimate = true;
55373
- }
55374
55684
  const claudishDir = join9(homedir6(), ".claudish");
55375
55685
  mkdirSync6(claudishDir, { recursive: true });
55376
55686
  writeFileSync7(join9(claudishDir, `tokens-${this.port}.json`), JSON.stringify(data), "utf-8");
@@ -55831,7 +56141,7 @@ data: ${JSON.stringify(data)}
55831
56141
  method: "POST",
55832
56142
  headers: {
55833
56143
  "Content-Type": "application/json",
55834
- Authorization: `Bearer ${this.apiKey}`
56144
+ ...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}
55835
56145
  },
55836
56146
  body: JSON.stringify(apiPayload),
55837
56147
  signal: controller.signal
@@ -58121,7 +58431,8 @@ function validateRemoteProviderApiKey(provider) {
58121
58431
  MINIMAX_API_KEY: "export MINIMAX_API_KEY='your-key' (get from https://www.minimaxi.com/)",
58122
58432
  MOONSHOT_API_KEY: "export MOONSHOT_API_KEY='your-key' (get from https://platform.moonshot.cn/)",
58123
58433
  ZHIPU_API_KEY: "export ZHIPU_API_KEY='your-key' (get from https://open.bigmodel.cn/)",
58124
- OLLAMA_API_KEY: "export OLLAMA_API_KEY='your-key' (get from https://ollama.com/account)"
58434
+ OLLAMA_API_KEY: "export OLLAMA_API_KEY='your-key' (get from https://ollama.com/account)",
58435
+ OPENCODE_API_KEY: "export OPENCODE_API_KEY='your-key' (get from https://opencode.ai/)"
58125
58436
  };
58126
58437
  const example = examples[provider.apiKeyEnvVar] || `export ${provider.apiKeyEnvVar}='your-key'`;
58127
58438
  return `Missing ${provider.apiKeyEnvVar} environment variable.
@@ -58250,6 +58561,20 @@ var getRemoteProviders = () => [
58250
58561
  supportsJsonMode: false,
58251
58562
  supportsReasoning: false
58252
58563
  }
58564
+ },
58565
+ {
58566
+ name: "opencode-zen",
58567
+ baseUrl: process.env.OPENCODE_BASE_URL || "https://opencode.ai/zen",
58568
+ apiPath: "/v1/chat/completions",
58569
+ apiKeyEnvVar: "",
58570
+ prefixes: ["zen/"],
58571
+ capabilities: {
58572
+ supportsTools: true,
58573
+ supportsVision: false,
58574
+ supportsStreaming: true,
58575
+ supportsJsonMode: true,
58576
+ supportsReasoning: false
58577
+ }
58253
58578
  }
58254
58579
  ];
58255
58580
 
@@ -58319,7 +58644,7 @@ async function createProxyServer(port, openrouterApiKey, model, monitorMode = fa
58319
58644
  if (apiKeyError) {
58320
58645
  throw new Error(apiKeyError);
58321
58646
  }
58322
- const apiKey = process.env[resolved.provider.apiKeyEnvVar];
58647
+ const apiKey = resolved.provider.apiKeyEnvVar ? process.env[resolved.provider.apiKeyEnvVar] || "" : "";
58323
58648
  let handler;
58324
58649
  if (resolved.provider.name === "gemini") {
58325
58650
  handler = new GeminiHandler(resolved.provider, resolved.modelName, apiKey, port);
@@ -58336,6 +58661,14 @@ async function createProxyServer(port, openrouterApiKey, model, monitorMode = fa
58336
58661
  } else if (resolved.provider.name === "glm") {
58337
58662
  handler = new OpenAIHandler(resolved.provider, resolved.modelName, apiKey, port);
58338
58663
  log(`[Proxy] Created ${resolved.provider.name} handler: ${resolved.modelName}`);
58664
+ } else if (resolved.provider.name === "opencode-zen") {
58665
+ if (resolved.modelName.toLowerCase().includes("minimax")) {
58666
+ handler = new AnthropicCompatHandler(resolved.provider, resolved.modelName, apiKey, port);
58667
+ log(`[Proxy] Created OpenCode Zen (Anthropic) handler: ${resolved.modelName}`);
58668
+ } else {
58669
+ handler = new OpenAIHandler(resolved.provider, resolved.modelName, apiKey, port);
58670
+ log(`[Proxy] Created OpenCode Zen (OpenAI) handler: ${resolved.modelName}`);
58671
+ }
58339
58672
  } else if (resolved.provider.name === "ollamacloud") {
58340
58673
  handler = new OllamaCloudHandler(resolved.provider, resolved.modelName, apiKey, port);
58341
58674
  log(`[Proxy] Created OllamaCloud handler: ${resolved.modelName}`);
@@ -60899,6 +61232,40 @@ __export(exports_model_selector, {
60899
61232
  import { readFileSync as readFileSync5, writeFileSync as writeFileSync12, existsSync as existsSync7 } from "node:fs";
60900
61233
  import { join as join15, dirname as dirname3 } from "node:path";
60901
61234
  import { fileURLToPath as fileURLToPath3 } from "node:url";
61235
+ async function fetchZenModels() {
61236
+ try {
61237
+ const response = await fetch("https://models.dev/api.json", {
61238
+ signal: AbortSignal.timeout(5000)
61239
+ });
61240
+ if (!response.ok)
61241
+ return [];
61242
+ const data = await response.json();
61243
+ const opencode = data.opencode;
61244
+ if (!opencode?.models)
61245
+ return [];
61246
+ return Object.entries(opencode.models).map(([id, m]) => {
61247
+ const isFree = m.cost?.input === 0 && m.cost?.output === 0;
61248
+ return {
61249
+ id: `zen/${id}`,
61250
+ name: m.name || id,
61251
+ description: isFree ? "OpenCode Zen - FREE (no API key needed)" : `OpenCode Zen - $${((m.cost?.input || 0) + (m.cost?.output || 0)).toFixed(1)}/M`,
61252
+ provider: "zen",
61253
+ pricing: { input: String(m.cost?.input || 0), output: String(m.cost?.output || 0), average: isFree ? "FREE" : `$${((m.cost?.input || 0) + (m.cost?.output || 0)).toFixed(1)}/M` },
61254
+ context: m.limit?.context ? `${Math.round(m.limit.context / 1000)}K` : "128K",
61255
+ contextLength: m.limit?.context || 128000,
61256
+ supportsTools: m.tool_call || false,
61257
+ supportsReasoning: m.reasoning || false,
61258
+ isFree
61259
+ };
61260
+ });
61261
+ } catch {
61262
+ return [];
61263
+ }
61264
+ }
61265
+ async function fetchZenFreeModels() {
61266
+ const allZen = await fetchZenModels();
61267
+ return allZen.filter((m) => m.isFree);
61268
+ }
60902
61269
  function loadRecommendedModels2() {
60903
61270
  if (existsSync7(RECOMMENDED_MODELS_JSON_PATH)) {
60904
61271
  try {
@@ -60977,13 +61344,19 @@ function toModelInfo(model) {
60977
61344
  };
60978
61345
  }
60979
61346
  async function getFreeModels() {
60980
- const allModels = await fetchAllModels();
61347
+ const [allModels, zenModels] = await Promise.all([
61348
+ fetchAllModels(),
61349
+ fetchZenFreeModels()
61350
+ ]);
60981
61351
  const freeModels = allModels.filter((model) => {
60982
61352
  const promptPrice = parseFloat(model.pricing?.prompt || "0");
60983
61353
  const completionPrice = parseFloat(model.pricing?.completion || "0");
60984
61354
  const isFree = promptPrice === 0 && completionPrice === 0;
60985
61355
  if (!isFree)
60986
61356
  return false;
61357
+ const supportsTools = (model.supported_parameters || []).includes("tools");
61358
+ if (!supportsTools)
61359
+ return false;
60987
61360
  const provider = model.id.split("/")[0].toLowerCase();
60988
61361
  return TRUSTED_FREE_PROVIDERS.includes(provider);
60989
61362
  });
@@ -61000,7 +61373,8 @@ async function getFreeModels() {
61000
61373
  seenBase.add(baseId);
61001
61374
  return true;
61002
61375
  });
61003
- return dedupedModels.slice(0, 20).map(toModelInfo);
61376
+ const openRouterFree = dedupedModels.slice(0, 15).map(toModelInfo);
61377
+ return [...zenModels, ...openRouterFree];
61004
61378
  }
61005
61379
  async function getAllModelsForSearch() {
61006
61380
  const allModels = await fetchAllModels();
@@ -61191,7 +61565,8 @@ var init_model_selector = __esm(() => {
61191
61565
  "microsoft",
61192
61566
  "mistralai",
61193
61567
  "nvidia",
61194
- "cohere"
61568
+ "cohere",
61569
+ "zen"
61195
61570
  ];
61196
61571
  });
61197
61572
 
@@ -61812,8 +62187,15 @@ async function parseArgs(args) {
61812
62187
  console.log("[claudish] Ensure you are logged in to Claude Code (claude auth login)");
61813
62188
  }
61814
62189
  } else {
61815
- const usingLocalModel = isLocalModel(config3.model);
61816
- if (!usingLocalModel) {
62190
+ const allModels = [
62191
+ config3.model,
62192
+ config3.modelOpus,
62193
+ config3.modelSonnet,
62194
+ config3.modelHaiku,
62195
+ config3.modelSubagent
62196
+ ];
62197
+ const hasNonLocalModel = allModels.some((m) => m && !isLocalModel(m));
62198
+ if (hasNonLocalModel) {
61817
62199
  const apiKey = process.env[ENV.OPENROUTER_API_KEY];
61818
62200
  if (!apiKey) {
61819
62201
  if (!config3.interactive) {
@@ -62003,7 +62385,10 @@ Found ${results.length} matching models:
62003
62385
  }
62004
62386
  async function printAllModels(jsonOutput, forceUpdate) {
62005
62387
  let models = [];
62006
- const ollamaModels = await fetchOllamaModels();
62388
+ const [ollamaModels, zenModels] = await Promise.all([
62389
+ fetchOllamaModels(),
62390
+ fetchZenModels2()
62391
+ ]);
62007
62392
  if (!forceUpdate && existsSync9(ALL_MODELS_JSON_PATH2)) {
62008
62393
  try {
62009
62394
  const cacheData = JSON.parse(readFileSync7(ALL_MODELS_JSON_PATH2, "utf-8"));
@@ -62037,17 +62422,19 @@ async function printAllModels(jsonOutput, forceUpdate) {
62037
62422
  }
62038
62423
  }
62039
62424
  if (jsonOutput) {
62040
- const allModels = [...ollamaModels, ...models];
62425
+ const allModels = [...ollamaModels, ...zenModels, ...models];
62041
62426
  console.log(JSON.stringify({
62042
62427
  count: allModels.length,
62043
62428
  localCount: ollamaModels.length,
62429
+ zenCount: zenModels.length,
62044
62430
  lastUpdated: new Date().toISOString().split("T")[0],
62045
62431
  models: allModels.map((m) => ({
62046
62432
  id: m.id,
62047
62433
  name: m.name,
62048
62434
  context: m.context_length || m.top_provider?.context_length,
62049
62435
  pricing: m.pricing,
62050
- isLocal: m.isLocal || false
62436
+ isLocal: m.isLocal || false,
62437
+ isZen: m.isZen || false
62051
62438
  }))
62052
62439
  }, null, 2));
62053
62440
  return;
@@ -62089,6 +62476,35 @@ async function printAllModels(jsonOutput, forceUpdate) {
62089
62476
  console.log(" Start Ollama: ollama serve");
62090
62477
  console.log(" Pull a model: ollama pull llama3.2");
62091
62478
  }
62479
+ if (zenModels.length > 0) {
62480
+ const freeCount = zenModels.filter((m) => m.isFree).length;
62481
+ console.log(`
62482
+ \uD83D\uDD2E OPENCODE ZEN (${zenModels.length} models, ${freeCount} FREE - no API key needed):
62483
+ `);
62484
+ console.log(" Model Context Pricing Tools");
62485
+ console.log(" " + "─".repeat(68));
62486
+ const sortedModels = [...zenModels].sort((a, b) => {
62487
+ if (a.isFree && !b.isFree)
62488
+ return -1;
62489
+ if (!a.isFree && b.isFree)
62490
+ return 1;
62491
+ return (b.context_length || 0) - (a.context_length || 0);
62492
+ });
62493
+ for (const model of sortedModels) {
62494
+ const modelId = model.id.length > 30 ? model.id.substring(0, 27) + "..." : model.id;
62495
+ const modelIdPadded = modelId.padEnd(32);
62496
+ const contextLen = model.context_length || 0;
62497
+ const context = contextLen > 0 ? `${Math.round(contextLen / 1000)}K` : "N/A";
62498
+ const contextPadded = context.padEnd(10);
62499
+ const pricing = model.isFree ? `${GREEN2}FREE${RESET2}` : `$${(parseFloat(model.pricing?.prompt || "0") + parseFloat(model.pricing?.completion || "0")).toFixed(1)}/M`;
62500
+ const pricingPadded = model.isFree ? "FREE " : pricing.padEnd(12);
62501
+ const tools = model.supportsTools ? `${GREEN2}✓${RESET2}` : `${RED}✗${RESET2}`;
62502
+ console.log(` ${modelIdPadded} ${contextPadded} ${pricingPadded} ${tools}`);
62503
+ }
62504
+ console.log("");
62505
+ console.log(` ${DIM2}FREE models work without API key!${RESET2}`);
62506
+ console.log(" Use: claudish --model zen/<model-id>");
62507
+ }
62092
62508
  const byProvider = new Map;
62093
62509
  for (const model of models) {
62094
62510
  const provider = model.id.split("/")[0];
@@ -62290,6 +62706,7 @@ MODEL ROUTING (prefix-based):
62290
62706
  kimi/, moonshot/ Kimi Direct API claudish --model kimi/kimi-k2-thinking-turbo "task"
62291
62707
  glm/, zhipu/ GLM Direct API claudish --model glm/glm-4.7 "task"
62292
62708
  oc/ OllamaCloud claudish --model oc/gpt-oss:20b "task"
62709
+ zen/ OpenCode Zen (free) claudish --model zen/grok-code "task"
62293
62710
  ollama/ Ollama (local) claudish --model ollama/llama3.2 "task"
62294
62711
  lmstudio/ LM Studio (local) claudish --model lmstudio/qwen "task"
62295
62712
  vllm/ vLLM (local) claudish --model vllm/model "task"
@@ -62314,7 +62731,7 @@ OPTIONS:
62314
62731
  --cost-tracker Enable cost tracking for API usage (NB!)
62315
62732
  --audit-costs Show cost analysis report
62316
62733
  --reset-costs Reset accumulated cost statistics
62317
- --models List ALL OpenRouter models grouped by provider
62734
+ --models List ALL models (OpenRouter + OpenCode Zen + Ollama)
62318
62735
  --models <query> Fuzzy search all models by name, ID, or description
62319
62736
  --top-models List recommended/top programming models (curated)
62320
62737
  --json Output in JSON format (use with --models or --top-models)
@@ -62379,6 +62796,7 @@ ENVIRONMENT VARIABLES:
62379
62796
  ZHIPU_API_KEY GLM/Zhipu API key (for glm/, zhipu/ prefix)
62380
62797
  GLM_API_KEY Alias for ZHIPU_API_KEY
62381
62798
  OLLAMA_API_KEY OllamaCloud API key (for oc/ prefix)
62799
+ OPENCODE_API_KEY OpenCode Zen API key (optional - free models work without it)
62382
62800
  ANTHROPIC_API_KEY Placeholder (prevents Claude Code dialog)
62383
62801
  ANTHROPIC_AUTH_TOKEN Placeholder (prevents Claude Code login screen)
62384
62802
 
@@ -62391,6 +62809,7 @@ ENVIRONMENT VARIABLES:
62391
62809
  ZHIPU_BASE_URL Custom GLM/Zhipu endpoint
62392
62810
  GLM_BASE_URL Alias for ZHIPU_BASE_URL
62393
62811
  OLLAMACLOUD_BASE_URL Custom OllamaCloud endpoint (default: https://ollama.com)
62812
+ OPENCODE_BASE_URL Custom OpenCode Zen endpoint (default: https://opencode.ai/zen)
62394
62813
 
62395
62814
  Local providers:
62396
62815
  OLLAMA_BASE_URL Ollama server (default: http://localhost:11434)
@@ -62451,6 +62870,11 @@ EXAMPLES:
62451
62870
  claudish --model glm/glm-4.7 "code generation"
62452
62871
  claudish --model zhipu/glm-4-plus "complex task"
62453
62872
 
62873
+ # OpenCode Zen (free models)
62874
+ claudish --model zen/grok-code "implement feature"
62875
+ claudish --model zen/glm-4.7-free "code review"
62876
+ claudish --model zen/minimax-m2.1-free "complex task"
62877
+
62454
62878
  # Local models (free, private)
62455
62879
  claudish --model ollama/llama3.2 "code review"
62456
62880
  claudish --model lmstudio/qwen2.5-coder "refactor"
@@ -62503,10 +62927,11 @@ LOCAL MODELS (Ollama, LM Studio, vLLM):
62503
62927
  OLLAMA_HOST=http://192.168.1.50:11434 claudish --model ollama/llama3.2 "task"
62504
62928
 
62505
62929
  AVAILABLE MODELS:
62506
- List all models: claudish --models
62930
+ List all models: claudish --models (includes OpenRouter, OpenCode Zen, Ollama)
62507
62931
  Search models: claudish --models <query>
62508
62932
  Top recommended: claudish --top-models
62509
- JSON output: claudish --models --json | claudish --top-models --json
62933
+ Free models only: claudish --free (interactive selector with free models)
62934
+ JSON output: claudish --models --json
62510
62935
  Force cache update: claudish --models --force-update
62511
62936
  (Cache auto-updates every 2 days)
62512
62937
 
@@ -62684,7 +63109,37 @@ function printAvailableModelsJSON() {
62684
63109
  console.log(JSON.stringify(output, null, 2));
62685
63110
  }
62686
63111
  }
62687
- var __filename6, __dirname6, VERSION = "3.7.8", CACHE_MAX_AGE_DAYS3 = 2, MODELS_JSON_PATH, ALL_MODELS_JSON_PATH2;
63112
+ async function fetchZenModels2() {
63113
+ try {
63114
+ const response = await fetch("https://models.dev/api.json", {
63115
+ signal: AbortSignal.timeout(1e4)
63116
+ });
63117
+ if (!response.ok) {
63118
+ return [];
63119
+ }
63120
+ const data = await response.json();
63121
+ const opencode = data.opencode;
63122
+ if (!opencode?.models)
63123
+ return [];
63124
+ return Object.entries(opencode.models).map(([id, m]) => {
63125
+ const isFree = m.cost?.input === 0 && m.cost?.output === 0;
63126
+ return {
63127
+ id: `zen/${id}`,
63128
+ name: m.name || id,
63129
+ context_length: m.limit?.context || 128000,
63130
+ max_output: m.limit?.output || 32000,
63131
+ pricing: isFree ? { prompt: "0", completion: "0" } : { prompt: String(m.cost?.input || 0), completion: String(m.cost?.output || 0) },
63132
+ isZen: true,
63133
+ isFree,
63134
+ supportsTools: m.tool_call || false,
63135
+ supportsReasoning: m.reasoning || false
63136
+ };
63137
+ });
63138
+ } catch {
63139
+ return [];
63140
+ }
63141
+ }
63142
+ var __filename6, __dirname6, VERSION = "3.8.0", CACHE_MAX_AGE_DAYS3 = 2, MODELS_JSON_PATH, ALL_MODELS_JSON_PATH2;
62688
63143
  var init_cli = __esm(() => {
62689
63144
  init_dist3();
62690
63145
  init_model_loader2();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudish",
3
- "version": "3.7.8",
3
+ "version": "3.8.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",