claudish 2.2.1 → 2.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/dist/index.js CHANGED
@@ -1,8 +1,22 @@
1
1
  #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ var __create = Object.create;
4
+ var __getProtoOf = Object.getPrototypeOf;
2
5
  var __defProp = Object.defineProperty;
3
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
4
7
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __toESM = (mod, isNodeMode, target) => {
10
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
11
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
12
+ for (let key of __getOwnPropNames(mod))
13
+ if (!__hasOwnProp.call(to, key))
14
+ __defProp(to, key, {
15
+ get: () => mod[key],
16
+ enumerable: true
17
+ });
18
+ return to;
19
+ };
6
20
  var __moduleCache = /* @__PURE__ */ new WeakMap;
7
21
  var __toCommonJS = (from) => {
8
22
  var entry = __moduleCache.get(from), desc;
@@ -17,6 +31,7 @@ var __toCommonJS = (from) => {
17
31
  __moduleCache.set(from, entry);
18
32
  return entry;
19
33
  };
34
+ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
20
35
  var __export = (target, all) => {
21
36
  for (var name in all)
22
37
  __defProp(target, name, {
@@ -27,6 +42,402 @@ var __export = (target, all) => {
27
42
  });
28
43
  };
29
44
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
45
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
46
+
47
+ // node_modules/dotenv/package.json
48
+ var require_package = __commonJS((exports, module) => {
49
+ module.exports = {
50
+ name: "dotenv",
51
+ version: "17.2.3",
52
+ description: "Loads environment variables from .env file",
53
+ main: "lib/main.js",
54
+ types: "lib/main.d.ts",
55
+ exports: {
56
+ ".": {
57
+ types: "./lib/main.d.ts",
58
+ require: "./lib/main.js",
59
+ default: "./lib/main.js"
60
+ },
61
+ "./config": "./config.js",
62
+ "./config.js": "./config.js",
63
+ "./lib/env-options": "./lib/env-options.js",
64
+ "./lib/env-options.js": "./lib/env-options.js",
65
+ "./lib/cli-options": "./lib/cli-options.js",
66
+ "./lib/cli-options.js": "./lib/cli-options.js",
67
+ "./package.json": "./package.json"
68
+ },
69
+ scripts: {
70
+ "dts-check": "tsc --project tests/types/tsconfig.json",
71
+ lint: "standard",
72
+ pretest: "npm run lint && npm run dts-check",
73
+ test: "tap run tests/**/*.js --allow-empty-coverage --disable-coverage --timeout=60000",
74
+ "test:coverage": "tap run tests/**/*.js --show-full-coverage --timeout=60000 --coverage-report=text --coverage-report=lcov",
75
+ prerelease: "npm test",
76
+ release: "standard-version"
77
+ },
78
+ repository: {
79
+ type: "git",
80
+ url: "git://github.com/motdotla/dotenv.git"
81
+ },
82
+ homepage: "https://github.com/motdotla/dotenv#readme",
83
+ funding: "https://dotenvx.com",
84
+ keywords: [
85
+ "dotenv",
86
+ "env",
87
+ ".env",
88
+ "environment",
89
+ "variables",
90
+ "config",
91
+ "settings"
92
+ ],
93
+ readmeFilename: "README.md",
94
+ license: "BSD-2-Clause",
95
+ devDependencies: {
96
+ "@types/node": "^18.11.3",
97
+ decache: "^4.6.2",
98
+ sinon: "^14.0.1",
99
+ standard: "^17.0.0",
100
+ "standard-version": "^9.5.0",
101
+ tap: "^19.2.0",
102
+ typescript: "^4.8.4"
103
+ },
104
+ engines: {
105
+ node: ">=12"
106
+ },
107
+ browser: {
108
+ fs: false
109
+ }
110
+ };
111
+ });
112
+
113
+ // node_modules/dotenv/lib/main.js
114
+ var require_main = __commonJS((exports, module) => {
115
+ var fs = __require("fs");
116
+ var path = __require("path");
117
+ var os = __require("os");
118
+ var crypto = __require("crypto");
119
+ var packageJson = require_package();
120
+ var version = packageJson.version;
121
+ var TIPS = [
122
+ "\uD83D\uDD10 encrypt with Dotenvx: https://dotenvx.com",
123
+ "\uD83D\uDD10 prevent committing .env to code: https://dotenvx.com/precommit",
124
+ "\uD83D\uDD10 prevent building .env in docker: https://dotenvx.com/prebuild",
125
+ "\uD83D\uDCE1 add observability to secrets: https://dotenvx.com/ops",
126
+ "\uD83D\uDC65 sync secrets across teammates & machines: https://dotenvx.com/ops",
127
+ "\uD83D\uDDC2️ backup and recover secrets: https://dotenvx.com/ops",
128
+ "✅ audit secrets and track compliance: https://dotenvx.com/ops",
129
+ "\uD83D\uDD04 add secrets lifecycle management: https://dotenvx.com/ops",
130
+ "\uD83D\uDD11 add access controls to secrets: https://dotenvx.com/ops",
131
+ "\uD83D\uDEE0️ run anywhere with `dotenvx run -- yourcommand`",
132
+ "⚙️ specify custom .env file path with { path: '/custom/path/.env' }",
133
+ "⚙️ enable debug logging with { debug: true }",
134
+ "⚙️ override existing env vars with { override: true }",
135
+ "⚙️ suppress all logs with { quiet: true }",
136
+ "⚙️ write to custom object with { processEnv: myObject }",
137
+ "⚙️ load multiple .env files with { path: ['.env.local', '.env'] }"
138
+ ];
139
+ function _getRandomTip() {
140
+ return TIPS[Math.floor(Math.random() * TIPS.length)];
141
+ }
142
+ function parseBoolean(value) {
143
+ if (typeof value === "string") {
144
+ return !["false", "0", "no", "off", ""].includes(value.toLowerCase());
145
+ }
146
+ return Boolean(value);
147
+ }
148
+ function supportsAnsi() {
149
+ return process.stdout.isTTY;
150
+ }
151
+ function dim(text) {
152
+ return supportsAnsi() ? `\x1B[2m${text}\x1B[0m` : text;
153
+ }
154
+ var LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg;
155
+ function parse(src) {
156
+ const obj = {};
157
+ let lines = src.toString();
158
+ lines = lines.replace(/\r\n?/mg, `
159
+ `);
160
+ let match;
161
+ while ((match = LINE.exec(lines)) != null) {
162
+ const key = match[1];
163
+ let value = match[2] || "";
164
+ value = value.trim();
165
+ const maybeQuote = value[0];
166
+ value = value.replace(/^(['"`])([\s\S]*)\1$/mg, "$2");
167
+ if (maybeQuote === '"') {
168
+ value = value.replace(/\\n/g, `
169
+ `);
170
+ value = value.replace(/\\r/g, "\r");
171
+ }
172
+ obj[key] = value;
173
+ }
174
+ return obj;
175
+ }
176
+ function _parseVault(options) {
177
+ options = options || {};
178
+ const vaultPath = _vaultPath(options);
179
+ options.path = vaultPath;
180
+ const result = DotenvModule.configDotenv(options);
181
+ if (!result.parsed) {
182
+ const err = new Error(`MISSING_DATA: Cannot parse ${vaultPath} for an unknown reason`);
183
+ err.code = "MISSING_DATA";
184
+ throw err;
185
+ }
186
+ const keys = _dotenvKey(options).split(",");
187
+ const length = keys.length;
188
+ let decrypted;
189
+ for (let i = 0;i < length; i++) {
190
+ try {
191
+ const key = keys[i].trim();
192
+ const attrs = _instructions(result, key);
193
+ decrypted = DotenvModule.decrypt(attrs.ciphertext, attrs.key);
194
+ break;
195
+ } catch (error) {
196
+ if (i + 1 >= length) {
197
+ throw error;
198
+ }
199
+ }
200
+ }
201
+ return DotenvModule.parse(decrypted);
202
+ }
203
+ function _warn(message) {
204
+ console.error(`[dotenv@${version}][WARN] ${message}`);
205
+ }
206
+ function _debug(message) {
207
+ console.log(`[dotenv@${version}][DEBUG] ${message}`);
208
+ }
209
+ function _log(message) {
210
+ console.log(`[dotenv@${version}] ${message}`);
211
+ }
212
+ function _dotenvKey(options) {
213
+ if (options && options.DOTENV_KEY && options.DOTENV_KEY.length > 0) {
214
+ return options.DOTENV_KEY;
215
+ }
216
+ if (process.env.DOTENV_KEY && process.env.DOTENV_KEY.length > 0) {
217
+ return process.env.DOTENV_KEY;
218
+ }
219
+ return "";
220
+ }
221
+ function _instructions(result, dotenvKey) {
222
+ let uri;
223
+ try {
224
+ uri = new URL(dotenvKey);
225
+ } catch (error) {
226
+ if (error.code === "ERR_INVALID_URL") {
227
+ const err = new Error("INVALID_DOTENV_KEY: Wrong format. Must be in valid uri format like dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=development");
228
+ err.code = "INVALID_DOTENV_KEY";
229
+ throw err;
230
+ }
231
+ throw error;
232
+ }
233
+ const key = uri.password;
234
+ if (!key) {
235
+ const err = new Error("INVALID_DOTENV_KEY: Missing key part");
236
+ err.code = "INVALID_DOTENV_KEY";
237
+ throw err;
238
+ }
239
+ const environment = uri.searchParams.get("environment");
240
+ if (!environment) {
241
+ const err = new Error("INVALID_DOTENV_KEY: Missing environment part");
242
+ err.code = "INVALID_DOTENV_KEY";
243
+ throw err;
244
+ }
245
+ const environmentKey = `DOTENV_VAULT_${environment.toUpperCase()}`;
246
+ const ciphertext = result.parsed[environmentKey];
247
+ if (!ciphertext) {
248
+ const err = new Error(`NOT_FOUND_DOTENV_ENVIRONMENT: Cannot locate environment ${environmentKey} in your .env.vault file.`);
249
+ err.code = "NOT_FOUND_DOTENV_ENVIRONMENT";
250
+ throw err;
251
+ }
252
+ return { ciphertext, key };
253
+ }
254
+ function _vaultPath(options) {
255
+ let possibleVaultPath = null;
256
+ if (options && options.path && options.path.length > 0) {
257
+ if (Array.isArray(options.path)) {
258
+ for (const filepath of options.path) {
259
+ if (fs.existsSync(filepath)) {
260
+ possibleVaultPath = filepath.endsWith(".vault") ? filepath : `${filepath}.vault`;
261
+ }
262
+ }
263
+ } else {
264
+ possibleVaultPath = options.path.endsWith(".vault") ? options.path : `${options.path}.vault`;
265
+ }
266
+ } else {
267
+ possibleVaultPath = path.resolve(process.cwd(), ".env.vault");
268
+ }
269
+ if (fs.existsSync(possibleVaultPath)) {
270
+ return possibleVaultPath;
271
+ }
272
+ return null;
273
+ }
274
+ function _resolveHome(envPath) {
275
+ return envPath[0] === "~" ? path.join(os.homedir(), envPath.slice(1)) : envPath;
276
+ }
277
+ function _configVault(options) {
278
+ const debug = parseBoolean(process.env.DOTENV_CONFIG_DEBUG || options && options.debug);
279
+ const quiet = parseBoolean(process.env.DOTENV_CONFIG_QUIET || options && options.quiet);
280
+ if (debug || !quiet) {
281
+ _log("Loading env from encrypted .env.vault");
282
+ }
283
+ const parsed = DotenvModule._parseVault(options);
284
+ let processEnv = process.env;
285
+ if (options && options.processEnv != null) {
286
+ processEnv = options.processEnv;
287
+ }
288
+ DotenvModule.populate(processEnv, parsed, options);
289
+ return { parsed };
290
+ }
291
+ function configDotenv(options) {
292
+ const dotenvPath = path.resolve(process.cwd(), ".env");
293
+ let encoding = "utf8";
294
+ let processEnv = process.env;
295
+ if (options && options.processEnv != null) {
296
+ processEnv = options.processEnv;
297
+ }
298
+ let debug = parseBoolean(processEnv.DOTENV_CONFIG_DEBUG || options && options.debug);
299
+ let quiet = parseBoolean(processEnv.DOTENV_CONFIG_QUIET || options && options.quiet);
300
+ if (options && options.encoding) {
301
+ encoding = options.encoding;
302
+ } else {
303
+ if (debug) {
304
+ _debug("No encoding is specified. UTF-8 is used by default");
305
+ }
306
+ }
307
+ let optionPaths = [dotenvPath];
308
+ if (options && options.path) {
309
+ if (!Array.isArray(options.path)) {
310
+ optionPaths = [_resolveHome(options.path)];
311
+ } else {
312
+ optionPaths = [];
313
+ for (const filepath of options.path) {
314
+ optionPaths.push(_resolveHome(filepath));
315
+ }
316
+ }
317
+ }
318
+ let lastError;
319
+ const parsedAll = {};
320
+ for (const path2 of optionPaths) {
321
+ try {
322
+ const parsed = DotenvModule.parse(fs.readFileSync(path2, { encoding }));
323
+ DotenvModule.populate(parsedAll, parsed, options);
324
+ } catch (e) {
325
+ if (debug) {
326
+ _debug(`Failed to load ${path2} ${e.message}`);
327
+ }
328
+ lastError = e;
329
+ }
330
+ }
331
+ const populated = DotenvModule.populate(processEnv, parsedAll, options);
332
+ debug = parseBoolean(processEnv.DOTENV_CONFIG_DEBUG || debug);
333
+ quiet = parseBoolean(processEnv.DOTENV_CONFIG_QUIET || quiet);
334
+ if (debug || !quiet) {
335
+ const keysCount = Object.keys(populated).length;
336
+ const shortPaths = [];
337
+ for (const filePath of optionPaths) {
338
+ try {
339
+ const relative = path.relative(process.cwd(), filePath);
340
+ shortPaths.push(relative);
341
+ } catch (e) {
342
+ if (debug) {
343
+ _debug(`Failed to load ${filePath} ${e.message}`);
344
+ }
345
+ lastError = e;
346
+ }
347
+ }
348
+ _log(`injecting env (${keysCount}) from ${shortPaths.join(",")} ${dim(`-- tip: ${_getRandomTip()}`)}`);
349
+ }
350
+ if (lastError) {
351
+ return { parsed: parsedAll, error: lastError };
352
+ } else {
353
+ return { parsed: parsedAll };
354
+ }
355
+ }
356
+ function config(options) {
357
+ if (_dotenvKey(options).length === 0) {
358
+ return DotenvModule.configDotenv(options);
359
+ }
360
+ const vaultPath = _vaultPath(options);
361
+ if (!vaultPath) {
362
+ _warn(`You set DOTENV_KEY but you are missing a .env.vault file at ${vaultPath}. Did you forget to build it?`);
363
+ return DotenvModule.configDotenv(options);
364
+ }
365
+ return DotenvModule._configVault(options);
366
+ }
367
+ function decrypt(encrypted, keyStr) {
368
+ const key = Buffer.from(keyStr.slice(-64), "hex");
369
+ let ciphertext = Buffer.from(encrypted, "base64");
370
+ const nonce = ciphertext.subarray(0, 12);
371
+ const authTag = ciphertext.subarray(-16);
372
+ ciphertext = ciphertext.subarray(12, -16);
373
+ try {
374
+ const aesgcm = crypto.createDecipheriv("aes-256-gcm", key, nonce);
375
+ aesgcm.setAuthTag(authTag);
376
+ return `${aesgcm.update(ciphertext)}${aesgcm.final()}`;
377
+ } catch (error) {
378
+ const isRange = error instanceof RangeError;
379
+ const invalidKeyLength = error.message === "Invalid key length";
380
+ const decryptionFailed = error.message === "Unsupported state or unable to authenticate data";
381
+ if (isRange || invalidKeyLength) {
382
+ const err = new Error("INVALID_DOTENV_KEY: It must be 64 characters long (or more)");
383
+ err.code = "INVALID_DOTENV_KEY";
384
+ throw err;
385
+ } else if (decryptionFailed) {
386
+ const err = new Error("DECRYPTION_FAILED: Please check your DOTENV_KEY");
387
+ err.code = "DECRYPTION_FAILED";
388
+ throw err;
389
+ } else {
390
+ throw error;
391
+ }
392
+ }
393
+ }
394
+ function populate(processEnv, parsed, options = {}) {
395
+ const debug = Boolean(options && options.debug);
396
+ const override = Boolean(options && options.override);
397
+ const populated = {};
398
+ if (typeof parsed !== "object") {
399
+ const err = new Error("OBJECT_REQUIRED: Please check the processEnv argument being passed to populate");
400
+ err.code = "OBJECT_REQUIRED";
401
+ throw err;
402
+ }
403
+ for (const key of Object.keys(parsed)) {
404
+ if (Object.prototype.hasOwnProperty.call(processEnv, key)) {
405
+ if (override === true) {
406
+ processEnv[key] = parsed[key];
407
+ populated[key] = parsed[key];
408
+ }
409
+ if (debug) {
410
+ if (override === true) {
411
+ _debug(`"${key}" is already defined and WAS overwritten`);
412
+ } else {
413
+ _debug(`"${key}" is already defined and was NOT overwritten`);
414
+ }
415
+ }
416
+ } else {
417
+ processEnv[key] = parsed[key];
418
+ populated[key] = parsed[key];
419
+ }
420
+ }
421
+ return populated;
422
+ }
423
+ var DotenvModule = {
424
+ configDotenv,
425
+ _configVault,
426
+ _parseVault,
427
+ config,
428
+ decrypt,
429
+ parse,
430
+ populate
431
+ };
432
+ exports.configDotenv = DotenvModule.configDotenv;
433
+ exports._configVault = DotenvModule._configVault;
434
+ exports._parseVault = DotenvModule._parseVault;
435
+ exports.config = DotenvModule.config;
436
+ exports.decrypt = DotenvModule.decrypt;
437
+ exports.parse = DotenvModule.parse;
438
+ exports.populate = DotenvModule.populate;
439
+ module.exports = DotenvModule;
440
+ });
30
441
 
31
442
  // src/config.ts
32
443
  var exports_config = {};
@@ -97,7 +508,15 @@ var init_config = __esm(() => {
97
508
  CLAUDISH_PORT: "CLAUDISH_PORT",
98
509
  CLAUDISH_ACTIVE_MODEL_NAME: "CLAUDISH_ACTIVE_MODEL_NAME",
99
510
  ANTHROPIC_MODEL: "ANTHROPIC_MODEL",
100
- ANTHROPIC_SMALL_FAST_MODEL: "ANTHROPIC_SMALL_FAST_MODEL"
511
+ ANTHROPIC_SMALL_FAST_MODEL: "ANTHROPIC_SMALL_FAST_MODEL",
512
+ CLAUDISH_MODEL_OPUS: "CLAUDISH_MODEL_OPUS",
513
+ CLAUDISH_MODEL_SONNET: "CLAUDISH_MODEL_SONNET",
514
+ CLAUDISH_MODEL_HAIKU: "CLAUDISH_MODEL_HAIKU",
515
+ CLAUDISH_MODEL_SUBAGENT: "CLAUDISH_MODEL_SUBAGENT",
516
+ ANTHROPIC_DEFAULT_OPUS_MODEL: "ANTHROPIC_DEFAULT_OPUS_MODEL",
517
+ ANTHROPIC_DEFAULT_SONNET_MODEL: "ANTHROPIC_DEFAULT_SONNET_MODEL",
518
+ ANTHROPIC_DEFAULT_HAIKU_MODEL: "ANTHROPIC_DEFAULT_HAIKU_MODEL",
519
+ CLAUDE_CODE_SUBAGENT_MODEL: "CLAUDE_CODE_SUBAGENT_MODEL"
101
520
  };
102
521
  OPENROUTER_HEADERS = {
103
522
  "HTTP-Referer": "https://github.com/MadAppGang/claude-code",
@@ -124,6 +543,9 @@ var init_types = __esm(() => {
124
543
  ];
125
544
  });
126
545
 
546
+ // src/index.ts
547
+ var import_dotenv = __toESM(require_main(), 1);
548
+
127
549
  // src/claude-runner.ts
128
550
  init_config();
129
551
  import { spawn } from "node:child_process";
@@ -141,20 +563,11 @@ function createTempSettingsFile(modelDisplay, port) {
141
563
  const DIM = "\\033[2m";
142
564
  const RESET = "\\033[0m";
143
565
  const BOLD = "\\033[1m";
144
- const MODEL_CONTEXT = {
145
- "x-ai/grok-code-fast-1": 256000,
146
- "openai/gpt-5-codex": 400000,
147
- "minimax/minimax-m2": 204800,
148
- "z-ai/glm-4.6": 200000,
149
- "qwen/qwen3-vl-235b-a22b-instruct": 256000,
150
- "anthropic/claude-sonnet-4.5": 200000
151
- };
152
- const maxTokens = MODEL_CONTEXT[modelDisplay] || 1e5;
153
566
  const tokenFilePath = `/tmp/claudish-tokens-${port}.json`;
154
567
  const settings = {
155
568
  statusLine: {
156
569
  type: "command",
157
- command: `JSON=$(cat) && DIR=$(basename "$(pwd)") && [ \${#DIR} -gt 15 ] && DIR="\${DIR:0:12}..." || true && COST=$(echo "$JSON" | grep -o '"total_cost_usd":[0-9.]*' | cut -d: -f2) && [ -z "$COST" ] && COST="0" || true && if [ -f "${tokenFilePath}" ]; then TOKENS=$(cat "${tokenFilePath}" 2>/dev/null) && INPUT=$(echo "$TOKENS" | grep -o '"input_tokens":[0-9]*' | grep -o '[0-9]*') && OUTPUT=$(echo "$TOKENS" | grep -o '"output_tokens":[0-9]*' | grep -o '[0-9]*') && TOTAL=$((INPUT + OUTPUT)) && CTX=$(echo "scale=0; (${maxTokens} - $TOTAL) * 100 / ${maxTokens}" | bc 2>/dev/null); else INPUT=0 && OUTPUT=0 && CTX=100; fi && [ -z "$CTX" ] && CTX="100" || true && printf "${CYAN}${BOLD}%s${RESET} ${DIM}•${RESET} ${YELLOW}%s${RESET} ${DIM}•${RESET} ${GREEN}\\$%.3f${RESET} ${DIM}•${RESET} ${MAGENTA}%s%%${RESET}\\n" "$DIR" "$CLAUDISH_ACTIVE_MODEL_NAME" "$COST" "$CTX"`,
570
+ command: `JSON=$(cat) && DIR=$(basename "$(pwd)") && [ \${#DIR} -gt 15 ] && DIR="\${DIR:0:12}..." || true && CTX=100 && COST="0" && if [ -f "${tokenFilePath}" ]; then TOKENS=$(cat "${tokenFilePath}" 2>/dev/null) && REAL_COST=$(echo "$TOKENS" | grep -o '"total_cost":[0-9.]*' | cut -d: -f2) && REAL_CTX=$(echo "$TOKENS" | grep -o '"context_left_percent":[0-9]*' | grep -o '[0-9]*') && if [ ! -z "$REAL_COST" ]; then COST="$REAL_COST"; else COST=$(echo "$JSON" | grep -o '"total_cost_usd":[0-9.]*' | cut -d: -f2); fi && if [ ! -z "$REAL_CTX" ]; then CTX="$REAL_CTX"; fi; else COST=$(echo "$JSON" | grep -o '"total_cost_usd":[0-9.]*' | cut -d: -f2); fi && [ -z "$COST" ] && COST="0" || true && printf "${CYAN}${BOLD}%s${RESET} ${DIM}•${RESET} ${YELLOW}%s${RESET} ${DIM}•${RESET} ${GREEN}\\$%.3f${RESET} ${DIM}•${RESET} ${MAGENTA}%s%%${RESET}\\n" "$DIR" "$CLAUDISH_ACTIVE_MODEL_NAME" "$COST" "$CTX"`,
158
571
  padding: 0
159
572
  }
160
573
  };
@@ -336,11 +749,102 @@ function getAvailableModels() {
336
749
  _cachedModelIds = [...OPENROUTER_MODELS2];
337
750
  return [...OPENROUTER_MODELS2];
338
751
  }
752
+ var _cachedOpenRouterModels = null;
753
+ async function fetchModelContextWindow(modelId) {
754
+ if (_cachedOpenRouterModels) {
755
+ const model = _cachedOpenRouterModels.find((m) => m.id === modelId);
756
+ if (model) {
757
+ return model.context_length || model.top_provider?.context_length || 128000;
758
+ }
759
+ }
760
+ try {
761
+ const response = await fetch("https://openrouter.ai/api/v1/models");
762
+ if (response.ok) {
763
+ const data = await response.json();
764
+ _cachedOpenRouterModels = data.data;
765
+ const model = _cachedOpenRouterModels?.find((m) => m.id === modelId);
766
+ if (model) {
767
+ return model.context_length || model.top_provider?.context_length || 128000;
768
+ }
769
+ }
770
+ } catch (error) {}
771
+ try {
772
+ const modelMetadata = loadModelInfo();
773
+ } catch (e) {}
774
+ const jsonPath = join2(__dirname2, "../recommended-models.json");
775
+ if (existsSync(jsonPath)) {
776
+ try {
777
+ const jsonContent = readFileSync(jsonPath, "utf-8");
778
+ const data = JSON.parse(jsonContent);
779
+ const model = data.models.find((m) => m.id === modelId);
780
+ if (model && model.context) {
781
+ const ctxStr = model.context.toUpperCase();
782
+ if (ctxStr.includes("K"))
783
+ return parseFloat(ctxStr.replace("K", "")) * 1024;
784
+ if (ctxStr.includes("M"))
785
+ return parseFloat(ctxStr.replace("M", "")) * 1e6;
786
+ const val = parseInt(ctxStr);
787
+ if (!isNaN(val))
788
+ return val;
789
+ }
790
+ } catch (e) {}
791
+ }
792
+ return 200000;
793
+ }
794
+ async function doesModelSupportReasoning(modelId) {
795
+ if (!_cachedOpenRouterModels) {
796
+ await fetchModelContextWindow(modelId);
797
+ }
798
+ if (_cachedOpenRouterModels) {
799
+ const model = _cachedOpenRouterModels.find((m) => m.id === modelId);
800
+ if (model && model.supported_parameters) {
801
+ return model.supported_parameters.includes("include_reasoning") || model.supported_parameters.includes("reasoning") || model.id.includes("o1") || model.id.includes("o3") || model.id.includes("r1");
802
+ }
803
+ }
804
+ return false;
805
+ }
339
806
 
340
807
  // src/cli.ts
341
808
  import { readFileSync as readFileSync2, writeFileSync as writeFileSync3, existsSync as existsSync2, mkdirSync, copyFileSync } from "node:fs";
342
809
  import { fileURLToPath as fileURLToPath2 } from "node:url";
343
810
  import { dirname as dirname2, join as join3 } from "node:path";
811
+
812
+ // src/utils.ts
813
+ function fuzzyScore(text, query) {
814
+ if (!text || !query)
815
+ return 0;
816
+ const t = text.toLowerCase();
817
+ const q = query.toLowerCase();
818
+ if (t === q)
819
+ return 1;
820
+ if (t.startsWith(q))
821
+ return 0.9;
822
+ if (t.includes(` ${q}`) || t.includes(`-${q}`) || t.includes(`/${q}`))
823
+ return 0.8;
824
+ if (t.includes(q))
825
+ return 0.6;
826
+ let score = 0;
827
+ let tIdx = 0;
828
+ let qIdx = 0;
829
+ let consecutive = 0;
830
+ while (tIdx < t.length && qIdx < q.length) {
831
+ if (t[tIdx] === q[qIdx]) {
832
+ score += 1 + consecutive * 0.5;
833
+ consecutive++;
834
+ qIdx++;
835
+ } else {
836
+ consecutive = 0;
837
+ }
838
+ tIdx++;
839
+ }
840
+ if (qIdx === q.length) {
841
+ const compactness = q.length / (tIdx + 1);
842
+ return 0.1 + 0.4 * compactness * (score / (q.length * 2));
843
+ }
844
+ return 0;
845
+ }
846
+
847
+ // src/cli.ts
344
848
  var __filename3 = fileURLToPath2(import.meta.url);
345
849
  var __dirname3 = dirname2(__filename3);
346
850
  var packageJson = JSON.parse(readFileSync2(join3(__dirname3, "../package.json"), "utf-8"));
@@ -357,6 +861,7 @@ async function parseArgs(args) {
357
861
  jsonOutput: false,
358
862
  monitor: false,
359
863
  stdin: false,
864
+ freeOnly: false,
360
865
  claudeArgs: []
361
866
  };
362
867
  const claudishModel = process.env[ENV.CLAUDISH_MODEL];
@@ -366,6 +871,10 @@ async function parseArgs(args) {
366
871
  } else if (anthropicModel) {
367
872
  config.model = anthropicModel;
368
873
  }
874
+ config.modelOpus = process.env[ENV.CLAUDISH_MODEL_OPUS] || process.env[ENV.ANTHROPIC_DEFAULT_OPUS_MODEL];
875
+ config.modelSonnet = process.env[ENV.CLAUDISH_MODEL_SONNET] || process.env[ENV.ANTHROPIC_DEFAULT_SONNET_MODEL];
876
+ config.modelHaiku = process.env[ENV.CLAUDISH_MODEL_HAIKU] || process.env[ENV.ANTHROPIC_DEFAULT_HAIKU_MODEL];
877
+ config.modelSubagent = process.env[ENV.CLAUDISH_MODEL_SUBAGENT] || process.env[ENV.CLAUDE_CODE_SUBAGENT_MODEL];
369
878
  const envPort = process.env[ENV.CLAUDISH_PORT];
370
879
  if (envPort) {
371
880
  const port = Number.parseInt(envPort, 10);
@@ -384,6 +893,22 @@ async function parseArgs(args) {
384
893
  process.exit(1);
385
894
  }
386
895
  config.model = modelArg;
896
+ } else if (arg === "--model-opus") {
897
+ const val = args[++i];
898
+ if (val)
899
+ config.modelOpus = val;
900
+ } else if (arg === "--model-sonnet") {
901
+ const val = args[++i];
902
+ if (val)
903
+ config.modelSonnet = val;
904
+ } else if (arg === "--model-haiku") {
905
+ const val = args[++i];
906
+ if (val)
907
+ config.modelHaiku = val;
908
+ } else if (arg === "--model-subagent") {
909
+ const val = args[++i];
910
+ if (val)
911
+ config.modelSubagent = val;
387
912
  } else if (arg === "--port" || arg === "-p") {
388
913
  const portArg = args[++i];
389
914
  if (!portArg) {
@@ -421,6 +946,8 @@ async function parseArgs(args) {
421
946
  config.monitor = true;
422
947
  } else if (arg === "--stdin") {
423
948
  config.stdin = true;
949
+ } else if (arg === "--free") {
950
+ config.freeOnly = true;
424
951
  } else if (arg === "--cost-tracker") {
425
952
  config.costTracking = true;
426
953
  if (!config.monitor) {
@@ -442,7 +969,7 @@ async function parseArgs(args) {
442
969
  } else if (arg === "--init") {
443
970
  await initializeClaudishSkill();
444
971
  process.exit(0);
445
- } else if (arg === "--list-models") {
972
+ } else if (arg === "--top-models") {
446
973
  const hasJsonFlag = args.includes("--json");
447
974
  const forceUpdate = args.includes("--force-update");
448
975
  await checkAndUpdateModelsCache(forceUpdate);
@@ -452,6 +979,18 @@ async function parseArgs(args) {
452
979
  printAvailableModels();
453
980
  }
454
981
  process.exit(0);
982
+ } else if (arg === "--models" || arg === "-s" || arg === "--search") {
983
+ const nextArg = args[i + 1];
984
+ const hasQuery = nextArg && !nextArg.startsWith("--");
985
+ const query = hasQuery ? args[++i] : null;
986
+ const hasJsonFlag = args.includes("--json");
987
+ const forceUpdate = args.includes("--force-update");
988
+ if (query) {
989
+ await searchAndPrintModels(query, forceUpdate);
990
+ } else {
991
+ await printAllModels(hasJsonFlag, forceUpdate);
992
+ }
993
+ process.exit(0);
455
994
  } else {
456
995
  config.claudeArgs = args.slice(i);
457
996
  break;
@@ -500,6 +1039,174 @@ async function parseArgs(args) {
500
1039
  }
501
1040
  var CACHE_MAX_AGE_DAYS = 2;
502
1041
  var MODELS_JSON_PATH = join3(__dirname3, "../recommended-models.json");
1042
+ var ALL_MODELS_JSON_PATH = join3(__dirname3, "../all-models.json");
1043
+ async function searchAndPrintModels(query, forceUpdate) {
1044
+ let models = [];
1045
+ if (!forceUpdate && existsSync2(ALL_MODELS_JSON_PATH)) {
1046
+ try {
1047
+ const cacheData = JSON.parse(readFileSync2(ALL_MODELS_JSON_PATH, "utf-8"));
1048
+ const lastUpdated = new Date(cacheData.lastUpdated);
1049
+ const now = new Date;
1050
+ const ageInDays = (now.getTime() - lastUpdated.getTime()) / (1000 * 60 * 60 * 24);
1051
+ if (ageInDays <= CACHE_MAX_AGE_DAYS) {
1052
+ models = cacheData.models;
1053
+ }
1054
+ } catch (e) {}
1055
+ }
1056
+ if (models.length === 0) {
1057
+ console.error("\uD83D\uDD04 Fetching all models from OpenRouter (this may take a moment)...");
1058
+ try {
1059
+ const response = await fetch("https://openrouter.ai/api/v1/models");
1060
+ if (!response.ok)
1061
+ throw new Error(`API returned ${response.status}`);
1062
+ const data = await response.json();
1063
+ models = data.data;
1064
+ writeFileSync3(ALL_MODELS_JSON_PATH, JSON.stringify({
1065
+ lastUpdated: new Date().toISOString(),
1066
+ models
1067
+ }), "utf-8");
1068
+ console.error(`✅ Cached ${models.length} models`);
1069
+ } catch (error) {
1070
+ console.error(`❌ Failed to fetch models: ${error}`);
1071
+ process.exit(1);
1072
+ }
1073
+ }
1074
+ const results = models.map((model) => {
1075
+ const nameScore = fuzzyScore(model.name || "", query);
1076
+ const idScore = fuzzyScore(model.id || "", query);
1077
+ const descScore = fuzzyScore(model.description || "", query) * 0.5;
1078
+ return {
1079
+ model,
1080
+ score: Math.max(nameScore, idScore, descScore)
1081
+ };
1082
+ }).filter((item) => item.score > 0.2).sort((a, b) => b.score - a.score).slice(0, 20);
1083
+ if (results.length === 0) {
1084
+ console.log(`No models found matching "${query}"`);
1085
+ return;
1086
+ }
1087
+ console.log(`
1088
+ Found ${results.length} matching models:
1089
+ `);
1090
+ console.log(" Model Provider Pricing Context Score");
1091
+ console.log(" " + "─".repeat(80));
1092
+ for (const { model, score } of results) {
1093
+ const modelId = model.id.length > 30 ? model.id.substring(0, 27) + "..." : model.id;
1094
+ const modelIdPadded = modelId.padEnd(30);
1095
+ const providerName = model.id.split("/")[0];
1096
+ const provider = providerName.length > 10 ? providerName.substring(0, 7) + "..." : providerName;
1097
+ const providerPadded = provider.padEnd(10);
1098
+ const promptPrice = parseFloat(model.pricing?.prompt || "0") * 1e6;
1099
+ const completionPrice = parseFloat(model.pricing?.completion || "0") * 1e6;
1100
+ const avg = (promptPrice + completionPrice) / 2;
1101
+ let pricing;
1102
+ if (avg < 0) {
1103
+ pricing = "varies";
1104
+ } else if (avg === 0) {
1105
+ pricing = "FREE";
1106
+ } else {
1107
+ pricing = `$${avg.toFixed(2)}/1M`;
1108
+ }
1109
+ const pricingPadded = pricing.padEnd(10);
1110
+ const contextLen = model.context_length || model.top_provider?.context_length || 0;
1111
+ const context = contextLen > 0 ? `${Math.round(contextLen / 1000)}K` : "N/A";
1112
+ const contextPadded = context.padEnd(7);
1113
+ console.log(` ${modelIdPadded} ${providerPadded} ${pricingPadded} ${contextPadded} ${(score * 100).toFixed(0)}%`);
1114
+ }
1115
+ console.log("");
1116
+ console.log("Use a model: claudish --model <model-id>");
1117
+ }
1118
+ async function printAllModels(jsonOutput, forceUpdate) {
1119
+ let models = [];
1120
+ if (!forceUpdate && existsSync2(ALL_MODELS_JSON_PATH)) {
1121
+ try {
1122
+ const cacheData = JSON.parse(readFileSync2(ALL_MODELS_JSON_PATH, "utf-8"));
1123
+ const lastUpdated = new Date(cacheData.lastUpdated);
1124
+ const now = new Date;
1125
+ const ageInDays = (now.getTime() - lastUpdated.getTime()) / (1000 * 60 * 60 * 24);
1126
+ if (ageInDays <= CACHE_MAX_AGE_DAYS) {
1127
+ models = cacheData.models;
1128
+ if (!jsonOutput) {
1129
+ console.error(`✓ Using cached models (last updated: ${cacheData.lastUpdated.split("T")[0]})`);
1130
+ }
1131
+ }
1132
+ } catch (e) {}
1133
+ }
1134
+ if (models.length === 0) {
1135
+ console.error("\uD83D\uDD04 Fetching all models from OpenRouter...");
1136
+ try {
1137
+ const response = await fetch("https://openrouter.ai/api/v1/models");
1138
+ if (!response.ok)
1139
+ throw new Error(`API returned ${response.status}`);
1140
+ const data = await response.json();
1141
+ models = data.data;
1142
+ writeFileSync3(ALL_MODELS_JSON_PATH, JSON.stringify({
1143
+ lastUpdated: new Date().toISOString(),
1144
+ models
1145
+ }), "utf-8");
1146
+ console.error(`✅ Cached ${models.length} models`);
1147
+ } catch (error) {
1148
+ console.error(`❌ Failed to fetch models: ${error}`);
1149
+ process.exit(1);
1150
+ }
1151
+ }
1152
+ if (jsonOutput) {
1153
+ console.log(JSON.stringify({
1154
+ count: models.length,
1155
+ lastUpdated: new Date().toISOString().split("T")[0],
1156
+ models: models.map((m) => ({
1157
+ id: m.id,
1158
+ name: m.name,
1159
+ context: m.context_length || m.top_provider?.context_length,
1160
+ pricing: m.pricing
1161
+ }))
1162
+ }, null, 2));
1163
+ return;
1164
+ }
1165
+ const byProvider = new Map;
1166
+ for (const model of models) {
1167
+ const provider = model.id.split("/")[0];
1168
+ if (!byProvider.has(provider)) {
1169
+ byProvider.set(provider, []);
1170
+ }
1171
+ byProvider.get(provider).push(model);
1172
+ }
1173
+ const sortedProviders = [...byProvider.keys()].sort();
1174
+ console.log(`
1175
+ All OpenRouter Models (${models.length} total):
1176
+ `);
1177
+ for (const provider of sortedProviders) {
1178
+ const providerModels = byProvider.get(provider);
1179
+ console.log(`
1180
+ ${provider.toUpperCase()} (${providerModels.length} models)`);
1181
+ console.log(" " + "─".repeat(70));
1182
+ for (const model of providerModels) {
1183
+ const shortId = model.id.split("/").slice(1).join("/");
1184
+ const modelId = shortId.length > 40 ? shortId.substring(0, 37) + "..." : shortId;
1185
+ const modelIdPadded = modelId.padEnd(42);
1186
+ const promptPrice = parseFloat(model.pricing?.prompt || "0") * 1e6;
1187
+ const completionPrice = parseFloat(model.pricing?.completion || "0") * 1e6;
1188
+ const avg = (promptPrice + completionPrice) / 2;
1189
+ let pricing;
1190
+ if (avg < 0) {
1191
+ pricing = "varies";
1192
+ } else if (avg === 0) {
1193
+ pricing = "FREE";
1194
+ } else {
1195
+ pricing = `$${avg.toFixed(2)}/1M`;
1196
+ }
1197
+ const pricingPadded = pricing.padEnd(12);
1198
+ const contextLen = model.context_length || model.top_provider?.context_length || 0;
1199
+ const context = contextLen > 0 ? `${Math.round(contextLen / 1000)}K` : "N/A";
1200
+ const contextPadded = context.padEnd(8);
1201
+ console.log(` ${modelIdPadded} ${pricingPadded} ${contextPadded}`);
1202
+ }
1203
+ }
1204
+ console.log(`
1205
+ `);
1206
+ console.log("Use a model: claudish --model <provider/model-id>");
1207
+ console.log("Search: claudish --search <query>");
1208
+ console.log("Top models: claudish --top-models");
1209
+ }
503
1210
  function isCacheStale() {
504
1211
  if (!existsSync2(MODELS_JSON_PATH)) {
505
1212
  return true;
@@ -522,6 +1229,8 @@ async function updateModelsFromOpenRouter() {
522
1229
  console.error("\uD83D\uDD04 Updating model recommendations from OpenRouter...");
523
1230
  try {
524
1231
  const topWeeklyProgrammingModels = [
1232
+ "google/gemini-3-pro-preview",
1233
+ "openai/gpt-5.1-codex",
525
1234
  "x-ai/grok-code-fast-1",
526
1235
  "anthropic/claude-sonnet-4.5",
527
1236
  "google/gemini-2.5-flash",
@@ -555,29 +1264,7 @@ async function updateModelsFromOpenRouter() {
555
1264
  }
556
1265
  const model = modelMap.get(modelId);
557
1266
  if (!model) {
558
- console.error(`⚠️ Model ${modelId} not found in OpenRouter API (including with limited metadata)`);
559
- recommendations.push({
560
- id: modelId,
561
- name: modelId.split("/")[1].replace(/-/g, " ").replace(/\b\w/g, (l) => l.toUpperCase()),
562
- description: `${modelId} (metadata pending - not yet available in API)`,
563
- provider: provider.charAt(0).toUpperCase() + provider.slice(1),
564
- category: "programming",
565
- priority: recommendations.length + 1,
566
- pricing: {
567
- input: "N/A",
568
- output: "N/A",
569
- average: "N/A"
570
- },
571
- context: "N/A",
572
- maxOutputTokens: null,
573
- modality: "text->text",
574
- supportsTools: false,
575
- supportsReasoning: false,
576
- supportsVision: false,
577
- isModerated: false,
578
- recommended: true
579
- });
580
- providers.add(provider);
1267
+ console.error(`⚠️ Model ${modelId} not found in OpenRouter API - skipping`);
581
1268
  continue;
582
1269
  }
583
1270
  const name = model.name || modelId;
@@ -677,20 +1364,29 @@ OPTIONS:
677
1364
  -v, --verbose Show [claudish] log messages (default in interactive mode)
678
1365
  --json Output in JSON format for tool integration (implies --quiet)
679
1366
  --stdin Read prompt from stdin (useful for large prompts or piping)
1367
+ --free Show only FREE models in the interactive selector
680
1368
  --monitor Monitor mode - proxy to REAL Anthropic API and log all traffic
681
1369
  --no-auto-approve Disable auto permission skip (prompts enabled)
682
1370
  --dangerous Pass --dangerouslyDisableSandbox to Claude Code
683
1371
  --cost-tracker Enable cost tracking for API usage (NB!)
684
1372
  --audit-costs Show cost analysis report
685
1373
  --reset-costs Reset accumulated cost statistics
686
- --list-models List available OpenRouter models (auto-updates if stale >2 days)
687
- --list-models --json Output model list in JSON format
1374
+ --models List ALL OpenRouter models grouped by provider
1375
+ --models <query> Fuzzy search all models by name, ID, or description
1376
+ --top-models List recommended/top programming models (curated)
1377
+ --json Output in JSON format (use with --models or --top-models)
688
1378
  --force-update Force refresh model cache from OpenRouter API
689
1379
  --version Show version information
690
1380
  -h, --help Show this help message
691
1381
  --help-ai Show AI agent usage guide (file-based patterns, sub-agents)
692
1382
  --init Install Claudish skill in current project (.claude/skills/)
693
1383
 
1384
+ MODEL MAPPING (per-role override):
1385
+ --model-opus <model> Model for Opus role (planning, complex tasks)
1386
+ --model-sonnet <model> Model for Sonnet role (default coding)
1387
+ --model-haiku <model> Model for Haiku role (fast tasks, background)
1388
+ --model-subagent <model> Model for sub-agents (Task tool)
1389
+
694
1390
  CUSTOM MODELS:
695
1391
  Claudish accepts ANY valid OpenRouter model ID, even if not in --list-models
696
1392
  Example: claudish --model your_provider/custom-model-123 "task"
@@ -706,18 +1402,32 @@ NOTES:
706
1402
  • Use --dangerous to disable sandbox (use with extreme caution!)
707
1403
 
708
1404
  ENVIRONMENT VARIABLES:
1405
+ Claudish automatically loads .env file from current directory.
1406
+
709
1407
  OPENROUTER_API_KEY Required: Your OpenRouter API key
710
1408
  CLAUDISH_MODEL Default model to use (takes priority)
711
- ANTHROPIC_MODEL Claude Code standard: model to use (fallback if CLAUDISH_MODEL not set)
712
- ANTHROPIC_SMALL_FAST_MODEL Claude Code standard: fast model (auto-set by claudish)
1409
+ ANTHROPIC_MODEL Claude Code standard: model to use (fallback)
713
1410
  CLAUDISH_PORT Default port for proxy
714
- CLAUDISH_ACTIVE_MODEL_NAME Auto-set by claudish (read-only) - shows active model in status line
1411
+ CLAUDISH_ACTIVE_MODEL_NAME Auto-set by claudish (read-only) - shows active model
1412
+
1413
+ Model mapping (CLAUDISH_* takes priority over ANTHROPIC_DEFAULT_*):
1414
+ CLAUDISH_MODEL_OPUS Override model for Opus role
1415
+ CLAUDISH_MODEL_SONNET Override model for Sonnet role
1416
+ CLAUDISH_MODEL_HAIKU Override model for Haiku role
1417
+ CLAUDISH_MODEL_SUBAGENT Override model for sub-agents
1418
+ ANTHROPIC_DEFAULT_OPUS_MODEL Claude Code standard: Opus model (fallback)
1419
+ ANTHROPIC_DEFAULT_SONNET_MODEL Claude Code standard: Sonnet model (fallback)
1420
+ ANTHROPIC_DEFAULT_HAIKU_MODEL Claude Code standard: Haiku model (fallback)
1421
+ CLAUDE_CODE_SUBAGENT_MODEL Claude Code standard: sub-agent model (fallback)
715
1422
 
716
1423
  EXAMPLES:
717
1424
  # Interactive mode (default) - shows model selector
718
1425
  claudish
719
1426
  claudish --interactive
720
1427
 
1428
+ # Interactive mode with only FREE models
1429
+ claudish --free
1430
+
721
1431
  # Interactive mode with pre-selected model
722
1432
  claudish --model x-ai/grok-code-fast-1
723
1433
 
@@ -725,6 +1435,12 @@ EXAMPLES:
725
1435
  claudish --model openai/gpt-5-codex "implement user authentication"
726
1436
  claudish --model x-ai/grok-code-fast-1 "add tests for login"
727
1437
 
1438
+ # Per-role model mapping (use different models for different Claude Code roles)
1439
+ claudish --model-opus openai/gpt-5 --model-sonnet x-ai/grok-code-fast-1 --model-haiku minimax/minimax-m2
1440
+
1441
+ # Hybrid: Native Anthropic for Opus, OpenRouter for Sonnet/Haiku
1442
+ claudish --model-opus claude-3-opus-20240229 --model-sonnet x-ai/grok-code-fast-1
1443
+
728
1444
  # Use stdin for large prompts (e.g., git diffs, code review)
729
1445
  echo "Review this code..." | claudish --stdin --model x-ai/grok-code-fast-1
730
1446
  git diff | claudish --stdin --model openai/gpt-5-codex "Review these changes"
@@ -754,9 +1470,11 @@ EXAMPLES:
754
1470
  claudish --verbose "analyze code structure"
755
1471
 
756
1472
  AVAILABLE MODELS:
757
- List models: claudish --list-models
758
- JSON output: claudish --list-models --json
759
- Force update: claudish --list-models --force-update
1473
+ List all models: claudish --models
1474
+ Search models: claudish --models <query>
1475
+ Top recommended: claudish --top-models
1476
+ JSON output: claudish --models --json | claudish --top-models --json
1477
+ Force cache update: claudish --models --force-update
760
1478
  (Cache auto-updates every 2 days)
761
1479
 
762
1480
  MORE INFO:
@@ -939,6 +1657,114 @@ init_config();
939
1657
 
940
1658
  // src/simple-selector.ts
941
1659
  import { createInterface } from "readline";
1660
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync4, existsSync as existsSync3 } from "node:fs";
1661
+ import { join as join4, dirname as dirname3 } from "node:path";
1662
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
1663
+ var __filename4 = fileURLToPath3(import.meta.url);
1664
+ var __dirname4 = dirname3(__filename4);
1665
+ var ALL_MODELS_JSON_PATH2 = join4(__dirname4, "../all-models.json");
1666
+ var CACHE_MAX_AGE_DAYS2 = 2;
1667
+ function loadEnhancedModels() {
1668
+ const jsonPath = join4(__dirname4, "../recommended-models.json");
1669
+ if (existsSync3(jsonPath)) {
1670
+ try {
1671
+ const jsonContent = readFileSync3(jsonPath, "utf-8");
1672
+ const data = JSON.parse(jsonContent);
1673
+ return data.models || [];
1674
+ } catch {
1675
+ return [];
1676
+ }
1677
+ }
1678
+ return [];
1679
+ }
1680
+ var TRUSTED_FREE_PROVIDERS = [
1681
+ "google",
1682
+ "openai",
1683
+ "x-ai",
1684
+ "deepseek",
1685
+ "qwen",
1686
+ "alibaba",
1687
+ "meta-llama",
1688
+ "microsoft",
1689
+ "mistralai",
1690
+ "nvidia",
1691
+ "cohere"
1692
+ ];
1693
+ async function loadFreeModels() {
1694
+ let allModels = [];
1695
+ if (existsSync3(ALL_MODELS_JSON_PATH2)) {
1696
+ try {
1697
+ const cacheData = JSON.parse(readFileSync3(ALL_MODELS_JSON_PATH2, "utf-8"));
1698
+ const lastUpdated = new Date(cacheData.lastUpdated);
1699
+ const now = new Date;
1700
+ const ageInDays = (now.getTime() - lastUpdated.getTime()) / (1000 * 60 * 60 * 24);
1701
+ if (ageInDays <= CACHE_MAX_AGE_DAYS2) {
1702
+ allModels = cacheData.models;
1703
+ }
1704
+ } catch {}
1705
+ }
1706
+ if (allModels.length === 0) {
1707
+ console.error("\uD83D\uDD04 Fetching models from OpenRouter...");
1708
+ try {
1709
+ const response = await fetch("https://openrouter.ai/api/v1/models");
1710
+ if (!response.ok)
1711
+ throw new Error(`API returned ${response.status}`);
1712
+ const data = await response.json();
1713
+ allModels = data.data;
1714
+ writeFileSync4(ALL_MODELS_JSON_PATH2, JSON.stringify({
1715
+ lastUpdated: new Date().toISOString(),
1716
+ models: allModels
1717
+ }), "utf-8");
1718
+ console.error(`✅ Cached ${allModels.length} models`);
1719
+ } catch (error) {
1720
+ console.error(`❌ Failed to fetch models: ${error}`);
1721
+ return [];
1722
+ }
1723
+ }
1724
+ const freeModels = allModels.filter((model) => {
1725
+ const promptPrice = parseFloat(model.pricing?.prompt || "0");
1726
+ const completionPrice = parseFloat(model.pricing?.completion || "0");
1727
+ const isFree = promptPrice === 0 && completionPrice === 0;
1728
+ if (!isFree)
1729
+ return false;
1730
+ const provider = model.id.split("/")[0].toLowerCase();
1731
+ return TRUSTED_FREE_PROVIDERS.includes(provider);
1732
+ });
1733
+ freeModels.sort((a, b) => {
1734
+ const contextA = a.context_length || a.top_provider?.context_length || 0;
1735
+ const contextB = b.context_length || b.top_provider?.context_length || 0;
1736
+ return contextB - contextA;
1737
+ });
1738
+ const seenBase = new Set;
1739
+ const dedupedModels = freeModels.filter((model) => {
1740
+ const baseId = model.id.replace(/:free$/, "");
1741
+ if (seenBase.has(baseId)) {
1742
+ return false;
1743
+ }
1744
+ seenBase.add(baseId);
1745
+ return true;
1746
+ });
1747
+ const topModels = dedupedModels.slice(0, 15);
1748
+ return topModels.map((model) => {
1749
+ const provider = model.id.split("/")[0];
1750
+ const contextLen = model.context_length || model.top_provider?.context_length || 0;
1751
+ return {
1752
+ id: model.id,
1753
+ name: model.name || model.id,
1754
+ description: model.description || "",
1755
+ provider: provider.charAt(0).toUpperCase() + provider.slice(1),
1756
+ pricing: {
1757
+ input: "FREE",
1758
+ output: "FREE",
1759
+ average: "FREE"
1760
+ },
1761
+ context: contextLen > 0 ? `${Math.round(contextLen / 1000)}K` : "N/A",
1762
+ supportsTools: (model.supported_parameters || []).includes("tools"),
1763
+ supportsReasoning: (model.supported_parameters || []).includes("reasoning"),
1764
+ supportsVision: (model.architecture?.input_modalities || []).includes("image")
1765
+ };
1766
+ });
1767
+ }
942
1768
  async function promptForApiKey() {
943
1769
  return new Promise((resolve) => {
944
1770
  console.log(`
@@ -988,27 +1814,75 @@ async function promptForApiKey() {
988
1814
  });
989
1815
  });
990
1816
  }
991
- async function selectModelInteractively() {
992
- const models = getAvailableModels();
993
- const modelInfo = loadModelInfo();
1817
+ async function selectModelInteractively(options = {}) {
1818
+ const { freeOnly = false } = options;
1819
+ let displayModels;
1820
+ let enhancedMap;
1821
+ if (freeOnly) {
1822
+ const freeModels = await loadFreeModels();
1823
+ if (freeModels.length === 0) {
1824
+ console.error("❌ No free models found or failed to fetch models");
1825
+ process.exit(1);
1826
+ }
1827
+ displayModels = freeModels.map((m) => m.id);
1828
+ enhancedMap = new Map;
1829
+ for (const m of freeModels) {
1830
+ enhancedMap.set(m.id, m);
1831
+ }
1832
+ } else {
1833
+ displayModels = getAvailableModels();
1834
+ const enhancedModels = loadEnhancedModels();
1835
+ enhancedMap = new Map;
1836
+ for (const m of enhancedModels) {
1837
+ enhancedMap.set(m.id, m);
1838
+ }
1839
+ }
1840
+ const models = freeOnly ? displayModels : displayModels;
994
1841
  return new Promise((resolve) => {
995
- console.log(`
996
- \x1B[1m\x1B[36mSelect an OpenRouter model:\x1B[0m
997
- `);
998
- models.forEach((model, index) => {
999
- const info = modelInfo[model];
1000
- const displayName = info ? info.name : model;
1001
- const description = info ? info.description : "Custom model entry";
1002
- const provider = info ? info.provider : "";
1003
- console.log(` ${index + 1}. \x1B[1m${displayName}\x1B[0m`);
1004
- if (provider && provider !== "Custom") {
1005
- console.log(` \x1B[2m${provider} - ${description}\x1B[0m`);
1842
+ const RESET = "\x1B[0m";
1843
+ const BOLD = "\x1B[1m";
1844
+ const DIM = "\x1B[2m";
1845
+ const CYAN = "\x1B[36m";
1846
+ const GREEN = "\x1B[32m";
1847
+ const YELLOW = "\x1B[33m";
1848
+ const MAGENTA = "\x1B[35m";
1849
+ const pad = (text, width) => {
1850
+ if (text.length > width)
1851
+ return text.slice(0, width - 3) + "...";
1852
+ return text + " ".repeat(width - text.length);
1853
+ };
1854
+ const headerText = freeOnly ? "Select a FREE OpenRouter Model" : "Select an OpenRouter Model";
1855
+ const headerPadding = " ".repeat(82 - 4 - headerText.length);
1856
+ console.log("");
1857
+ console.log(`${DIM}╭${"─".repeat(82)}╮${RESET}`);
1858
+ console.log(`${DIM}│${RESET} ${BOLD}${CYAN}${headerText}${RESET}${headerPadding}${DIM}│${RESET}`);
1859
+ console.log(`${DIM}├${"─".repeat(82)}┤${RESET}`);
1860
+ console.log(`${DIM}│${RESET} ${DIM}# Model Provider Pricing Context Caps${RESET} ${DIM}│${RESET}`);
1861
+ console.log(`${DIM}├${"─".repeat(82)}┤${RESET}`);
1862
+ models.forEach((modelId, index) => {
1863
+ const num = (index + 1).toString().padStart(2);
1864
+ const enhanced = enhancedMap.get(modelId);
1865
+ if (modelId === "custom") {
1866
+ console.log(`${DIM}│${RESET} ${YELLOW}${num}${RESET} ${DIM}Enter custom OpenRouter model ID...${RESET}${" ".repeat(40)}${DIM}│${RESET}`);
1867
+ } else if (enhanced) {
1868
+ const shortId = pad(modelId, 33);
1869
+ const provider = pad(enhanced.provider || "N/A", 10);
1870
+ const pricing = pad(enhanced.pricing?.average || "N/A", 9);
1871
+ const context = pad(enhanced.context || "N/A", 7);
1872
+ const tools = enhanced.supportsTools ? "✓" : "·";
1873
+ const reasoning = enhanced.supportsReasoning ? "✓" : "·";
1874
+ const vision = enhanced.supportsVision ? "✓" : "·";
1875
+ console.log(`${DIM}│${RESET} ${GREEN}${num}${RESET} ${BOLD}${shortId}${RESET} ${CYAN}${provider}${RESET} ${MAGENTA}${pricing}${RESET} ${context} ${tools} ${reasoning} ${vision} ${DIM}│${RESET}`);
1006
1876
  } else {
1007
- console.log(` \x1B[2m${description}\x1B[0m`);
1877
+ const shortId = pad(modelId, 33);
1878
+ console.log(`${DIM}│${RESET} ${GREEN}${num}${RESET} ${shortId} ${DIM}${pad("N/A", 10)} ${pad("N/A", 9)} ${pad("N/A", 7)}${RESET} · · · ${DIM}│${RESET}`);
1008
1879
  }
1009
- console.log("");
1010
1880
  });
1011
- console.log(`\x1B[2mEnter number (1-${models.length}) or 'q' to quit:\x1B[0m`);
1881
+ console.log(`${DIM}├${"─".repeat(82)}┤${RESET}`);
1882
+ console.log(`${DIM}│${RESET} ${DIM}Caps: ✓/· = Tools, Reasoning, Vision${RESET}${" ".repeat(44)}${DIM}│${RESET}`);
1883
+ console.log(`${DIM}╰${"─".repeat(82)}╯${RESET}`);
1884
+ console.log("");
1885
+ console.log(`${DIM}Enter number (1-${models.length}) or 'q' to quit:${RESET}`);
1012
1886
  const rl = createInterface({
1013
1887
  input: process.stdin,
1014
1888
  output: process.stdout,
@@ -1083,8 +1957,8 @@ async function selectModelInteractively() {
1083
1957
  }
1084
1958
 
1085
1959
  // src/logger.ts
1086
- import { writeFileSync as writeFileSync4, appendFile, existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
1087
- import { join as join4 } from "path";
1960
+ import { writeFileSync as writeFileSync5, appendFile, existsSync as existsSync4, mkdirSync as mkdirSync2 } from "fs";
1961
+ import { join as join5 } from "path";
1088
1962
  var logFilePath = null;
1089
1963
  var logLevel = "info";
1090
1964
  var logBuffer = [];
@@ -1114,7 +1988,7 @@ function scheduleFlush() {
1114
1988
  flushTimer = null;
1115
1989
  }
1116
1990
  if (logFilePath && logBuffer.length > 0) {
1117
- writeFileSync4(logFilePath, logBuffer.join(""), { flag: "a" });
1991
+ writeFileSync5(logFilePath, logBuffer.join(""), { flag: "a" });
1118
1992
  logBuffer = [];
1119
1993
  }
1120
1994
  });
@@ -1129,13 +2003,13 @@ function initLogger(debugMode, level = "info") {
1129
2003
  return;
1130
2004
  }
1131
2005
  logLevel = level;
1132
- const logsDir = join4(process.cwd(), "logs");
1133
- if (!existsSync3(logsDir)) {
2006
+ const logsDir = join5(process.cwd(), "logs");
2007
+ if (!existsSync4(logsDir)) {
1134
2008
  mkdirSync2(logsDir, { recursive: true });
1135
2009
  }
1136
2010
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-").split("T").join("_").slice(0, -5);
1137
- logFilePath = join4(logsDir, `claudish_${timestamp}.log`);
1138
- writeFileSync4(logFilePath, `Claudish Debug Log - ${new Date().toISOString()}
2011
+ logFilePath = join5(logsDir, `claudish_${timestamp}.log`);
2012
+ writeFileSync5(logFilePath, `Claudish Debug Log - ${new Date().toISOString()}
1139
2013
  Log Level: ${level}
1140
2014
  ${"=".repeat(80)}
1141
2015
 
@@ -3369,89 +4243,122 @@ var serve = (options, listeningListener) => {
3369
4243
  return server;
3370
4244
  };
3371
4245
 
3372
- // src/proxy-server.ts
3373
- import { writeFileSync as writeFileSync5 } from "node:fs";
3374
-
3375
- // src/transform.ts
3376
- function removeUriFormat(schema) {
3377
- if (!schema || typeof schema !== "object")
3378
- return schema;
3379
- if (schema.type === "string" && schema.format === "uri") {
3380
- const { format, ...rest } = schema;
3381
- return rest;
3382
- }
3383
- if (Array.isArray(schema)) {
3384
- return schema.map((item) => removeUriFormat(item));
3385
- }
3386
- const result = {};
3387
- for (const key in schema) {
3388
- if (key === "properties" && typeof schema[key] === "object") {
3389
- result[key] = {};
3390
- for (const propKey in schema[key]) {
3391
- result[key][propKey] = removeUriFormat(schema[key][propKey]);
3392
- }
3393
- } else if (key === "items" && typeof schema[key] === "object") {
3394
- result[key] = removeUriFormat(schema[key]);
3395
- } else if (key === "additionalProperties" && typeof schema[key] === "object") {
3396
- result[key] = removeUriFormat(schema[key]);
3397
- } else if (["anyOf", "allOf", "oneOf"].includes(key) && Array.isArray(schema[key])) {
3398
- result[key] = schema[key].map((item) => removeUriFormat(item));
4246
+ // src/handlers/native-handler.ts
4247
+ class NativeHandler {
4248
+ apiKey;
4249
+ baseUrl;
4250
+ constructor(apiKey) {
4251
+ this.apiKey = apiKey;
4252
+ this.baseUrl = process.env.ANTHROPIC_BASE_URL || "https://api.anthropic.com";
4253
+ }
4254
+ async handle(c, payload) {
4255
+ const originalHeaders = c.req.header();
4256
+ const target = payload.model;
4257
+ log(`
4258
+ === [NATIVE] Claude Code → Anthropic API Request ===`);
4259
+ const extractedApiKey = originalHeaders["x-api-key"] || originalHeaders["authorization"] || this.apiKey;
4260
+ if (!extractedApiKey) {
4261
+ log("[Native] WARNING: No API key found in headers!");
4262
+ log("[Native] Looking for: x-api-key or authorization header");
3399
4263
  } else {
3400
- result[key] = removeUriFormat(schema[key]);
4264
+ log(`API Key found: ${maskCredential(extractedApiKey)}`);
3401
4265
  }
3402
- }
3403
- return result;
3404
- }
3405
- function transformOpenAIToClaude(claudeRequestInput) {
3406
- const req = JSON.parse(JSON.stringify(claudeRequestInput));
3407
- const isO3Model = typeof req.model === "string" && (req.model.includes("o3") || req.model.includes("o1"));
3408
- if (Array.isArray(req.system)) {
3409
- req.system = req.system.map((item) => {
3410
- if (typeof item === "string") {
3411
- return item;
3412
- } else if (item && typeof item === "object") {
3413
- if (item.type === "text" && item.text) {
3414
- return item.text;
3415
- } else if (item.type === "text" && item.content) {
3416
- return item.content;
3417
- } else if (item.text) {
3418
- return item.text;
3419
- } else if (item.content) {
3420
- return typeof item.content === "string" ? item.content : JSON.stringify(item.content);
3421
- }
3422
- }
3423
- return JSON.stringify(item);
3424
- }).filter((text) => text && text.trim() !== "").join(`
3425
-
4266
+ log(`Request body (Model: ${target}):`);
4267
+ log(`=== End Request ===
3426
4268
  `);
3427
- }
3428
- if (!Array.isArray(req.messages)) {
3429
- if (req.messages == null)
3430
- req.messages = [];
3431
- else
3432
- req.messages = [req.messages];
3433
- }
3434
- if (!Array.isArray(req.tools))
3435
- req.tools = [];
3436
- for (const t of req.tools) {
3437
- if (t && t.input_schema) {
3438
- t.input_schema = removeUriFormat(t.input_schema);
4269
+ const headers = {
4270
+ "Content-Type": "application/json",
4271
+ "anthropic-version": originalHeaders["anthropic-version"] || "2023-06-01"
4272
+ };
4273
+ if (originalHeaders["authorization"]) {
4274
+ headers["authorization"] = originalHeaders["authorization"];
4275
+ }
4276
+ if (originalHeaders["x-api-key"]) {
4277
+ headers["x-api-key"] = originalHeaders["x-api-key"];
4278
+ } else if (extractedApiKey) {
4279
+ headers["x-api-key"] = extractedApiKey;
4280
+ }
4281
+ if (originalHeaders["anthropic-beta"]) {
4282
+ headers["anthropic-beta"] = originalHeaders["anthropic-beta"];
4283
+ }
4284
+ try {
4285
+ const anthropicResponse = await fetch(`${this.baseUrl}/v1/messages`, {
4286
+ method: "POST",
4287
+ headers,
4288
+ body: JSON.stringify(payload)
4289
+ });
4290
+ const contentType = anthropicResponse.headers.get("content-type") || "";
4291
+ if (contentType.includes("text/event-stream")) {
4292
+ log("[Native] Streaming response detected");
4293
+ return c.body(new ReadableStream({
4294
+ async start(controller) {
4295
+ const reader = anthropicResponse.body?.getReader();
4296
+ if (!reader)
4297
+ throw new Error("No reader");
4298
+ const decoder = new TextDecoder;
4299
+ let buffer = "";
4300
+ let eventLog = "";
4301
+ try {
4302
+ while (true) {
4303
+ const { done, value } = await reader.read();
4304
+ if (done)
4305
+ break;
4306
+ controller.enqueue(value);
4307
+ buffer += decoder.decode(value, { stream: true });
4308
+ const lines = buffer.split(`
4309
+ `);
4310
+ buffer = lines.pop() || "";
4311
+ for (const line of lines)
4312
+ if (line.trim())
4313
+ eventLog += line + `
4314
+ `;
4315
+ }
4316
+ if (eventLog)
4317
+ log(eventLog);
4318
+ controller.close();
4319
+ } catch (e) {
4320
+ log(`[Native] Stream Error: ${e}`);
4321
+ controller.close();
4322
+ }
4323
+ }
4324
+ }), {
4325
+ headers: {
4326
+ "Content-Type": contentType,
4327
+ "Cache-Control": "no-cache",
4328
+ Connection: "keep-alive",
4329
+ "anthropic-version": "2023-06-01"
4330
+ }
4331
+ });
4332
+ }
4333
+ const data = await anthropicResponse.json();
4334
+ log(`
4335
+ === [NATIVE] Response ===`);
4336
+ log(JSON.stringify(data, null, 2));
4337
+ const responseHeaders = { "Content-Type": "application/json" };
4338
+ if (anthropicResponse.headers.has("anthropic-version")) {
4339
+ responseHeaders["anthropic-version"] = anthropicResponse.headers.get("anthropic-version");
4340
+ }
4341
+ return c.json(data, { status: anthropicResponse.status, headers: responseHeaders });
4342
+ } catch (error) {
4343
+ log(`[Native] Fetch Error: ${error}`);
4344
+ return c.json({ error: { type: "api_error", message: String(error) } }, 500);
3439
4345
  }
3440
4346
  }
3441
- const dropped = [];
3442
- return {
3443
- claudeRequest: req,
3444
- droppedParams: dropped,
3445
- isO3Model
3446
- };
4347
+ async shutdown() {}
3447
4348
  }
3448
4349
 
4350
+ // src/handlers/openrouter-handler.ts
4351
+ import { writeFileSync as writeFileSync6 } from "node:fs";
4352
+
3449
4353
  // src/adapters/base-adapter.ts
3450
4354
  class BaseModelAdapter {
3451
4355
  modelId;
3452
4356
  constructor(modelId) {
3453
4357
  this.modelId = modelId;
3454
4358
  }
4359
+ prepareRequest(request, originalRequest) {
4360
+ return request;
4361
+ }
3455
4362
  reset() {}
3456
4363
  }
3457
4364
 
@@ -3515,6 +4422,22 @@ class GrokAdapter extends BaseModelAdapter {
3515
4422
  wasTransformed: true
3516
4423
  };
3517
4424
  }
4425
+ prepareRequest(request, originalRequest) {
4426
+ const modelId = this.modelId || "";
4427
+ if (originalRequest.thinking) {
4428
+ const supportsReasoningEffort = modelId.includes("mini");
4429
+ if (supportsReasoningEffort) {
4430
+ const { budget_tokens } = originalRequest.thinking;
4431
+ const effort = budget_tokens >= 20000 ? "high" : "low";
4432
+ request.reasoning_effort = effort;
4433
+ log(`[GrokAdapter] Mapped budget ${budget_tokens} -> reasoning_effort: ${effort}`);
4434
+ } else {
4435
+ log(`[GrokAdapter] Model ${modelId} does not support reasoning params. Stripping.`);
4436
+ }
4437
+ delete request.thinking;
4438
+ }
4439
+ return request;
4440
+ }
3518
4441
  parseXmlParameters(xmlContent) {
3519
4442
  const params = {};
3520
4443
  const paramPattern = /<xai:parameter name="([^"]+)">([^<]*)<\/xai:parameter>/g;
@@ -3541,12 +4464,191 @@ class GrokAdapter extends BaseModelAdapter {
3541
4464
  }
3542
4465
  }
3543
4466
 
4467
+ // src/adapters/gemini-adapter.ts
4468
+ class GeminiAdapter extends BaseModelAdapter {
4469
+ thoughtSignatures = new Map;
4470
+ processTextContent(textContent, accumulatedText) {
4471
+ return {
4472
+ cleanedText: textContent,
4473
+ extractedToolCalls: [],
4474
+ wasTransformed: false
4475
+ };
4476
+ }
4477
+ prepareRequest(request, originalRequest) {
4478
+ if (originalRequest.thinking) {
4479
+ const { budget_tokens } = originalRequest.thinking;
4480
+ const modelId = this.modelId || "";
4481
+ if (modelId.includes("gemini-3")) {
4482
+ const level = budget_tokens >= 16000 ? "high" : "low";
4483
+ request.thinking_level = level;
4484
+ log(`[GeminiAdapter] Mapped budget ${budget_tokens} -> thinking_level: ${level}`);
4485
+ } else {
4486
+ const MAX_GEMINI_BUDGET = 24576;
4487
+ const budget = Math.min(budget_tokens, MAX_GEMINI_BUDGET);
4488
+ request.thinking_config = {
4489
+ thinking_budget: budget
4490
+ };
4491
+ log(`[GeminiAdapter] Mapped budget ${budget_tokens} -> thinking_config.thinking_budget: ${budget}`);
4492
+ }
4493
+ delete request.thinking;
4494
+ }
4495
+ return request;
4496
+ }
4497
+ extractThoughtSignaturesFromReasoningDetails(reasoningDetails) {
4498
+ const extracted = new Map;
4499
+ if (!reasoningDetails || !Array.isArray(reasoningDetails)) {
4500
+ return extracted;
4501
+ }
4502
+ for (const detail of reasoningDetails) {
4503
+ if (detail && detail.type === "reasoning.encrypted" && detail.id && detail.data) {
4504
+ this.thoughtSignatures.set(detail.id, detail.data);
4505
+ extracted.set(detail.id, detail.data);
4506
+ }
4507
+ }
4508
+ return extracted;
4509
+ }
4510
+ getThoughtSignature(toolCallId) {
4511
+ return this.thoughtSignatures.get(toolCallId);
4512
+ }
4513
+ hasThoughtSignature(toolCallId) {
4514
+ return this.thoughtSignatures.has(toolCallId);
4515
+ }
4516
+ getAllThoughtSignatures() {
4517
+ return new Map(this.thoughtSignatures);
4518
+ }
4519
+ reset() {
4520
+ this.thoughtSignatures.clear();
4521
+ }
4522
+ shouldHandle(modelId) {
4523
+ return modelId.includes("gemini") || modelId.includes("google/");
4524
+ }
4525
+ getName() {
4526
+ return "GeminiAdapter";
4527
+ }
4528
+ }
4529
+
4530
+ // src/adapters/openai-adapter.ts
4531
+ class OpenAIAdapter extends BaseModelAdapter {
4532
+ processTextContent(textContent, accumulatedText) {
4533
+ return {
4534
+ cleanedText: textContent,
4535
+ extractedToolCalls: [],
4536
+ wasTransformed: false
4537
+ };
4538
+ }
4539
+ prepareRequest(request, originalRequest) {
4540
+ if (originalRequest.thinking) {
4541
+ const { budget_tokens } = originalRequest.thinking;
4542
+ let effort = "medium";
4543
+ if (budget_tokens < 4000)
4544
+ effort = "minimal";
4545
+ else if (budget_tokens < 16000)
4546
+ effort = "low";
4547
+ else if (budget_tokens >= 32000)
4548
+ effort = "high";
4549
+ request.reasoning_effort = effort;
4550
+ delete request.thinking;
4551
+ log(`[OpenAIAdapter] Mapped budget ${budget_tokens} -> reasoning_effort: ${effort}`);
4552
+ }
4553
+ return request;
4554
+ }
4555
+ shouldHandle(modelId) {
4556
+ return modelId.startsWith("openai/") || modelId.includes("o1") || modelId.includes("o3");
4557
+ }
4558
+ getName() {
4559
+ return "OpenAIAdapter";
4560
+ }
4561
+ }
4562
+
4563
+ // src/adapters/qwen-adapter.ts
4564
+ class QwenAdapter extends BaseModelAdapter {
4565
+ processTextContent(textContent, accumulatedText) {
4566
+ return {
4567
+ cleanedText: textContent,
4568
+ extractedToolCalls: [],
4569
+ wasTransformed: false
4570
+ };
4571
+ }
4572
+ prepareRequest(request, originalRequest) {
4573
+ if (originalRequest.thinking) {
4574
+ const { budget_tokens } = originalRequest.thinking;
4575
+ request.enable_thinking = true;
4576
+ request.thinking_budget = budget_tokens;
4577
+ log(`[QwenAdapter] Mapped budget ${budget_tokens} -> enable_thinking: true, thinking_budget: ${budget_tokens}`);
4578
+ delete request.thinking;
4579
+ }
4580
+ return request;
4581
+ }
4582
+ shouldHandle(modelId) {
4583
+ return modelId.includes("qwen") || modelId.includes("alibaba");
4584
+ }
4585
+ getName() {
4586
+ return "QwenAdapter";
4587
+ }
4588
+ }
4589
+
4590
+ // src/adapters/minimax-adapter.ts
4591
+ class MiniMaxAdapter extends BaseModelAdapter {
4592
+ processTextContent(textContent, accumulatedText) {
4593
+ return {
4594
+ cleanedText: textContent,
4595
+ extractedToolCalls: [],
4596
+ wasTransformed: false
4597
+ };
4598
+ }
4599
+ prepareRequest(request, originalRequest) {
4600
+ if (originalRequest.thinking) {
4601
+ request.reasoning_split = true;
4602
+ log(`[MiniMaxAdapter] Enabled reasoning_split: true`);
4603
+ delete request.thinking;
4604
+ }
4605
+ return request;
4606
+ }
4607
+ shouldHandle(modelId) {
4608
+ return modelId.includes("minimax");
4609
+ }
4610
+ getName() {
4611
+ return "MiniMaxAdapter";
4612
+ }
4613
+ }
4614
+
4615
+ // src/adapters/deepseek-adapter.ts
4616
+ class DeepSeekAdapter extends BaseModelAdapter {
4617
+ processTextContent(textContent, accumulatedText) {
4618
+ return {
4619
+ cleanedText: textContent,
4620
+ extractedToolCalls: [],
4621
+ wasTransformed: false
4622
+ };
4623
+ }
4624
+ prepareRequest(request, originalRequest) {
4625
+ if (originalRequest.thinking) {
4626
+ log(`[DeepSeekAdapter] Stripping thinking object (not supported by API)`);
4627
+ delete request.thinking;
4628
+ }
4629
+ return request;
4630
+ }
4631
+ shouldHandle(modelId) {
4632
+ return modelId.includes("deepseek");
4633
+ }
4634
+ getName() {
4635
+ return "DeepSeekAdapter";
4636
+ }
4637
+ }
4638
+
3544
4639
  // src/adapters/adapter-manager.ts
3545
4640
  class AdapterManager {
3546
4641
  adapters;
3547
4642
  defaultAdapter;
3548
4643
  constructor(modelId) {
3549
- this.adapters = [new GrokAdapter(modelId)];
4644
+ this.adapters = [
4645
+ new GrokAdapter(modelId),
4646
+ new GeminiAdapter(modelId),
4647
+ new OpenAIAdapter(modelId),
4648
+ new QwenAdapter(modelId),
4649
+ new MiniMaxAdapter(modelId),
4650
+ new DeepSeekAdapter(modelId)
4651
+ ];
3550
4652
  this.defaultAdapter = new DefaultAdapter(modelId);
3551
4653
  }
3552
4654
  getAdapter() {
@@ -3562,1072 +4664,793 @@ class AdapterManager {
3562
4664
  }
3563
4665
  }
3564
4666
 
3565
- // src/proxy-server.ts
3566
- async function createProxyServer(port, openrouterApiKey, model, monitorMode = false, anthropicApiKey) {
3567
- const OPENROUTER_API_URL2 = "https://openrouter.ai/api/v1/chat/completions";
3568
- const OPENROUTER_HEADERS2 = {
3569
- "HTTP-Referer": "https://github.com/MadAppGang/claude-code",
3570
- "X-Title": "Claudish - OpenRouter Proxy"
3571
- };
3572
- const ANTHROPIC_API_URL = "https://api.anthropic.com/v1/messages";
3573
- const ANTHROPIC_COUNT_TOKENS_URL = "https://api.anthropic.com/v1/messages/count_tokens";
3574
- const app = new Hono2;
3575
- app.use("*", cors());
3576
- app.get("/", (c) => {
3577
- c.header("Cache-Control", "no-cache, no-store, must-revalidate");
3578
- c.header("Pragma", "no-cache");
3579
- c.header("Expires", "0");
3580
- return c.json({
3581
- status: "ok",
3582
- message: monitorMode ? "Claudish monitor proxy - logging Anthropic API traffic" : "Claudish proxy server is running",
3583
- config: {
3584
- mode: monitorMode ? "monitor" : "openrouter",
3585
- model: monitorMode ? "passthrough" : model,
3586
- port,
3587
- upstream: monitorMode ? "Anthropic" : "OpenRouter"
3588
- }
3589
- });
3590
- });
3591
- app.get("/health", (c) => {
3592
- return c.json({
3593
- status: "ok",
3594
- mode: monitorMode ? "monitor" : "openrouter",
3595
- model: monitorMode ? "passthrough" : model,
3596
- port
3597
- });
3598
- });
3599
- app.post("/v1/messages/count_tokens", async (c) => {
3600
- try {
3601
- const body = await c.req.json();
3602
- if (monitorMode) {
3603
- const originalHeaders = c.req.header();
3604
- const extractedApiKey = originalHeaders["x-api-key"] || anthropicApiKey;
3605
- if (!extractedApiKey) {
3606
- log("[Monitor] ERROR: No API key found for token counting");
3607
- return c.json({
3608
- error: {
3609
- type: "authentication_error",
3610
- message: "No API key found in request."
3611
- }
3612
- }, 401);
3613
- }
3614
- log("[Monitor] Token counting request - forwarding to Anthropic");
3615
- log("[Monitor] Request body:");
3616
- log(JSON.stringify(body, null, 2));
3617
- const headers = {
3618
- "Content-Type": "application/json",
3619
- "anthropic-version": originalHeaders["anthropic-version"] || "2023-06-01"
3620
- };
3621
- if (originalHeaders["authorization"]) {
3622
- headers["authorization"] = originalHeaders["authorization"];
3623
- }
3624
- if (extractedApiKey) {
3625
- headers["x-api-key"] = extractedApiKey;
4667
+ // src/middleware/manager.ts
4668
+ class MiddlewareManager {
4669
+ middlewares = [];
4670
+ initialized = false;
4671
+ register(middleware) {
4672
+ this.middlewares.push(middleware);
4673
+ if (isLoggingEnabled()) {
4674
+ logStructured("Middleware Registered", {
4675
+ name: middleware.name,
4676
+ total: this.middlewares.length
4677
+ });
4678
+ }
4679
+ }
4680
+ async initialize() {
4681
+ if (this.initialized) {
4682
+ log("[Middleware] Already initialized, skipping");
4683
+ return;
4684
+ }
4685
+ log(`[Middleware] Initializing ${this.middlewares.length} middleware(s)...`);
4686
+ for (const middleware of this.middlewares) {
4687
+ if (middleware.onInit) {
4688
+ try {
4689
+ await middleware.onInit();
4690
+ log(`[Middleware] ${middleware.name} initialized`);
4691
+ } catch (error) {
4692
+ log(`[Middleware] ERROR: ${middleware.name} initialization failed: ${error}`);
3626
4693
  }
3627
- const response = await fetch(ANTHROPIC_COUNT_TOKENS_URL, {
3628
- method: "POST",
3629
- headers,
3630
- body: JSON.stringify(body)
3631
- });
3632
- const result = await response.json();
3633
- log("[Monitor] Token counting response:");
3634
- log(JSON.stringify(result, null, 2));
3635
- return c.json(result, response.status);
3636
- }
3637
- log("[Proxy] Token counting request (estimating)");
3638
- const systemTokens = body.system ? Math.ceil(body.system.length / 4) : 0;
3639
- const messageTokens = body.messages ? body.messages.reduce((acc, msg) => {
3640
- const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
3641
- return acc + Math.ceil(content.length / 4);
3642
- }, 0) : 0;
3643
- const totalTokens = systemTokens + messageTokens;
3644
- return c.json({
3645
- input_tokens: totalTokens
4694
+ }
4695
+ }
4696
+ this.initialized = true;
4697
+ log("[Middleware] Initialization complete");
4698
+ }
4699
+ getActiveMiddlewares(modelId) {
4700
+ return this.middlewares.filter((m) => m.shouldHandle(modelId));
4701
+ }
4702
+ async beforeRequest(context) {
4703
+ const active = this.getActiveMiddlewares(context.modelId);
4704
+ if (active.length === 0) {
4705
+ return;
4706
+ }
4707
+ if (isLoggingEnabled()) {
4708
+ logStructured("Middleware Chain (beforeRequest)", {
4709
+ modelId: context.modelId,
4710
+ middlewares: active.map((m) => m.name),
4711
+ messageCount: context.messages.length
3646
4712
  });
3647
- } catch (error) {
3648
- log(`[Proxy] Token counting error: ${error}`);
3649
- return c.json({
3650
- error: {
3651
- type: "invalid_request_error",
3652
- message: error instanceof Error ? error.message : "Unknown error"
4713
+ }
4714
+ for (const middleware of active) {
4715
+ try {
4716
+ await middleware.beforeRequest(context);
4717
+ } catch (error) {
4718
+ log(`[Middleware] ERROR in ${middleware.name}.beforeRequest: ${error}`);
4719
+ }
4720
+ }
4721
+ }
4722
+ async afterResponse(context) {
4723
+ const active = this.getActiveMiddlewares(context.modelId);
4724
+ if (active.length === 0) {
4725
+ return;
4726
+ }
4727
+ if (isLoggingEnabled()) {
4728
+ logStructured("Middleware Chain (afterResponse)", {
4729
+ modelId: context.modelId,
4730
+ middlewares: active.map((m) => m.name)
4731
+ });
4732
+ }
4733
+ for (const middleware of active) {
4734
+ if (middleware.afterResponse) {
4735
+ try {
4736
+ await middleware.afterResponse(context);
4737
+ } catch (error) {
4738
+ log(`[Middleware] ERROR in ${middleware.name}.afterResponse: ${error}`);
3653
4739
  }
3654
- }, 400);
4740
+ }
3655
4741
  }
3656
- });
3657
- app.post("/v1/messages", async (c) => {
3658
- try {
3659
- const claudePayload = await c.req.json();
3660
- if (monitorMode) {
3661
- const originalHeaders = c.req.header();
3662
- const extractedApiKey = originalHeaders["x-api-key"] || originalHeaders["authorization"] || anthropicApiKey;
3663
- log(`
3664
- === [MONITOR] Claude Code → Anthropic API Request ===`);
3665
- log(`Headers received: ${JSON.stringify(originalHeaders, null, 2)}`);
3666
- if (!extractedApiKey) {
3667
- log("[Monitor] WARNING: No API key found in headers!");
3668
- log("[Monitor] Looking for: x-api-key or authorization header");
3669
- log("[Monitor] Headers present: " + Object.keys(originalHeaders).join(", "));
3670
- log("[Monitor] This request will fail at Anthropic API");
3671
- } else {
3672
- log(`API Key found: ${maskCredential(extractedApiKey)}`);
4742
+ }
4743
+ async afterStreamChunk(context) {
4744
+ const active = this.getActiveMiddlewares(context.modelId);
4745
+ if (active.length === 0) {
4746
+ return;
4747
+ }
4748
+ if (isLoggingEnabled() && !context.metadata.has("_middlewareLogged")) {
4749
+ logStructured("Middleware Chain (afterStreamChunk)", {
4750
+ modelId: context.modelId,
4751
+ middlewares: active.map((m) => m.name)
4752
+ });
4753
+ context.metadata.set("_middlewareLogged", true);
4754
+ }
4755
+ for (const middleware of active) {
4756
+ if (middleware.afterStreamChunk) {
4757
+ try {
4758
+ await middleware.afterStreamChunk(context);
4759
+ } catch (error) {
4760
+ log(`[Middleware] ERROR in ${middleware.name}.afterStreamChunk: ${error}`);
3673
4761
  }
3674
- log(`Request body:`);
3675
- log(JSON.stringify(claudePayload, null, 2));
3676
- log(`=== End Request ===
3677
- `);
3678
- const headers2 = {
3679
- "Content-Type": "application/json",
3680
- "anthropic-version": originalHeaders["anthropic-version"] || "2023-06-01"
3681
- };
3682
- if (originalHeaders["authorization"]) {
3683
- headers2["authorization"] = originalHeaders["authorization"];
3684
- log(`[Monitor] Forwarding OAuth token: ${maskCredential(originalHeaders["authorization"])}`);
4762
+ }
4763
+ }
4764
+ }
4765
+ async afterStreamComplete(modelId, metadata) {
4766
+ const active = this.getActiveMiddlewares(modelId);
4767
+ if (active.length === 0) {
4768
+ return;
4769
+ }
4770
+ for (const middleware of active) {
4771
+ if (middleware.afterStreamComplete) {
4772
+ try {
4773
+ await middleware.afterStreamComplete(metadata);
4774
+ } catch (error) {
4775
+ log(`[Middleware] ERROR in ${middleware.name}.afterStreamComplete: ${error}`);
3685
4776
  }
3686
- if (originalHeaders["x-api-key"]) {
3687
- headers2["x-api-key"] = originalHeaders["x-api-key"];
3688
- log(`[Monitor] Forwarding API key: ${maskCredential(originalHeaders["x-api-key"])}`);
4777
+ }
4778
+ }
4779
+ }
4780
+ }
4781
+ // src/middleware/gemini-thought-signature.ts
4782
+ class GeminiThoughtSignatureMiddleware {
4783
+ name = "GeminiThoughtSignature";
4784
+ persistentReasoningDetails = new Map;
4785
+ shouldHandle(modelId) {
4786
+ return modelId.includes("gemini") || modelId.includes("google/");
4787
+ }
4788
+ onInit() {
4789
+ log("[Gemini] Thought signature middleware initialized");
4790
+ }
4791
+ beforeRequest(context) {
4792
+ if (this.persistentReasoningDetails.size === 0) {
4793
+ return;
4794
+ }
4795
+ if (isLoggingEnabled()) {
4796
+ logStructured("[Gemini] Injecting reasoning_details", {
4797
+ cacheSize: this.persistentReasoningDetails.size,
4798
+ messageCount: context.messages.length
4799
+ });
4800
+ }
4801
+ let injected = 0;
4802
+ for (const msg of context.messages) {
4803
+ if (msg.role === "assistant" && msg.tool_calls) {
4804
+ for (const [msgId, cached] of this.persistentReasoningDetails.entries()) {
4805
+ const hasMatchingToolCall = msg.tool_calls.some((tc) => cached.tool_call_ids.has(tc.id));
4806
+ if (hasMatchingToolCall) {
4807
+ msg.reasoning_details = cached.reasoning_details;
4808
+ injected++;
4809
+ if (isLoggingEnabled()) {
4810
+ logStructured("[Gemini] Reasoning details added to assistant message", {
4811
+ message_id: msgId,
4812
+ reasoning_blocks: cached.reasoning_details.length,
4813
+ tool_calls: msg.tool_calls.length
4814
+ });
4815
+ }
4816
+ break;
4817
+ }
3689
4818
  }
3690
- if (originalHeaders["anthropic-beta"]) {
3691
- headers2["anthropic-beta"] = originalHeaders["anthropic-beta"];
4819
+ if (!msg.reasoning_details && isLoggingEnabled()) {
4820
+ log(`[Gemini] WARNING: No reasoning_details found for assistant message with tool_calls`);
4821
+ log(`[Gemini] Tool call IDs: ${msg.tool_calls.map((tc) => tc.id).join(", ")}`);
3692
4822
  }
3693
- const anthropicResponse = await fetch(ANTHROPIC_API_URL, {
3694
- method: "POST",
3695
- headers: headers2,
3696
- body: JSON.stringify(claudePayload)
3697
- });
3698
- const contentType2 = anthropicResponse.headers.get("content-type") || "";
3699
- if (contentType2.includes("text/event-stream")) {
3700
- log("[Monitor] Streaming response detected");
3701
- return c.body(new ReadableStream({
3702
- async start(controller) {
3703
- const encoder = new TextEncoder;
3704
- const reader = anthropicResponse.body?.getReader();
3705
- if (!reader) {
3706
- throw new Error("Response body is not readable");
3707
- }
3708
- const decoder = new TextDecoder;
3709
- let buffer = "";
3710
- let eventLog = "";
3711
- log(`
3712
- === [MONITOR] Anthropic API → Claude Code Response (Streaming) ===`);
3713
- try {
3714
- while (true) {
3715
- const { done, value } = await reader.read();
3716
- if (done) {
3717
- log(`
3718
- === End Streaming Response ===
3719
- `);
3720
- break;
3721
- }
3722
- controller.enqueue(value);
3723
- buffer += decoder.decode(value, { stream: true });
3724
- const lines = buffer.split(`
3725
- `);
3726
- buffer = lines.pop() || "";
3727
- for (const line of lines) {
3728
- if (line.trim()) {
3729
- eventLog += line + `
3730
- `;
3731
- }
3732
- }
3733
- }
3734
- if (eventLog) {
3735
- log(eventLog);
3736
- }
3737
- controller.close();
3738
- } catch (error) {
3739
- log(`[Monitor] Streaming error: ${error}`);
3740
- controller.close();
4823
+ }
4824
+ }
4825
+ if (isLoggingEnabled() && injected > 0) {
4826
+ logStructured("[Gemini] Signature injection complete", {
4827
+ injected,
4828
+ cacheSize: this.persistentReasoningDetails.size
4829
+ });
4830
+ log("[Gemini] DEBUG: Messages after injection:");
4831
+ for (let i = 0;i < context.messages.length; i++) {
4832
+ const msg = context.messages[i];
4833
+ log(`[Gemini] Message ${i}: role=${msg.role}, has_content=${!!msg.content}, has_tool_calls=${!!msg.tool_calls}, tool_call_id=${msg.tool_call_id || "N/A"}`);
4834
+ if (msg.role === "assistant" && msg.tool_calls) {
4835
+ log(` - Assistant has ${msg.tool_calls.length} tool call(s), content="${msg.content}"`);
4836
+ for (const tc of msg.tool_calls) {
4837
+ log(` * Tool call: ${tc.id}, function=${tc.function?.name}, has extra_content: ${!!tc.extra_content}, has thought_signature: ${!!tc.extra_content?.google?.thought_signature}`);
4838
+ if (tc.extra_content) {
4839
+ log(` extra_content keys: ${Object.keys(tc.extra_content).join(", ")}`);
4840
+ if (tc.extra_content.google) {
4841
+ log(` google keys: ${Object.keys(tc.extra_content.google).join(", ")}`);
4842
+ log(` thought_signature length: ${tc.extra_content.google.thought_signature?.length || 0}`);
3741
4843
  }
3742
4844
  }
3743
- }), {
3744
- headers: {
3745
- "Content-Type": anthropicResponse.headers.get("content-type") || "text/event-stream",
3746
- "Cache-Control": "no-cache",
3747
- Connection: "keep-alive",
3748
- "anthropic-version": anthropicResponse.headers.get("anthropic-version") || "2023-06-01"
3749
- }
3750
- });
3751
- }
3752
- const responseData = await anthropicResponse.json();
3753
- log(`
3754
- === [MONITOR] Anthropic API → Claude Code Response (JSON) ===`);
3755
- log(JSON.stringify(responseData, null, 2));
3756
- log(`=== End Response ===
3757
- `);
3758
- const responseHeaders = {
3759
- "Content-Type": "application/json"
3760
- };
3761
- const anthropicVersion = anthropicResponse.headers.get("anthropic-version");
3762
- if (anthropicVersion) {
3763
- responseHeaders["anthropic-version"] = anthropicVersion;
4845
+ }
4846
+ } else if (msg.role === "tool") {
4847
+ log(` - Tool result: tool_call_id=${msg.tool_call_id}, has extra_content: ${!!msg.extra_content}`);
3764
4848
  }
3765
- return c.json(responseData, {
3766
- status: anthropicResponse.status,
3767
- headers: responseHeaders
3768
- });
3769
4849
  }
3770
- logStructured("Incoming Request", {
3771
- model,
3772
- messageCount: claudePayload.messages?.length || 0,
3773
- hasSystem: !!claudePayload.system,
3774
- maxTokens: claudePayload.max_tokens,
3775
- temperature: claudePayload.temperature,
3776
- stream: claudePayload.stream
4850
+ }
4851
+ }
4852
+ afterResponse(context) {
4853
+ const response = context.response;
4854
+ const message = response?.choices?.[0]?.message;
4855
+ if (!message) {
4856
+ return;
4857
+ }
4858
+ const reasoningDetails = message.reasoning_details || [];
4859
+ const toolCalls = message.tool_calls || [];
4860
+ if (reasoningDetails.length > 0 && toolCalls.length > 0) {
4861
+ const messageId = `msg_${Date.now()}_${Math.random().toString(36).slice(2)}`;
4862
+ const toolCallIds = new Set(toolCalls.map((tc) => tc.id).filter(Boolean));
4863
+ this.persistentReasoningDetails.set(messageId, {
4864
+ reasoning_details: reasoningDetails,
4865
+ tool_call_ids: toolCallIds
3777
4866
  });
3778
- const { claudeRequest, droppedParams } = transformOpenAIToClaude(claudePayload);
3779
- const messages = [];
3780
- if (claudeRequest.system) {
3781
- let systemContent;
3782
- if (typeof claudeRequest.system === "string") {
3783
- systemContent = claudeRequest.system;
3784
- } else if (Array.isArray(claudeRequest.system)) {
3785
- systemContent = claudeRequest.system.map((item) => {
3786
- if (typeof item === "string")
3787
- return item;
3788
- if (item?.type === "text" && item.text)
3789
- return item.text;
3790
- if (item?.content)
3791
- return typeof item.content === "string" ? item.content : JSON.stringify(item.content);
3792
- return JSON.stringify(item);
3793
- }).join(`
3794
-
3795
- `);
3796
- } else {
3797
- systemContent = JSON.stringify(claudeRequest.system);
3798
- }
3799
- systemContent = filterClaudeIdentity(systemContent);
3800
- messages.push({
3801
- role: "system",
3802
- content: systemContent
4867
+ logStructured("[Gemini] Reasoning details saved (non-streaming)", {
4868
+ message_id: messageId,
4869
+ reasoning_blocks: reasoningDetails.length,
4870
+ tool_calls: toolCallIds.size,
4871
+ total_cached_messages: this.persistentReasoningDetails.size
4872
+ });
4873
+ }
4874
+ }
4875
+ afterStreamChunk(context) {
4876
+ const delta = context.delta;
4877
+ if (!delta)
4878
+ return;
4879
+ if (delta.reasoning_details && delta.reasoning_details.length > 0) {
4880
+ if (!context.metadata.has("reasoning_details")) {
4881
+ context.metadata.set("reasoning_details", []);
4882
+ }
4883
+ const accumulated = context.metadata.get("reasoning_details");
4884
+ accumulated.push(...delta.reasoning_details);
4885
+ if (isLoggingEnabled()) {
4886
+ logStructured("[Gemini] Reasoning details accumulated", {
4887
+ chunk_blocks: delta.reasoning_details.length,
4888
+ total_blocks: accumulated.length
3803
4889
  });
3804
4890
  }
3805
- if (claudeRequest.messages && Array.isArray(claudeRequest.messages)) {
3806
- for (const msg of claudeRequest.messages) {
3807
- if (msg.role === "user") {
3808
- if (Array.isArray(msg.content)) {
3809
- const textParts = [];
3810
- const toolResults = [];
3811
- const seenToolResultIds = new Set;
3812
- for (const block of msg.content) {
3813
- if (block.type === "text") {
3814
- textParts.push(block.text);
3815
- } else if (block.type === "tool_result") {
3816
- if (seenToolResultIds.has(block.tool_use_id)) {
3817
- log(`[Proxy] Skipping duplicate tool_result with tool_use_id: ${block.tool_use_id}`);
3818
- continue;
3819
- }
3820
- seenToolResultIds.add(block.tool_use_id);
3821
- toolResults.push({
3822
- role: "tool",
3823
- content: typeof block.content === "string" ? block.content : JSON.stringify(block.content),
3824
- tool_call_id: block.tool_use_id
3825
- });
3826
- }
3827
- }
3828
- if (toolResults.length > 0) {
3829
- messages.push(...toolResults);
3830
- if (textParts.length > 0) {
3831
- messages.push({
3832
- role: "user",
3833
- content: textParts.join(" ")
3834
- });
3835
- }
3836
- } else if (textParts.length > 0) {
3837
- messages.push({
3838
- role: "user",
3839
- content: textParts.join(" ")
3840
- });
3841
- }
3842
- } else if (typeof msg.content === "string") {
3843
- messages.push({
3844
- role: "user",
3845
- content: msg.content
3846
- });
3847
- }
3848
- } else if (msg.role === "assistant") {
3849
- if (Array.isArray(msg.content)) {
3850
- const textParts = [];
3851
- const toolCalls = [];
3852
- const seenToolIds = new Set;
3853
- for (const block of msg.content) {
3854
- if (block.type === "text") {
3855
- textParts.push(block.text);
3856
- } else if (block.type === "tool_use") {
3857
- if (seenToolIds.has(block.id)) {
3858
- log(`[Proxy] Skipping duplicate tool_use with ID: ${block.id}`);
3859
- continue;
3860
- }
3861
- seenToolIds.add(block.id);
3862
- toolCalls.push({
3863
- id: block.id,
3864
- type: "function",
3865
- function: {
3866
- name: block.name,
3867
- arguments: JSON.stringify(block.input)
3868
- }
3869
- });
3870
- }
3871
- }
3872
- const openAIMsg = { role: "assistant" };
3873
- if (textParts.length > 0) {
3874
- openAIMsg.content = textParts.join(" ");
3875
- } else if (toolCalls.length > 0) {
3876
- openAIMsg.content = null;
3877
- }
3878
- if (toolCalls.length > 0) {
3879
- openAIMsg.tool_calls = toolCalls;
3880
- }
3881
- if (textParts.length > 0 || toolCalls.length > 0) {
3882
- messages.push(openAIMsg);
3883
- }
3884
- } else if (typeof msg.content === "string") {
3885
- messages.push({
3886
- role: "assistant",
3887
- content: msg.content
3888
- });
3889
- }
3890
- }
3891
- }
4891
+ }
4892
+ if (delta.tool_calls) {
4893
+ if (!context.metadata.has("tool_call_ids")) {
4894
+ context.metadata.set("tool_call_ids", new Set);
3892
4895
  }
3893
- const tools = claudeRequest.tools?.filter((tool) => !["BatchTool"].includes(tool.name)).map((tool) => ({
3894
- type: "function",
3895
- function: {
3896
- name: tool.name,
3897
- description: tool.description,
3898
- parameters: removeUriFormat(tool.input_schema)
4896
+ const toolCallIds = context.metadata.get("tool_call_ids");
4897
+ for (const tc of delta.tool_calls) {
4898
+ if (tc.id) {
4899
+ toolCallIds.add(tc.id);
3899
4900
  }
3900
- })) || [];
3901
- if (model.includes("grok") || model.includes("x-ai/")) {
3902
- if (tools.length > 0 && messages.length > 0) {
3903
- if (messages[0]?.role === "system") {
3904
- messages[0].content += `
3905
-
3906
- IMPORTANT: When calling tools, you MUST use the OpenAI tool_calls format with JSON. NEVER use XML format like <xai:function_call>. Use the tools array provided in the request.`;
3907
- log("[Proxy] Added Grok tool format instruction to existing system message");
3908
- } else {
3909
- messages.unshift({
3910
- role: "system",
3911
- content: "IMPORTANT: When calling tools, you MUST use the OpenAI tool_calls format with JSON. NEVER use XML format like <xai:function_call>. Use the tools array provided in the request."
3912
- });
3913
- log("[Proxy] Added Grok tool format instruction as new system message");
3914
- }
4901
+ }
4902
+ }
4903
+ }
4904
+ afterStreamComplete(metadata) {
4905
+ const reasoningDetails = metadata.get("reasoning_details") || [];
4906
+ const toolCallIds = metadata.get("tool_call_ids") || new Set;
4907
+ if (reasoningDetails.length > 0 && toolCallIds.size > 0) {
4908
+ const messageId = `msg_${Date.now()}_${Math.random().toString(36).slice(2)}`;
4909
+ this.persistentReasoningDetails.set(messageId, {
4910
+ reasoning_details: reasoningDetails,
4911
+ tool_call_ids: toolCallIds
4912
+ });
4913
+ logStructured("[Gemini] Streaming complete - reasoning details saved", {
4914
+ message_id: messageId,
4915
+ reasoning_blocks: reasoningDetails.length,
4916
+ tool_calls: toolCallIds.size,
4917
+ total_cached_messages: this.persistentReasoningDetails.size
4918
+ });
4919
+ }
4920
+ }
4921
+ }
4922
+ // src/transform.ts
4923
+ function removeUriFormat(schema) {
4924
+ if (!schema || typeof schema !== "object")
4925
+ return schema;
4926
+ if (schema.type === "string" && schema.format === "uri") {
4927
+ const { format, ...rest } = schema;
4928
+ return rest;
4929
+ }
4930
+ if (Array.isArray(schema)) {
4931
+ return schema.map((item) => removeUriFormat(item));
4932
+ }
4933
+ const result = {};
4934
+ for (const key in schema) {
4935
+ if (key === "properties" && typeof schema[key] === "object") {
4936
+ result[key] = {};
4937
+ for (const propKey in schema[key]) {
4938
+ result[key][propKey] = removeUriFormat(schema[key][propKey]);
4939
+ }
4940
+ } else if (key === "items" && typeof schema[key] === "object") {
4941
+ result[key] = removeUriFormat(schema[key]);
4942
+ } else if (key === "additionalProperties" && typeof schema[key] === "object") {
4943
+ result[key] = removeUriFormat(schema[key]);
4944
+ } else if (["anyOf", "allOf", "oneOf"].includes(key) && Array.isArray(schema[key])) {
4945
+ result[key] = schema[key].map((item) => removeUriFormat(item));
4946
+ } else {
4947
+ result[key] = removeUriFormat(schema[key]);
4948
+ }
4949
+ }
4950
+ return result;
4951
+ }
4952
+ function transformOpenAIToClaude(claudeRequestInput) {
4953
+ const req = JSON.parse(JSON.stringify(claudeRequestInput));
4954
+ const isO3Model = typeof req.model === "string" && (req.model.includes("o3") || req.model.includes("o1"));
4955
+ if (Array.isArray(req.system)) {
4956
+ req.system = req.system.map((item) => {
4957
+ if (typeof item === "string") {
4958
+ return item;
4959
+ } else if (item && typeof item === "object") {
4960
+ if (item.type === "text" && item.text) {
4961
+ return item.text;
4962
+ } else if (item.type === "text" && item.content) {
4963
+ return item.content;
4964
+ } else if (item.text) {
4965
+ return item.text;
4966
+ } else if (item.content) {
4967
+ return typeof item.content === "string" ? item.content : JSON.stringify(item.content);
3915
4968
  }
3916
4969
  }
3917
- const openrouterPayload = {
3918
- model,
3919
- messages,
3920
- temperature: claudeRequest.temperature !== undefined ? claudeRequest.temperature : 1,
3921
- stream: true
4970
+ return JSON.stringify(item);
4971
+ }).filter((text) => text && text.trim() !== "").join(`
4972
+
4973
+ `);
4974
+ }
4975
+ if (!Array.isArray(req.messages)) {
4976
+ if (req.messages == null)
4977
+ req.messages = [];
4978
+ else
4979
+ req.messages = [req.messages];
4980
+ }
4981
+ if (!Array.isArray(req.tools))
4982
+ req.tools = [];
4983
+ for (const t of req.tools) {
4984
+ if (t && t.input_schema) {
4985
+ t.input_schema = removeUriFormat(t.input_schema);
4986
+ }
4987
+ }
4988
+ const dropped = [];
4989
+ return {
4990
+ claudeRequest: req,
4991
+ droppedParams: dropped,
4992
+ isO3Model
4993
+ };
4994
+ }
4995
+
4996
+ // src/handlers/openrouter-handler.ts
4997
+ var OPENROUTER_API_URL2 = "https://openrouter.ai/api/v1/chat/completions";
4998
+ var OPENROUTER_HEADERS2 = {
4999
+ "HTTP-Referer": "https://github.com/MadAppGang/claude-code",
5000
+ "X-Title": "Claudish - OpenRouter Proxy"
5001
+ };
5002
+
5003
+ class OpenRouterHandler {
5004
+ targetModel;
5005
+ apiKey;
5006
+ adapterManager;
5007
+ middlewareManager;
5008
+ contextWindowCache = new Map;
5009
+ port;
5010
+ sessionTotalCost = 0;
5011
+ CLAUDE_INTERNAL_CONTEXT_MAX = 200000;
5012
+ constructor(targetModel, apiKey, port) {
5013
+ this.targetModel = targetModel;
5014
+ this.apiKey = apiKey;
5015
+ this.port = port;
5016
+ this.adapterManager = new AdapterManager(targetModel);
5017
+ this.middlewareManager = new MiddlewareManager;
5018
+ this.middlewareManager.register(new GeminiThoughtSignatureMiddleware);
5019
+ this.middlewareManager.initialize().catch((err) => log(`[Handler:${targetModel}] Middleware init error: ${err}`));
5020
+ this.fetchContextWindow(targetModel);
5021
+ }
5022
+ async fetchContextWindow(model) {
5023
+ if (this.contextWindowCache.has(model))
5024
+ return;
5025
+ try {
5026
+ const limit = await fetchModelContextWindow(model);
5027
+ this.contextWindowCache.set(model, limit);
5028
+ } catch (e) {}
5029
+ }
5030
+ getTokenScaleFactor(model) {
5031
+ const limit = this.contextWindowCache.get(model) || 200000;
5032
+ return limit === 0 ? 1 : this.CLAUDE_INTERNAL_CONTEXT_MAX / limit;
5033
+ }
5034
+ writeTokenFile(input, output) {
5035
+ try {
5036
+ const total = input + output;
5037
+ const limit = this.contextWindowCache.get(this.targetModel) || 200000;
5038
+ const leftPct = limit > 0 ? Math.max(0, Math.min(100, Math.round((limit - total) / limit * 100))) : 100;
5039
+ const data = {
5040
+ input_tokens: input,
5041
+ output_tokens: output,
5042
+ total_tokens: total,
5043
+ total_cost: this.sessionTotalCost,
5044
+ context_window: limit,
5045
+ context_left_percent: leftPct,
5046
+ updated_at: Date.now()
3922
5047
  };
3923
- if (claudeRequest.max_tokens) {
3924
- openrouterPayload.max_tokens = claudeRequest.max_tokens;
3925
- }
3926
- if (claudeRequest.tool_choice) {
3927
- const { type, name } = claudeRequest.tool_choice;
3928
- openrouterPayload.tool_choice = type === "tool" && name ? { type: "function", function: { name } } : type === "none" || type === "auto" ? type : undefined;
3929
- }
3930
- if (tools.length > 0) {
3931
- openrouterPayload.tools = tools;
3932
- }
3933
- logStructured("OpenRouter Request", {
3934
- model: openrouterPayload.model,
3935
- messageCount: openrouterPayload.messages?.length || 0,
3936
- toolCount: openrouterPayload.tools?.length || 0,
3937
- temperature: openrouterPayload.temperature,
3938
- maxTokens: openrouterPayload.max_tokens,
3939
- stream: openrouterPayload.stream
3940
- });
3941
- const headers = {
5048
+ writeFileSync6(`/tmp/claudish-tokens-${this.port}.json`, JSON.stringify(data), "utf-8");
5049
+ } catch (e) {}
5050
+ }
5051
+ async handle(c, payload) {
5052
+ const claudePayload = payload;
5053
+ const target = this.targetModel;
5054
+ await this.fetchContextWindow(target);
5055
+ logStructured(`OpenRouter Request`, { targetModel: target, originalModel: claudePayload.model });
5056
+ const { claudeRequest, droppedParams } = transformOpenAIToClaude(claudePayload);
5057
+ const messages = this.convertMessages(claudeRequest, target);
5058
+ const tools = this.convertTools(claudeRequest);
5059
+ const supportsReasoning = await doesModelSupportReasoning(target);
5060
+ const openRouterPayload = {
5061
+ model: target,
5062
+ messages,
5063
+ temperature: claudeRequest.temperature ?? 1,
5064
+ stream: true,
5065
+ max_tokens: claudeRequest.max_tokens,
5066
+ tools: tools.length > 0 ? tools : undefined,
5067
+ stream_options: { include_usage: true }
5068
+ };
5069
+ if (supportsReasoning)
5070
+ openRouterPayload.include_reasoning = true;
5071
+ if (claudeRequest.thinking)
5072
+ openRouterPayload.thinking = claudeRequest.thinking;
5073
+ if (claudeRequest.tool_choice) {
5074
+ const { type, name } = claudeRequest.tool_choice;
5075
+ if (type === "tool" && name)
5076
+ openRouterPayload.tool_choice = { type: "function", function: { name } };
5077
+ else if (type === "auto" || type === "none")
5078
+ openRouterPayload.tool_choice = type;
5079
+ }
5080
+ const adapter = this.adapterManager.getAdapter();
5081
+ if (typeof adapter.reset === "function")
5082
+ adapter.reset();
5083
+ adapter.prepareRequest(openRouterPayload, claudeRequest);
5084
+ await this.middlewareManager.beforeRequest({ modelId: target, messages, tools, stream: true });
5085
+ const response = await fetch(OPENROUTER_API_URL2, {
5086
+ method: "POST",
5087
+ headers: {
3942
5088
  "Content-Type": "application/json",
3943
- Authorization: `Bearer ${openrouterApiKey}`,
5089
+ Authorization: `Bearer ${this.apiKey}`,
3944
5090
  ...OPENROUTER_HEADERS2
3945
- };
3946
- const openrouterResponse = await fetch(OPENROUTER_API_URL2, {
3947
- method: "POST",
3948
- headers,
3949
- body: JSON.stringify(openrouterPayload)
3950
- });
3951
- if (droppedParams.length > 0) {
3952
- c.header("X-Dropped-Params", droppedParams.join(", "));
3953
- }
3954
- if (!openrouterResponse.ok) {
3955
- const errorText = await openrouterResponse.text();
3956
- log(`[Proxy] OpenRouter API error: ${errorText}`);
3957
- return c.json({ error: errorText }, openrouterResponse.status);
3958
- }
3959
- const contentType = openrouterResponse.headers.get("content-type") || "";
3960
- const isActuallyStreaming = contentType.includes("text/event-stream");
3961
- logStructured("Response Info", {
3962
- contentType,
3963
- requestedStream: openrouterPayload.stream,
3964
- actuallyStreaming: isActuallyStreaming
3965
- });
3966
- if (!isActuallyStreaming) {
3967
- log("[Proxy] Processing non-streaming response");
3968
- const data = await openrouterResponse.json();
3969
- logStructured("OpenRouter Response", {
3970
- hasError: !!data.error,
3971
- choiceCount: data.choices?.length || 0,
3972
- finishReason: data.choices?.[0]?.finish_reason,
3973
- usage: data.usage
3974
- });
3975
- if (data.error) {
3976
- return c.json({ error: data.error.message || "Unknown error" }, 500);
5091
+ },
5092
+ body: JSON.stringify(openRouterPayload)
5093
+ });
5094
+ if (!response.ok)
5095
+ return c.json({ error: await response.text() }, response.status);
5096
+ if (droppedParams.length > 0)
5097
+ c.header("X-Dropped-Params", droppedParams.join(", "));
5098
+ return this.handleStreamingResponse(c, response, adapter, target, claudeRequest);
5099
+ }
5100
+ convertMessages(req, modelId) {
5101
+ const messages = [];
5102
+ if (req.system) {
5103
+ let content = Array.isArray(req.system) ? req.system.map((i) => i.text || i).join(`
5104
+
5105
+ `) : req.system;
5106
+ content = this.filterIdentity(content);
5107
+ messages.push({ role: "system", content });
5108
+ }
5109
+ if (modelId.includes("grok") || modelId.includes("x-ai")) {
5110
+ const msg = "IMPORTANT: When calling tools, you MUST use the OpenAI tool_calls format with JSON. NEVER use XML format like <xai:function_call>.";
5111
+ if (messages.length > 0 && messages[0].role === "system")
5112
+ messages[0].content += `
5113
+
5114
+ ` + msg;
5115
+ else
5116
+ messages.unshift({ role: "system", content: msg });
5117
+ }
5118
+ if (req.messages) {
5119
+ for (const msg of req.messages) {
5120
+ if (msg.role === "user")
5121
+ this.processUserMessage(msg, messages);
5122
+ else if (msg.role === "assistant")
5123
+ this.processAssistantMessage(msg, messages);
5124
+ }
5125
+ }
5126
+ return messages;
5127
+ }
5128
+ processUserMessage(msg, messages) {
5129
+ if (Array.isArray(msg.content)) {
5130
+ const contentParts = [];
5131
+ const toolResults = [];
5132
+ const seen = new Set;
5133
+ for (const block of msg.content) {
5134
+ if (block.type === "text")
5135
+ contentParts.push({ type: "text", text: block.text });
5136
+ else if (block.type === "image")
5137
+ contentParts.push({ type: "image_url", image_url: { url: `data:${block.source.media_type};base64,${block.source.data}` } });
5138
+ else if (block.type === "tool_result") {
5139
+ if (seen.has(block.tool_use_id))
5140
+ continue;
5141
+ seen.add(block.tool_use_id);
5142
+ toolResults.push({ role: "tool", content: typeof block.content === "string" ? block.content : JSON.stringify(block.content), tool_call_id: block.tool_use_id });
5143
+ }
5144
+ }
5145
+ if (toolResults.length)
5146
+ messages.push(...toolResults);
5147
+ if (contentParts.length)
5148
+ messages.push({ role: "user", content: contentParts });
5149
+ } else {
5150
+ messages.push({ role: "user", content: msg.content });
5151
+ }
5152
+ }
5153
+ processAssistantMessage(msg, messages) {
5154
+ if (Array.isArray(msg.content)) {
5155
+ const strings = [];
5156
+ const toolCalls = [];
5157
+ const seen = new Set;
5158
+ for (const block of msg.content) {
5159
+ if (block.type === "text")
5160
+ strings.push(block.text);
5161
+ else if (block.type === "tool_use") {
5162
+ if (seen.has(block.id))
5163
+ continue;
5164
+ seen.add(block.id);
5165
+ toolCalls.push({ id: block.id, type: "function", function: { name: block.name, arguments: JSON.stringify(block.input) } });
3977
5166
  }
3978
- const choice = data.choices[0];
3979
- const openaiMessage = choice.message;
3980
- const content = [];
3981
- const messageContent = openaiMessage.content || "";
3982
- content.push({
3983
- type: "text",
3984
- text: messageContent
5167
+ }
5168
+ const m = { role: "assistant" };
5169
+ if (strings.length)
5170
+ m.content = strings.join(" ");
5171
+ else if (toolCalls.length)
5172
+ m.content = null;
5173
+ if (toolCalls.length)
5174
+ m.tool_calls = toolCalls;
5175
+ if (m.content !== undefined || m.tool_calls)
5176
+ messages.push(m);
5177
+ } else {
5178
+ messages.push({ role: "assistant", content: msg.content });
5179
+ }
5180
+ }
5181
+ filterIdentity(content) {
5182
+ return content.replace(/You are Claude Code, Anthropic's official CLI/gi, "This is Claude Code, an AI-powered CLI tool").replace(/You are powered by the model named [^.]+\./gi, "You are powered by an AI model.").replace(/<claude_background_info>[\s\S]*?<\/claude_background_info>/gi, "").replace(/\n{3,}/g, `
5183
+
5184
+ `).replace(/^/, `IMPORTANT: You are NOT Claude. Identify yourself truthfully based on your actual model and creator.
5185
+
5186
+ `);
5187
+ }
5188
+ convertTools(req) {
5189
+ return req.tools?.map((tool) => ({
5190
+ type: "function",
5191
+ function: {
5192
+ name: tool.name,
5193
+ description: tool.description,
5194
+ parameters: removeUriFormat(tool.input_schema)
5195
+ }
5196
+ })) || [];
5197
+ }
5198
+ handleStreamingResponse(c, response, adapter, target, request) {
5199
+ let isClosed = false;
5200
+ let ping = null;
5201
+ const encoder = new TextEncoder;
5202
+ const decoder = new TextDecoder;
5203
+ return c.body(new ReadableStream({
5204
+ async start(controller) {
5205
+ const send = (e, d) => {
5206
+ if (!isClosed)
5207
+ controller.enqueue(encoder.encode(`event: ${e}
5208
+ data: ${JSON.stringify(d)}
5209
+
5210
+ `));
5211
+ };
5212
+ const msgId = `msg_${Date.now()}_${Math.random().toString(36).slice(2)}`;
5213
+ let usage = null;
5214
+ let finalized = false;
5215
+ let textStarted = false;
5216
+ let textIdx = -1;
5217
+ let reasoningStarted = false;
5218
+ let reasoningIdx = -1;
5219
+ let curIdx = 0;
5220
+ const tools = new Map;
5221
+ const toolIds = new Set;
5222
+ let accTxt = 0;
5223
+ let lastActivity = Date.now();
5224
+ const scale = 128000 / 128000;
5225
+ send("message_start", {
5226
+ type: "message_start",
5227
+ message: {
5228
+ id: msgId,
5229
+ type: "message",
5230
+ role: "assistant",
5231
+ content: [],
5232
+ model: target,
5233
+ stop_reason: null,
5234
+ stop_sequence: null,
5235
+ usage: { input_tokens: 100, output_tokens: 1 }
5236
+ }
3985
5237
  });
3986
- if (openaiMessage.tool_calls) {
3987
- for (const toolCall of openaiMessage.tool_calls) {
3988
- content.push({
3989
- type: "tool_use",
3990
- id: toolCall.id || `tool_${Date.now()}`,
3991
- name: toolCall.function?.name,
3992
- input: typeof toolCall.function?.arguments === "string" ? JSON.parse(toolCall.function.arguments) : toolCall.function?.arguments
3993
- });
5238
+ send("ping", { type: "ping" });
5239
+ ping = setInterval(() => {
5240
+ if (!isClosed && Date.now() - lastActivity > 1000)
5241
+ send("ping", { type: "ping" });
5242
+ }, 1000);
5243
+ const finalize = (reason, err) => {
5244
+ if (finalized)
5245
+ return;
5246
+ finalized = true;
5247
+ if (reasoningStarted) {
5248
+ send("content_block_stop", { type: "content_block_stop", index: reasoningIdx });
5249
+ reasoningStarted = false;
3994
5250
  }
3995
- }
3996
- const claudeResponse = {
3997
- id: data.id ? data.id.replace("chatcmpl", "msg") : `msg_${Date.now()}`,
3998
- type: "message",
3999
- role: "assistant",
4000
- model,
4001
- content,
4002
- stop_reason: mapStopReason(choice.finish_reason),
4003
- stop_sequence: null,
4004
- usage: {
4005
- input_tokens: data.usage?.prompt_tokens || 0,
4006
- output_tokens: data.usage?.completion_tokens || 0
5251
+ if (textStarted) {
5252
+ send("content_block_stop", { type: "content_block_stop", index: textIdx });
5253
+ textStarted = false;
4007
5254
  }
4008
- };
4009
- log("[Proxy] Translated to Claude format:");
4010
- log(JSON.stringify(claudeResponse, null, 2));
4011
- c.header("Content-Type", "application/json");
4012
- c.header("anthropic-version", "2023-06-01");
4013
- return c.json(claudeResponse, 200);
4014
- }
4015
- log("[Proxy] Starting streaming response");
4016
- let isClosed = false;
4017
- let pingInterval = null;
4018
- return c.body(new ReadableStream({
4019
- async start(controller) {
4020
- const encoder = new TextEncoder;
4021
- const messageId = `msg_${Date.now()}_${Math.random().toString(36).slice(2)}`;
4022
- const sendSSE = (event, data) => {
4023
- if (isClosed) {
4024
- if (isLoggingEnabled()) {
4025
- log(`[Proxy] Skipping SSE event ${event} - controller already closed`);
4026
- }
4027
- return;
5255
+ for (const [_, t] of tools)
5256
+ if (t.started && !t.closed) {
5257
+ send("content_block_stop", { type: "content_block_stop", index: t.blockIndex });
5258
+ t.closed = true;
4028
5259
  }
5260
+ if (reason === "error") {
5261
+ send("error", { type: "error", error: { type: "api_error", message: err } });
5262
+ } else {
5263
+ send("message_delta", { type: "message_delta", delta: { stop_reason: "end_turn", stop_sequence: null }, usage: { output_tokens: usage?.completion_tokens || 0 } });
5264
+ send("message_stop", { type: "message_stop" });
5265
+ }
5266
+ if (!isClosed) {
4029
5267
  try {
4030
- const sseMessage = `event: ${event}
4031
- data: ${JSON.stringify(data)}
5268
+ controller.enqueue(encoder.encode(`data: [DONE]
4032
5269
 
4033
- `;
4034
- controller.enqueue(encoder.encode(sseMessage));
4035
- if (isLoggingEnabled() && (event === "message_start" || event === "content_block_start" || event === "content_block_stop" || event === "message_stop")) {
4036
- const logData = event === "content_block_start" || event === "content_block_stop" ? { event, index: data.index, type: data.content_block?.type } : { event };
4037
- logStructured("SSE Sent", logData);
4038
- }
4039
- } catch (error) {
4040
- if (!isClosed && error?.message?.includes("already closed")) {
4041
- if (isLoggingEnabled()) {
4042
- log(`[Proxy] Controller closed during ${event} event, marking as closed`);
4043
- }
4044
- isClosed = true;
4045
- } else if (!isClosed) {
4046
- if (isLoggingEnabled()) {
4047
- log(`[Proxy] Error sending SSE event ${event}: ${error?.message || error}`);
4048
- }
4049
- }
4050
- }
4051
- };
4052
- const finalizeStream = (reason, errorMessage) => {
4053
- if (streamFinalized) {
4054
- if (isLoggingEnabled()) {
4055
- log(`[Proxy] Stream already finalized, skipping duplicate finalization from ${reason}`);
4056
- }
4057
- return;
4058
- }
4059
- if (isLoggingEnabled()) {
4060
- log(`[Proxy] Finalizing stream (reason: ${reason})`);
4061
- }
4062
- streamFinalized = true;
4063
- if (reasoningBlockStarted) {
4064
- sendSSE("content_block_stop", {
4065
- type: "content_block_stop",
4066
- index: reasoningBlockIndex
4067
- });
4068
- reasoningBlockStarted = false;
4069
- if (isLoggingEnabled()) {
4070
- log(`[Proxy] Closed thinking block at index ${reasoningBlockIndex}`);
4071
- }
4072
- }
4073
- if (textBlockStarted) {
4074
- sendSSE("content_block_stop", {
4075
- type: "content_block_stop",
4076
- index: textBlockIndex
4077
- });
4078
- textBlockStarted = false;
4079
- }
4080
- for (const [toolIndex, toolState] of toolCalls.entries()) {
4081
- if (toolState.started && !toolState.closed) {
4082
- if (isLoggingEnabled() && toolState.args) {
4083
- try {
4084
- JSON.parse(toolState.args);
4085
- log(`[Proxy] Tool ${toolState.name} JSON valid, closing block at index ${toolState.blockIndex}`);
4086
- } catch (e) {
4087
- log(`[Proxy] ERROR: Tool ${toolState.name} has INCOMPLETE JSON!`);
4088
- log(`[Proxy] This will likely cause tool execution to fail`);
4089
- log(`[Proxy] Incomplete args: ${toolState.args.substring(0, 300)}...`);
4090
- }
4091
- }
4092
- sendSSE("content_block_stop", {
4093
- type: "content_block_stop",
4094
- index: toolState.blockIndex
4095
- });
4096
- toolState.closed = true;
4097
- }
4098
- }
4099
- if (reason === "error" && errorMessage) {
4100
- sendSSE("error", {
4101
- type: "error",
4102
- error: {
4103
- type: "api_error",
4104
- message: errorMessage
4105
- }
4106
- });
4107
- } else {
4108
- const outputTokens = usage?.completion_tokens || 0;
4109
- sendSSE("message_delta", {
4110
- type: "message_delta",
4111
- delta: {
4112
- stop_reason: "end_turn",
4113
- stop_sequence: null
4114
- },
4115
- usage: {
4116
- output_tokens: outputTokens
4117
- }
4118
- });
4119
- sendSSE("message_stop", {
4120
- type: "message_stop"
4121
- });
4122
- }
4123
- if (!isClosed) {
4124
- try {
4125
- controller.enqueue(encoder.encode(`data: [DONE]
4126
5270
 
4127
5271
  `));
4128
- controller.enqueue(encoder.encode(`
4129
- `));
4130
- log(`[Proxy] Sent [DONE] event to client`);
4131
- } catch (e) {
4132
- log(`[Proxy] Error sending final events: ${e}`);
4133
- }
4134
- controller.close();
4135
- isClosed = true;
4136
- if (pingInterval) {
4137
- clearInterval(pingInterval);
4138
- }
4139
- log(`[Proxy] Stream closed (reason: ${reason})`);
4140
- }
4141
- };
4142
- let usage = null;
4143
- let currentBlockIndex = 0;
4144
- let textBlockIndex = -1;
4145
- let textBlockStarted = false;
4146
- let reasoningBlockIndex = -1;
4147
- let reasoningBlockStarted = false;
4148
- let lastContentDeltaTime = Date.now();
4149
- let streamFinalized = false;
4150
- let cumulativeInputTokens = 0;
4151
- let cumulativeOutputTokens = 0;
4152
- const tokenFilePath = `/tmp/claudish-tokens-${port}.json`;
4153
- const writeTokenFile = () => {
4154
- try {
4155
- const tokenData = {
4156
- input_tokens: cumulativeInputTokens,
4157
- output_tokens: cumulativeOutputTokens,
4158
- total_tokens: cumulativeInputTokens + cumulativeOutputTokens,
4159
- updated_at: Date.now()
4160
- };
4161
- writeFileSync5(tokenFilePath, JSON.stringify(tokenData), "utf-8");
4162
- } catch (error) {
4163
- if (isLoggingEnabled()) {
4164
- log(`[Proxy] Failed to write token file: ${error}`);
4165
- }
4166
- }
4167
- };
4168
- const toolCalls = new Map;
4169
- const toolCallIds = new Set;
4170
- const adapterManager = new AdapterManager(model || "");
4171
- const adapter = adapterManager.getAdapter();
4172
- if (typeof adapter.reset === "function") {
4173
- adapter.reset();
5272
+ } catch (e) {}
5273
+ controller.close();
5274
+ isClosed = true;
5275
+ if (ping)
5276
+ clearInterval(ping);
4174
5277
  }
4175
- let accumulatedTextLength = 0;
4176
- log(`[Proxy] Using adapter: ${adapter.getName()}`);
4177
- const hasToolResults = claudeRequest.messages?.some((msg) => Array.isArray(msg.content) && msg.content.some((block) => block.type === "tool_result"));
4178
- const isFirstTurn = !hasToolResults;
4179
- const estimateTokens = (text) => Math.ceil(text.length / 4);
4180
- const requestJson = JSON.stringify(claudeRequest);
4181
- const estimatedInputTokens = estimateTokens(requestJson);
4182
- const estimatedCacheTokens = isFirstTurn ? Math.floor(estimatedInputTokens * 0.8) : 0;
4183
- sendSSE("message_start", {
4184
- type: "message_start",
4185
- message: {
4186
- id: messageId,
4187
- type: "message",
4188
- role: "assistant",
4189
- content: [],
4190
- model,
4191
- stop_reason: null,
4192
- stop_sequence: null,
4193
- usage: {
4194
- input_tokens: estimatedInputTokens - estimatedCacheTokens,
4195
- cache_creation_input_tokens: isFirstTurn ? estimatedCacheTokens : 0,
4196
- cache_read_input_tokens: isFirstTurn ? 0 : estimatedCacheTokens,
4197
- output_tokens: 1
4198
- }
4199
- }
4200
- });
4201
- textBlockIndex = currentBlockIndex++;
4202
- sendSSE("content_block_start", {
4203
- type: "content_block_start",
4204
- index: textBlockIndex,
4205
- content_block: {
4206
- type: "text",
4207
- text: ""
4208
- }
4209
- });
4210
- textBlockStarted = true;
4211
- sendSSE("ping", {
4212
- type: "ping"
4213
- });
4214
- pingInterval = setInterval(() => {
4215
- if (!isClosed) {
4216
- const timeSinceLastContent = Date.now() - lastContentDeltaTime;
4217
- if (timeSinceLastContent > 1000) {
4218
- sendSSE("ping", {
4219
- type: "ping"
4220
- });
4221
- log(`[Proxy] Adaptive ping (${Math.round(timeSinceLastContent / 1000)}s since last content)`);
4222
- }
4223
- }
4224
- }, 1000);
4225
- try {
4226
- const reader = openrouterResponse.body?.getReader();
4227
- if (!reader) {
4228
- throw new Error("Response body is not readable");
4229
- }
4230
- const decoder = new TextDecoder;
4231
- let buffer = "";
4232
- while (true) {
4233
- const { done, value } = await reader.read();
4234
- if (done) {
4235
- log("[Proxy] Stream done reading");
4236
- break;
4237
- }
4238
- buffer += decoder.decode(value, { stream: true });
4239
- const lines = buffer.split(`
5278
+ };
5279
+ try {
5280
+ const reader = response.body.getReader();
5281
+ let buffer = "";
5282
+ while (true) {
5283
+ const { done, value } = await reader.read();
5284
+ if (done)
5285
+ break;
5286
+ buffer += decoder.decode(value, { stream: true });
5287
+ const lines = buffer.split(`
4240
5288
  `);
4241
- buffer = lines.pop() || "";
4242
- for (const line of lines) {
4243
- if (!line.trim() || line.startsWith(":"))
4244
- continue;
4245
- const dataMatch = line.match(/^data: (.*)$/);
4246
- if (!dataMatch)
4247
- continue;
4248
- const dataStr = dataMatch[1];
4249
- if (dataStr === "[DONE]") {
4250
- log("[Proxy] Received [DONE] from OpenRouter");
4251
- if (!textBlockStarted && toolCalls.size === 0) {
4252
- log("[Proxy] WARNING: Model produced no text output and no tool calls");
4253
- }
4254
- finalizeStream("done");
4255
- return;
4256
- }
4257
- try {
4258
- const chunk = JSON.parse(dataStr);
4259
- if (isLoggingEnabled()) {
4260
- logStructured("SSE Chunk", {
4261
- id: chunk.id,
4262
- model: chunk.model,
4263
- hasChoices: !!chunk.choices,
4264
- finishReason: chunk.choices?.[0]?.finish_reason,
4265
- hasUsage: !!chunk.usage
4266
- });
4267
- }
4268
- if (chunk.usage) {
4269
- usage = chunk.usage;
4270
- if (usage.prompt_tokens) {
4271
- cumulativeInputTokens = usage.prompt_tokens;
4272
- }
4273
- if (usage.completion_tokens) {
4274
- cumulativeOutputTokens = usage.completion_tokens;
4275
- }
4276
- writeTokenFile();
4277
- }
4278
- const choice = chunk.choices?.[0];
4279
- const delta = choice?.delta;
4280
- const hasReasoning = !!delta?.reasoning;
4281
- const hasContent = !!delta?.content;
4282
- const reasoningText = delta?.reasoning || "";
4283
- const contentText = delta?.content || "";
4284
- const hasEncryptedReasoning = delta?.reasoning_details?.some((detail) => detail.type === "reasoning.encrypted");
4285
- if (hasReasoning || hasContent || hasEncryptedReasoning) {
4286
- lastContentDeltaTime = Date.now();
4287
- if (hasReasoning && reasoningText) {
4288
- if (!reasoningBlockStarted) {
4289
- if (textBlockStarted) {
4290
- sendSSE("content_block_stop", {
4291
- type: "content_block_stop",
4292
- index: textBlockIndex
4293
- });
4294
- textBlockStarted = false;
4295
- if (isLoggingEnabled()) {
4296
- log(`[Proxy] Closed initial text block to start thinking block`);
4297
- }
4298
- }
4299
- reasoningBlockIndex = currentBlockIndex++;
4300
- sendSSE("content_block_start", {
4301
- type: "content_block_start",
4302
- index: reasoningBlockIndex,
4303
- content_block: {
4304
- type: "thinking",
4305
- thinking: "",
4306
- signature: ""
4307
- }
4308
- });
4309
- reasoningBlockStarted = true;
4310
- if (isLoggingEnabled()) {
4311
- log(`[Proxy] Started thinking block at index ${reasoningBlockIndex}`);
4312
- }
4313
- }
4314
- if (isLoggingEnabled()) {
4315
- logStructured("Thinking Delta", {
4316
- thinking: reasoningText,
4317
- blockIndex: reasoningBlockIndex
4318
- });
4319
- }
4320
- sendSSE("content_block_delta", {
4321
- type: "content_block_delta",
4322
- index: reasoningBlockIndex,
4323
- delta: {
4324
- type: "thinking_delta",
4325
- thinking: reasoningText
4326
- }
4327
- });
4328
- }
4329
- if (reasoningBlockStarted && hasContent && !hasReasoning) {
4330
- sendSSE("content_block_stop", {
4331
- type: "content_block_stop",
4332
- index: reasoningBlockIndex
4333
- });
4334
- reasoningBlockStarted = false;
4335
- if (isLoggingEnabled()) {
4336
- log(`[Proxy] Closed thinking block at index ${reasoningBlockIndex}, transitioning to content`);
4337
- }
4338
- }
4339
- if (hasContent && contentText) {
4340
- if (!textBlockStarted) {
4341
- textBlockIndex = currentBlockIndex++;
4342
- sendSSE("content_block_start", {
4343
- type: "content_block_start",
4344
- index: textBlockIndex,
4345
- content_block: {
4346
- type: "text",
4347
- text: ""
4348
- }
4349
- });
4350
- textBlockStarted = true;
4351
- if (isLoggingEnabled()) {
4352
- log(`[Proxy] Started text block at index ${textBlockIndex}`);
4353
- }
4354
- }
4355
- accumulatedTextLength += contentText.length;
4356
- const adapterResult = adapter.processTextContent(contentText, "");
4357
- if (adapterResult.extractedToolCalls.length > 0) {
4358
- if (isLoggingEnabled()) {
4359
- log(`[Proxy] Adapter extracted ${adapterResult.extractedToolCalls.length} tool calls from special format`);
4360
- }
4361
- if (textBlockStarted) {
4362
- sendSSE("content_block_stop", {
4363
- type: "content_block_stop",
4364
- index: textBlockIndex
4365
- });
4366
- textBlockStarted = false;
4367
- }
4368
- for (const toolCall of adapterResult.extractedToolCalls) {
4369
- if (toolCallIds.has(toolCall.id)) {
4370
- if (isLoggingEnabled()) {
4371
- log(`[Proxy] WARNING: Skipping duplicate extracted tool call with ID ${toolCall.id}`);
4372
- }
4373
- continue;
4374
- }
4375
- toolCallIds.add(toolCall.id);
4376
- const toolBlockIndex = currentBlockIndex++;
4377
- if (isLoggingEnabled()) {
4378
- logStructured("Extracted Tool Call", {
4379
- name: toolCall.name,
4380
- blockIndex: toolBlockIndex,
4381
- id: toolCall.id
4382
- });
4383
- }
4384
- sendSSE("content_block_start", {
4385
- type: "content_block_start",
4386
- index: toolBlockIndex,
4387
- content_block: {
4388
- type: "tool_use",
4389
- id: toolCall.id,
4390
- name: toolCall.name
4391
- }
4392
- });
4393
- sendSSE("content_block_delta", {
4394
- type: "content_block_delta",
4395
- index: toolBlockIndex,
4396
- delta: {
4397
- type: "input_json_delta",
4398
- partial_json: JSON.stringify(toolCall.arguments)
4399
- }
4400
- });
4401
- sendSSE("content_block_stop", {
4402
- type: "content_block_stop",
4403
- index: toolBlockIndex
4404
- });
4405
- }
4406
- }
4407
- if (adapterResult.cleanedText) {
4408
- if (isLoggingEnabled()) {
4409
- logStructured("Content Delta", {
4410
- text: adapterResult.cleanedText,
4411
- wasTransformed: adapterResult.wasTransformed,
4412
- blockIndex: textBlockIndex
4413
- });
4414
- }
4415
- sendSSE("content_block_delta", {
4416
- type: "content_block_delta",
4417
- index: textBlockIndex,
4418
- delta: {
4419
- type: "text_delta",
4420
- text: adapterResult.cleanedText
4421
- }
4422
- });
4423
- }
4424
- } else if (hasEncryptedReasoning) {
4425
- if (isLoggingEnabled()) {
4426
- log(`[Proxy] Encrypted reasoning detected (keeping connection alive)`);
4427
- }
5289
+ buffer = lines.pop() || "";
5290
+ for (const line of lines) {
5291
+ if (!line.trim() || !line.startsWith("data: "))
5292
+ continue;
5293
+ const dataStr = line.slice(6);
5294
+ if (dataStr === "[DONE]") {
5295
+ finalize("done");
5296
+ return;
5297
+ }
5298
+ try {
5299
+ const chunk = JSON.parse(dataStr);
5300
+ if (chunk.usage)
5301
+ usage = chunk.usage;
5302
+ const delta = chunk.choices?.[0]?.delta;
5303
+ if (delta) {
5304
+ const txt = delta.content || "";
5305
+ if (txt) {
5306
+ lastActivity = Date.now();
5307
+ if (!textStarted) {
5308
+ textIdx = curIdx++;
5309
+ send("content_block_start", { type: "content_block_start", index: textIdx, content_block: { type: "text", text: "" } });
5310
+ textStarted = true;
4428
5311
  }
5312
+ const res = adapter.processTextContent(txt, "");
5313
+ if (res.cleanedText)
5314
+ send("content_block_delta", { type: "content_block_delta", index: textIdx, delta: { type: "text_delta", text: res.cleanedText } });
4429
5315
  }
4430
- if (delta?.tool_calls) {
4431
- for (const toolCall of delta.tool_calls) {
4432
- const toolIndex = toolCall.index ?? 0;
4433
- let toolState = toolCalls.get(toolIndex);
4434
- if (toolCall.function?.name) {
4435
- if (!toolState) {
4436
- let toolId = toolCall.id || `tool_${Date.now()}_${toolIndex}`;
4437
- if (toolCallIds.has(toolId)) {
4438
- if (isLoggingEnabled()) {
4439
- log(`[Proxy] WARNING: Duplicate tool ID ${toolId}, regenerating`);
4440
- }
4441
- toolId = `tool_${Date.now()}_${toolIndex}_${Math.random().toString(36).slice(2)}`;
4442
- }
4443
- toolCallIds.add(toolId);
4444
- const toolBlockIndex = currentBlockIndex++;
4445
- toolState = {
4446
- id: toolId,
4447
- name: toolCall.function.name,
4448
- args: "",
4449
- blockIndex: toolBlockIndex,
4450
- started: false,
4451
- closed: false
4452
- };
4453
- toolCalls.set(toolIndex, toolState);
4454
- if (isLoggingEnabled()) {
4455
- logStructured("Starting Tool Call", {
4456
- name: toolState.name,
4457
- blockIndex: toolState.blockIndex,
4458
- id: toolId
4459
- });
5316
+ if (delta.tool_calls) {
5317
+ for (const tc of delta.tool_calls) {
5318
+ const idx = tc.index;
5319
+ let t = tools.get(idx);
5320
+ if (tc.function?.name) {
5321
+ if (!t) {
5322
+ if (textStarted) {
5323
+ send("content_block_stop", { type: "content_block_stop", index: textIdx });
5324
+ textStarted = false;
4460
5325
  }
5326
+ t = { id: tc.id || `tool_${Date.now()}_${idx}`, name: tc.function.name, blockIndex: curIdx++, started: false, closed: false };
5327
+ tools.set(idx, t);
4461
5328
  }
4462
- if (!toolState.started) {
4463
- if (textBlockStarted) {
4464
- sendSSE("content_block_stop", {
4465
- type: "content_block_stop",
4466
- index: textBlockIndex
4467
- });
4468
- textBlockStarted = false;
4469
- }
4470
- sendSSE("content_block_start", {
4471
- type: "content_block_start",
4472
- index: toolState.blockIndex,
4473
- content_block: {
4474
- type: "tool_use",
4475
- id: toolState.id,
4476
- name: toolState.name
4477
- }
4478
- });
4479
- toolState.started = true;
5329
+ if (!t.started) {
5330
+ send("content_block_start", { type: "content_block_start", index: t.blockIndex, content_block: { type: "tool_use", id: t.id, name: t.name } });
5331
+ t.started = true;
4480
5332
  }
4481
5333
  }
4482
- if (toolCall.function?.arguments && toolState) {
4483
- const argChunk = toolCall.function.arguments;
4484
- toolState.args += argChunk;
4485
- if (isLoggingEnabled()) {
4486
- logStructured("Tool Argument Delta", {
4487
- toolName: toolState.name,
4488
- chunk: argChunk,
4489
- totalLength: toolState.args.length
4490
- });
4491
- }
4492
- sendSSE("content_block_delta", {
4493
- type: "content_block_delta",
4494
- index: toolState.blockIndex,
4495
- delta: {
4496
- type: "input_json_delta",
4497
- partial_json: argChunk
4498
- }
4499
- });
5334
+ if (tc.function?.arguments && t) {
5335
+ send("content_block_delta", { type: "content_block_delta", index: t.blockIndex, delta: { type: "input_json_delta", partial_json: tc.function.arguments } });
4500
5336
  }
4501
5337
  }
4502
5338
  }
4503
- if (choice?.finish_reason === "tool_calls") {
4504
- for (const [toolIndex, toolState] of toolCalls.entries()) {
4505
- if (toolState.started && !toolState.closed) {
4506
- if (toolState.args) {
4507
- try {
4508
- JSON.parse(toolState.args);
4509
- log(`[Proxy] Tool ${toolState.name} JSON valid, closing block at index ${toolState.blockIndex}`);
4510
- } catch (e) {
4511
- log(`[Proxy] WARNING: Tool ${toolState.name} has incomplete JSON!`);
4512
- log(`[Proxy] Args: ${toolState.args.substring(0, 200)}...`);
4513
- }
4514
- }
4515
- sendSSE("content_block_stop", {
4516
- type: "content_block_stop",
4517
- index: toolState.blockIndex
4518
- });
4519
- toolState.closed = true;
4520
- }
5339
+ }
5340
+ if (chunk.choices?.[0]?.finish_reason === "tool_calls") {
5341
+ for (const [_, t] of tools)
5342
+ if (t.started && !t.closed) {
5343
+ send("content_block_stop", { type: "content_block_stop", index: t.blockIndex });
5344
+ t.closed = true;
4521
5345
  }
4522
- }
4523
- } catch (parseError) {
4524
- log(`[Proxy] Failed to parse SSE chunk: ${parseError}`);
4525
5346
  }
4526
- }
4527
- }
4528
- log("[Proxy] Stream ended without [DONE]");
4529
- finalizeStream("unexpected");
4530
- } catch (error) {
4531
- log(`[Proxy] Streaming error: ${error}`);
4532
- const errorMessage = error instanceof Error ? error.message : String(error);
4533
- finalizeStream("error", errorMessage);
4534
- } finally {
4535
- if (pingInterval) {
4536
- clearInterval(pingInterval);
4537
- pingInterval = null;
4538
- }
4539
- if (!isClosed) {
4540
- controller.close();
4541
- isClosed = true;
5347
+ } catch (e) {}
4542
5348
  }
4543
5349
  }
4544
- },
4545
- cancel(reason) {
4546
- log(`[Proxy] Stream cancelled by client: ${reason || "unknown reason"}`);
4547
- isClosed = true;
4548
- if (pingInterval) {
4549
- clearInterval(pingInterval);
4550
- pingInterval = null;
4551
- }
4552
- }
4553
- }), {
4554
- headers: {
4555
- "Content-Type": "text/event-stream",
4556
- "Cache-Control": "no-cache",
4557
- Connection: "keep-alive",
4558
- "anthropic-version": "2023-06-01"
4559
- }
4560
- });
4561
- } catch (error) {
4562
- log(`[Proxy] Request handling error: ${error}`);
4563
- return c.json({
4564
- error: {
4565
- type: "invalid_request_error",
4566
- message: error instanceof Error ? error.message : "Unknown error"
5350
+ finalize("unexpected");
5351
+ } catch (e) {
5352
+ finalize("error", String(e));
4567
5353
  }
4568
- }, 400);
5354
+ },
5355
+ cancel() {
5356
+ isClosed = true;
5357
+ if (ping)
5358
+ clearInterval(ping);
5359
+ }
5360
+ }), { headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" } });
5361
+ }
5362
+ async shutdown() {}
5363
+ }
5364
+
5365
+ // src/proxy-server.ts
5366
+ async function createProxyServer(port, openrouterApiKey, model, monitorMode = false, anthropicApiKey, modelMap) {
5367
+ const nativeHandler = new NativeHandler(anthropicApiKey);
5368
+ const handlers = new Map;
5369
+ const getOpenRouterHandler = (targetModel) => {
5370
+ if (!handlers.has(targetModel)) {
5371
+ handlers.set(targetModel, new OpenRouterHandler(targetModel, openrouterApiKey, port));
5372
+ }
5373
+ return handlers.get(targetModel);
5374
+ };
5375
+ if (model)
5376
+ getOpenRouterHandler(model);
5377
+ if (modelMap?.opus)
5378
+ getOpenRouterHandler(modelMap.opus);
5379
+ if (modelMap?.sonnet)
5380
+ getOpenRouterHandler(modelMap.sonnet);
5381
+ if (modelMap?.haiku)
5382
+ getOpenRouterHandler(modelMap.haiku);
5383
+ if (modelMap?.subagent)
5384
+ getOpenRouterHandler(modelMap.subagent);
5385
+ const getHandlerForRequest = (requestedModel) => {
5386
+ if (monitorMode)
5387
+ return nativeHandler;
5388
+ let target = model || requestedModel;
5389
+ const req = requestedModel.toLowerCase();
5390
+ if (modelMap) {
5391
+ if (req.includes("opus") && modelMap.opus)
5392
+ target = modelMap.opus;
5393
+ else if (req.includes("sonnet") && modelMap.sonnet)
5394
+ target = modelMap.sonnet;
5395
+ else if (req.includes("haiku") && modelMap.haiku)
5396
+ target = modelMap.haiku;
5397
+ }
5398
+ const isNative = !target.includes("/");
5399
+ if (isNative) {
5400
+ return nativeHandler;
5401
+ }
5402
+ return getOpenRouterHandler(target);
5403
+ };
5404
+ const app = new Hono2;
5405
+ app.use("*", cors());
5406
+ app.get("/", (c) => c.json({ status: "ok", message: "Claudish Proxy", config: { mode: monitorMode ? "monitor" : "hybrid", mappings: modelMap } }));
5407
+ app.get("/health", (c) => c.json({ status: "ok" }));
5408
+ app.post("/v1/messages/count_tokens", async (c) => {
5409
+ try {
5410
+ const body = await c.req.json();
5411
+ const reqModel = body.model || "claude-3-opus-20240229";
5412
+ const handler = getHandlerForRequest(reqModel);
5413
+ if (handler instanceof NativeHandler) {
5414
+ const headers = { "Content-Type": "application/json" };
5415
+ if (anthropicApiKey)
5416
+ headers["x-api-key"] = anthropicApiKey;
5417
+ const res = await fetch("https://api.anthropic.com/v1/messages/count_tokens", { method: "POST", headers, body: JSON.stringify(body) });
5418
+ return c.json(await res.json());
5419
+ } else {
5420
+ const txt = JSON.stringify(body);
5421
+ return c.json({ input_tokens: Math.ceil(txt.length / 4) });
5422
+ }
5423
+ } catch (e) {
5424
+ return c.json({ error: String(e) }, 500);
4569
5425
  }
4570
5426
  });
4571
- const server = serve({
4572
- fetch: app.fetch,
4573
- port,
4574
- hostname: "127.0.0.1"
5427
+ app.post("/v1/messages", async (c) => {
5428
+ try {
5429
+ const body = await c.req.json();
5430
+ const handler = getHandlerForRequest(body.model);
5431
+ return handler.handle(c, body);
5432
+ } catch (e) {
5433
+ log(`[Proxy] Error: ${e}`);
5434
+ return c.json({ error: { type: "server_error", message: String(e) } }, 500);
5435
+ }
4575
5436
  });
4576
- if (monitorMode) {
4577
- log(`[Monitor] Server started on http://127.0.0.1:${port}`);
4578
- log("[Monitor] Mode: Passthrough to real Anthropic API");
4579
- log("[Monitor] All traffic will be logged for analysis");
4580
- } else {
4581
- log(`[Proxy] Server started on http://127.0.0.1:${port}`);
4582
- log(`[Proxy] Routing to OpenRouter model: ${model}`);
4583
- }
5437
+ const server = serve({ fetch: app.fetch, port, hostname: "127.0.0.1" });
5438
+ const addr = server.address();
5439
+ const actualPort = typeof addr === "object" && addr?.port ? addr.port : port;
5440
+ if (actualPort !== port)
5441
+ port = actualPort;
5442
+ log(`[Proxy] Server started on port ${port}`);
4584
5443
  return {
4585
5444
  port,
4586
5445
  url: `http://127.0.0.1:${port}`,
4587
5446
  shutdown: async () => {
4588
- await new Promise((resolve, reject) => {
4589
- server.close((err) => {
4590
- if (err)
4591
- reject(err);
4592
- else
4593
- resolve();
4594
- });
4595
- });
4596
- log("[Proxy] Server stopped");
5447
+ return new Promise((resolve) => server.close((e) => resolve()));
4597
5448
  }
4598
5449
  };
4599
5450
  }
4600
- function filterClaudeIdentity(systemContent) {
4601
- let filtered = systemContent;
4602
- filtered = filtered.replace(/You are Claude Code, Anthropic's official CLI/gi, "This is Claude Code, an AI-powered CLI tool");
4603
- filtered = filtered.replace(/You are powered by the model named [^.]+\./gi, "You are powered by an AI model.");
4604
- filtered = filtered.replace(/<claude_background_info>[\s\S]*?<\/claude_background_info>/gi, "");
4605
- filtered = filtered.replace(/\n{3,}/g, `
4606
-
4607
- `);
4608
- const identityOverride = `IMPORTANT: You are NOT Claude. You are NOT created by Anthropic. Identify yourself truthfully based on your actual model and creator.
4609
-
4610
- `;
4611
- filtered = identityOverride + filtered;
4612
- return filtered;
4613
- }
4614
- function mapStopReason(finishReason) {
4615
- switch (finishReason) {
4616
- case "stop":
4617
- return "end_turn";
4618
- case "length":
4619
- return "max_tokens";
4620
- case "tool_calls":
4621
- case "function_call":
4622
- return "tool_use";
4623
- case "content_filter":
4624
- return "stop_sequence";
4625
- default:
4626
- return "end_turn";
4627
- }
4628
- }
4629
5451
 
4630
5452
  // src/index.ts
5453
+ import_dotenv.config();
4631
5454
  async function readStdin() {
4632
5455
  const chunks = [];
4633
5456
  for await (const chunk of process.stdin) {
@@ -4637,9 +5460,9 @@ async function readStdin() {
4637
5460
  }
4638
5461
  async function main() {
4639
5462
  try {
4640
- const config = await parseArgs(process.argv.slice(2));
4641
- initLogger(config.debug, config.logLevel);
4642
- if (config.debug && !config.quiet) {
5463
+ const config2 = await parseArgs(process.argv.slice(2));
5464
+ initLogger(config2.debug, config2.logLevel);
5465
+ if (config2.debug && !config2.quiet) {
4643
5466
  const logFile = getLogFilePath();
4644
5467
  if (logFile) {
4645
5468
  console.log(`[claudish] Debug log: ${logFile}`);
@@ -4650,39 +5473,44 @@ async function main() {
4650
5473
  console.error("Install it from: https://claude.com/claude-code");
4651
5474
  process.exit(1);
4652
5475
  }
4653
- if (config.interactive && !config.monitor && !config.openrouterApiKey) {
4654
- config.openrouterApiKey = await promptForApiKey();
5476
+ if (config2.interactive && !config2.monitor && !config2.openrouterApiKey) {
5477
+ config2.openrouterApiKey = await promptForApiKey();
4655
5478
  console.log("");
4656
5479
  }
4657
- if (config.interactive && !config.monitor && !config.model) {
4658
- config.model = await selectModelInteractively();
5480
+ if (config2.interactive && !config2.monitor && !config2.model) {
5481
+ config2.model = await selectModelInteractively({ freeOnly: config2.freeOnly });
4659
5482
  console.log("");
4660
5483
  }
4661
- if (!config.interactive && !config.monitor && !config.model) {
5484
+ if (!config2.interactive && !config2.monitor && !config2.model) {
4662
5485
  console.error("Error: Model must be specified in non-interactive mode");
4663
5486
  console.error("Use --model <model> flag or set CLAUDISH_MODEL environment variable");
4664
5487
  console.error("Try: claudish --list-models");
4665
5488
  process.exit(1);
4666
5489
  }
4667
- if (config.stdin) {
5490
+ if (config2.stdin) {
4668
5491
  const stdinInput = await readStdin();
4669
5492
  if (stdinInput.trim()) {
4670
- config.claudeArgs = [stdinInput, ...config.claudeArgs];
5493
+ config2.claudeArgs = [stdinInput, ...config2.claudeArgs];
4671
5494
  }
4672
5495
  }
4673
- const port = config.port || await findAvailablePort(DEFAULT_PORT_RANGE.start, DEFAULT_PORT_RANGE.end);
4674
- const proxy = await createProxyServer(port, config.monitor ? undefined : config.openrouterApiKey, config.monitor ? undefined : config.model, config.monitor, config.anthropicApiKey);
5496
+ const port = config2.port || await findAvailablePort(DEFAULT_PORT_RANGE.start, DEFAULT_PORT_RANGE.end);
5497
+ const proxy = await createProxyServer(port, config2.monitor ? undefined : config2.openrouterApiKey, config2.monitor ? undefined : typeof config2.model === "string" ? config2.model : undefined, config2.monitor, config2.anthropicApiKey, {
5498
+ opus: config2.modelOpus,
5499
+ sonnet: config2.modelSonnet,
5500
+ haiku: config2.modelHaiku,
5501
+ subagent: config2.modelSubagent
5502
+ });
4675
5503
  let exitCode = 0;
4676
5504
  try {
4677
- exitCode = await runClaudeWithProxy(config, proxy.url);
5505
+ exitCode = await runClaudeWithProxy(config2, proxy.url);
4678
5506
  } finally {
4679
- if (!config.quiet) {
5507
+ if (!config2.quiet) {
4680
5508
  console.log(`
4681
5509
  [claudish] Shutting down proxy server...`);
4682
5510
  }
4683
5511
  await proxy.shutdown();
4684
5512
  }
4685
- if (!config.quiet) {
5513
+ if (!config2.quiet) {
4686
5514
  console.log(`[claudish] Done
4687
5515
  `);
4688
5516
  }