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/README.md +83 -4
- package/dist/index.js +1988 -1160
- package/package.json +21 -20
- package/recommended-models.json +28 -112
- package/scripts/extract-models.ts +10 -0
- package/scripts/postinstall.cjs +0 -0
- package/skills/claudish-usage/SKILL.md +43 -24
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 &&
|
|
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 === "--
|
|
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
|
|
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
|
-
--
|
|
687
|
-
--
|
|
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
|
|
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
|
|
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:
|
|
758
|
-
|
|
759
|
-
|
|
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
|
|
993
|
-
|
|
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
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
1087
|
-
import { join as
|
|
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
|
-
|
|
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 =
|
|
1133
|
-
if (!
|
|
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 =
|
|
1138
|
-
|
|
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/
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
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
|
-
|
|
4264
|
+
log(`API Key found: ${maskCredential(extractedApiKey)}`);
|
|
3401
4265
|
}
|
|
3402
|
-
|
|
3403
|
-
|
|
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
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
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
|
-
|
|
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 = [
|
|
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/
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
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
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
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
|
-
}
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
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
|
-
}
|
|
4740
|
+
}
|
|
3655
4741
|
}
|
|
3656
|
-
}
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
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
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
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
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
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 (
|
|
3691
|
-
|
|
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
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
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
|
-
|
|
3745
|
-
|
|
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
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
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
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
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
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
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
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
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
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
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
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
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
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
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 ${
|
|
5089
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
3944
5090
|
...OPENROUTER_HEADERS2
|
|
3945
|
-
}
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
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
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
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
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
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
|
-
|
|
3997
|
-
|
|
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
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
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
|
-
|
|
4176
|
-
|
|
4177
|
-
const
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
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
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
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
|
|
4431
|
-
for (const
|
|
4432
|
-
const
|
|
4433
|
-
let
|
|
4434
|
-
if (
|
|
4435
|
-
if (!
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
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 (!
|
|
4463
|
-
|
|
4464
|
-
|
|
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 (
|
|
4483
|
-
|
|
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
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
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
|
-
|
|
4546
|
-
|
|
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
|
-
},
|
|
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
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
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
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
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
|
-
|
|
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
|
|
4641
|
-
initLogger(
|
|
4642
|
-
if (
|
|
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 (
|
|
4654
|
-
|
|
5476
|
+
if (config2.interactive && !config2.monitor && !config2.openrouterApiKey) {
|
|
5477
|
+
config2.openrouterApiKey = await promptForApiKey();
|
|
4655
5478
|
console.log("");
|
|
4656
5479
|
}
|
|
4657
|
-
if (
|
|
4658
|
-
|
|
5480
|
+
if (config2.interactive && !config2.monitor && !config2.model) {
|
|
5481
|
+
config2.model = await selectModelInteractively({ freeOnly: config2.freeOnly });
|
|
4659
5482
|
console.log("");
|
|
4660
5483
|
}
|
|
4661
|
-
if (!
|
|
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 (
|
|
5490
|
+
if (config2.stdin) {
|
|
4668
5491
|
const stdinInput = await readStdin();
|
|
4669
5492
|
if (stdinInput.trim()) {
|
|
4670
|
-
|
|
5493
|
+
config2.claudeArgs = [stdinInput, ...config2.claudeArgs];
|
|
4671
5494
|
}
|
|
4672
5495
|
}
|
|
4673
|
-
const port =
|
|
4674
|
-
const proxy = await createProxyServer(port,
|
|
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(
|
|
5505
|
+
exitCode = await runClaudeWithProxy(config2, proxy.url);
|
|
4678
5506
|
} finally {
|
|
4679
|
-
if (!
|
|
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 (!
|
|
5513
|
+
if (!config2.quiet) {
|
|
4686
5514
|
console.log(`[claudish] Done
|
|
4687
5515
|
`);
|
|
4688
5516
|
}
|