claudish 2.8.1 → 2.10.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 +23 -26
- package/dist/index.js +879 -46
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,47 +24,44 @@
|
|
|
24
24
|
|
|
25
25
|
## Installation
|
|
26
26
|
|
|
27
|
-
###
|
|
27
|
+
### Quick Install
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
-
|
|
29
|
+
```bash
|
|
30
|
+
# Shell script (Linux/macOS)
|
|
31
|
+
curl -fsSL https://raw.githubusercontent.com/MadAppGang/claudish/main/install.sh | bash
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
# Homebrew (macOS)
|
|
34
|
+
brew tap MadAppGang/claudish && brew install claudish
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
# npm
|
|
37
|
+
npm install -g claudish
|
|
36
38
|
|
|
37
|
-
|
|
39
|
+
# Bun
|
|
40
|
+
bun install -g claudish
|
|
41
|
+
```
|
|
38
42
|
|
|
39
|
-
|
|
40
|
-
# With Node.js (works everywhere)
|
|
41
|
-
npx claudish@latest --model x-ai/grok-code-fast-1 "your prompt"
|
|
43
|
+
### Prerequisites
|
|
42
44
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
```
|
|
45
|
+
- [Claude Code](https://claude.com/claude-code) - Claude CLI must be installed
|
|
46
|
+
- [OpenRouter API Key](https://openrouter.ai/keys) - Free tier available
|
|
46
47
|
|
|
47
|
-
|
|
48
|
+
### Other Install Options
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
# With npm (Node.js)
|
|
51
|
-
npm install -g claudish
|
|
50
|
+
**Use without installing:**
|
|
52
51
|
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
```bash
|
|
53
|
+
npx claudish@latest --model x-ai/grok-code-fast-1 "your prompt"
|
|
54
|
+
bunx claudish@latest --model x-ai/grok-code-fast-1 "your prompt"
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
**
|
|
57
|
+
**Install from source:**
|
|
58
58
|
|
|
59
59
|
```bash
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
bun run build
|
|
63
|
-
bun link # or: npm link
|
|
60
|
+
git clone https://github.com/MadAppGang/claudish.git
|
|
61
|
+
cd claudish
|
|
62
|
+
bun install && bun run build && bun link
|
|
64
63
|
```
|
|
65
64
|
|
|
66
|
-
**Performance Note:** While Claudish works with both runtimes, Bun offers faster startup times. Both provide identical functionality.
|
|
67
|
-
|
|
68
65
|
## Quick Start
|
|
69
66
|
|
|
70
67
|
### Step 0: Initialize Claudish Skill (First Time Only)
|
package/dist/index.js
CHANGED
|
@@ -34304,7 +34304,11 @@ var init_config = __esm(() => {
|
|
|
34304
34304
|
ANTHROPIC_DEFAULT_OPUS_MODEL: "ANTHROPIC_DEFAULT_OPUS_MODEL",
|
|
34305
34305
|
ANTHROPIC_DEFAULT_SONNET_MODEL: "ANTHROPIC_DEFAULT_SONNET_MODEL",
|
|
34306
34306
|
ANTHROPIC_DEFAULT_HAIKU_MODEL: "ANTHROPIC_DEFAULT_HAIKU_MODEL",
|
|
34307
|
-
CLAUDE_CODE_SUBAGENT_MODEL: "CLAUDE_CODE_SUBAGENT_MODEL"
|
|
34307
|
+
CLAUDE_CODE_SUBAGENT_MODEL: "CLAUDE_CODE_SUBAGENT_MODEL",
|
|
34308
|
+
OLLAMA_BASE_URL: "OLLAMA_BASE_URL",
|
|
34309
|
+
OLLAMA_HOST: "OLLAMA_HOST",
|
|
34310
|
+
LMSTUDIO_BASE_URL: "LMSTUDIO_BASE_URL",
|
|
34311
|
+
VLLM_BASE_URL: "VLLM_BASE_URL"
|
|
34308
34312
|
};
|
|
34309
34313
|
OPENROUTER_HEADERS = {
|
|
34310
34314
|
"HTTP-Referer": "https://github.com/MadAppGang/claude-code",
|
|
@@ -34349,6 +34353,7 @@ process.stdin.on('end', () => {
|
|
|
34349
34353
|
|
|
34350
34354
|
let ctx = 100, cost = 0;
|
|
34351
34355
|
const model = process.env.CLAUDISH_ACTIVE_MODEL_NAME || 'unknown';
|
|
34356
|
+
const isLocal = process.env.CLAUDISH_IS_LOCAL === 'true';
|
|
34352
34357
|
|
|
34353
34358
|
try {
|
|
34354
34359
|
const tokens = JSON.parse(fs.readFileSync('${escapedTokenPath}', 'utf-8'));
|
|
@@ -34361,8 +34366,8 @@ process.stdin.on('end', () => {
|
|
|
34361
34366
|
} catch {}
|
|
34362
34367
|
}
|
|
34363
34368
|
|
|
34364
|
-
const
|
|
34365
|
-
console.log(\`\${CYAN}\${BOLD}\${dir}\${RESET} \${DIM}•\${RESET} \${YELLOW}\${model}\${RESET} \${DIM}•\${RESET} \${GREEN}
|
|
34369
|
+
const costDisplay = isLocal ? 'LOCAL' : ('$' + cost.toFixed(3));
|
|
34370
|
+
console.log(\`\${CYAN}\${BOLD}\${dir}\${RESET} \${DIM}•\${RESET} \${YELLOW}\${model}\${RESET} \${DIM}•\${RESET} \${GREEN}\${costDisplay}\${RESET} \${DIM}•\${RESET} \${MAGENTA}\${ctx}%\${RESET}\`);
|
|
34366
34371
|
} catch (e) {
|
|
34367
34372
|
console.log('claudish');
|
|
34368
34373
|
}
|
|
@@ -34388,7 +34393,7 @@ function createTempSettingsFile(modelDisplay, port) {
|
|
|
34388
34393
|
const DIM2 = "\\033[2m";
|
|
34389
34394
|
const RESET2 = "\\033[0m";
|
|
34390
34395
|
const BOLD2 = "\\033[1m";
|
|
34391
|
-
statusCommand = `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 "${CYAN2}${BOLD2}%s${RESET2} ${DIM2}•${RESET2} ${YELLOW2}%s${RESET2} ${DIM2}•${RESET2} ${GREEN2}
|
|
34396
|
+
statusCommand = `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 && if [ "$CLAUDISH_IS_LOCAL" = "true" ]; then COST_DISPLAY="LOCAL"; else COST_DISPLAY=$(printf "\\$%.3f" "$COST"); fi && printf "${CYAN2}${BOLD2}%s${RESET2} ${DIM2}•${RESET2} ${YELLOW2}%s${RESET2} ${DIM2}•${RESET2} ${GREEN2}%s${RESET2} ${DIM2}•${RESET2} ${MAGENTA}%s%%${RESET2}\\n" "$DIR" "$CLAUDISH_ACTIVE_MODEL_NAME" "$COST_DISPLAY" "$CTX"`;
|
|
34392
34397
|
}
|
|
34393
34398
|
const settings = {
|
|
34394
34399
|
statusLine: {
|
|
@@ -34434,10 +34439,12 @@ async function runClaudeWithProxy(config3, proxyUrl) {
|
|
|
34434
34439
|
claudeArgs.push(...config3.claudeArgs);
|
|
34435
34440
|
}
|
|
34436
34441
|
}
|
|
34442
|
+
const isLocalModel = modelId.startsWith("ollama/") || modelId.startsWith("ollama:") || modelId.startsWith("lmstudio/") || modelId.startsWith("lmstudio:") || modelId.startsWith("vllm/") || modelId.startsWith("vllm:") || modelId.startsWith("http://") || modelId.startsWith("https://");
|
|
34437
34443
|
const env = {
|
|
34438
34444
|
...process.env,
|
|
34439
34445
|
ANTHROPIC_BASE_URL: proxyUrl,
|
|
34440
34446
|
[ENV.CLAUDISH_ACTIVE_MODEL_NAME]: modelId,
|
|
34447
|
+
CLAUDISH_IS_LOCAL: isLocalModel ? "true" : "false",
|
|
34441
34448
|
[ENV.ANTHROPIC_MODEL]: modelId,
|
|
34442
34449
|
[ENV.ANTHROPIC_SMALL_FAST_MODEL]: modelId
|
|
34443
34450
|
};
|
|
@@ -34912,6 +34919,54 @@ async function parseArgs(args) {
|
|
|
34912
34919
|
}
|
|
34913
34920
|
return config3;
|
|
34914
34921
|
}
|
|
34922
|
+
async function fetchOllamaModels() {
|
|
34923
|
+
const ollamaHost = process.env.OLLAMA_HOST || process.env.OLLAMA_BASE_URL || "http://localhost:11434";
|
|
34924
|
+
try {
|
|
34925
|
+
const response = await fetch(`${ollamaHost}/api/tags`, {
|
|
34926
|
+
signal: AbortSignal.timeout(3000)
|
|
34927
|
+
});
|
|
34928
|
+
if (!response.ok)
|
|
34929
|
+
return [];
|
|
34930
|
+
const data = await response.json();
|
|
34931
|
+
const models = data.models || [];
|
|
34932
|
+
const modelsWithCapabilities = await Promise.all(models.map(async (m) => {
|
|
34933
|
+
let capabilities = [];
|
|
34934
|
+
try {
|
|
34935
|
+
const showResponse = await fetch(`${ollamaHost}/api/show`, {
|
|
34936
|
+
method: "POST",
|
|
34937
|
+
headers: { "Content-Type": "application/json" },
|
|
34938
|
+
body: JSON.stringify({ name: m.name }),
|
|
34939
|
+
signal: AbortSignal.timeout(2000)
|
|
34940
|
+
});
|
|
34941
|
+
if (showResponse.ok) {
|
|
34942
|
+
const showData = await showResponse.json();
|
|
34943
|
+
capabilities = showData.capabilities || [];
|
|
34944
|
+
}
|
|
34945
|
+
} catch {}
|
|
34946
|
+
const supportsTools = capabilities.includes("tools");
|
|
34947
|
+
const isEmbeddingModel = capabilities.includes("embedding") || m.name.toLowerCase().includes("embed");
|
|
34948
|
+
const sizeInfo = m.details?.parameter_size || "unknown size";
|
|
34949
|
+
const toolsIndicator = supportsTools ? "✓ tools" : "✗ no tools";
|
|
34950
|
+
return {
|
|
34951
|
+
id: `ollama/${m.name}`,
|
|
34952
|
+
name: m.name,
|
|
34953
|
+
description: `Local Ollama model (${sizeInfo}, ${toolsIndicator})`,
|
|
34954
|
+
provider: "ollama",
|
|
34955
|
+
context_length: null,
|
|
34956
|
+
pricing: { prompt: "0", completion: "0" },
|
|
34957
|
+
isLocal: true,
|
|
34958
|
+
supportsTools,
|
|
34959
|
+
isEmbeddingModel,
|
|
34960
|
+
capabilities,
|
|
34961
|
+
details: m.details,
|
|
34962
|
+
size: m.size
|
|
34963
|
+
};
|
|
34964
|
+
}));
|
|
34965
|
+
return modelsWithCapabilities.filter((m) => !m.isEmbeddingModel);
|
|
34966
|
+
} catch (e) {
|
|
34967
|
+
return [];
|
|
34968
|
+
}
|
|
34969
|
+
}
|
|
34915
34970
|
async function searchAndPrintModels(query, forceUpdate) {
|
|
34916
34971
|
let models = [];
|
|
34917
34972
|
if (!forceUpdate && existsSync5(ALL_MODELS_JSON_PATH2)) {
|
|
@@ -34943,6 +34998,11 @@ async function searchAndPrintModels(query, forceUpdate) {
|
|
|
34943
34998
|
process.exit(1);
|
|
34944
34999
|
}
|
|
34945
35000
|
}
|
|
35001
|
+
const ollamaModels = await fetchOllamaModels();
|
|
35002
|
+
if (ollamaModels.length > 0) {
|
|
35003
|
+
console.error(`\uD83C\uDFE0 Found ${ollamaModels.length} local Ollama models`);
|
|
35004
|
+
models = [...ollamaModels, ...models];
|
|
35005
|
+
}
|
|
34946
35006
|
const results = models.map((model) => {
|
|
34947
35007
|
const nameScore = fuzzyScore2(model.name || "", query);
|
|
34948
35008
|
const idScore = fuzzyScore2(model.id || "", query);
|
|
@@ -34956,6 +35016,10 @@ async function searchAndPrintModels(query, forceUpdate) {
|
|
|
34956
35016
|
console.log(`No models found matching "${query}"`);
|
|
34957
35017
|
return;
|
|
34958
35018
|
}
|
|
35019
|
+
const RED = "\x1B[31m";
|
|
35020
|
+
const GREEN2 = "\x1B[32m";
|
|
35021
|
+
const RESET2 = "\x1B[0m";
|
|
35022
|
+
const DIM2 = "\x1B[2m";
|
|
34959
35023
|
console.log(`
|
|
34960
35024
|
Found ${results.length} matching models:
|
|
34961
35025
|
`);
|
|
@@ -34967,28 +35031,42 @@ Found ${results.length} matching models:
|
|
|
34967
35031
|
const providerName = model.id.split("/")[0];
|
|
34968
35032
|
const provider = providerName.length > 10 ? providerName.substring(0, 7) + "..." : providerName;
|
|
34969
35033
|
const providerPadded = provider.padEnd(10);
|
|
34970
|
-
const promptPrice = parseFloat(model.pricing?.prompt || "0") * 1e6;
|
|
34971
|
-
const completionPrice = parseFloat(model.pricing?.completion || "0") * 1e6;
|
|
34972
|
-
const avg = (promptPrice + completionPrice) / 2;
|
|
34973
35034
|
let pricing;
|
|
34974
|
-
if (
|
|
34975
|
-
pricing = "
|
|
34976
|
-
} else if (avg === 0) {
|
|
34977
|
-
pricing = "FREE";
|
|
35035
|
+
if (model.isLocal) {
|
|
35036
|
+
pricing = "LOCAL";
|
|
34978
35037
|
} else {
|
|
34979
|
-
|
|
35038
|
+
const promptPrice = parseFloat(model.pricing?.prompt || "0") * 1e6;
|
|
35039
|
+
const completionPrice = parseFloat(model.pricing?.completion || "0") * 1e6;
|
|
35040
|
+
const avg = (promptPrice + completionPrice) / 2;
|
|
35041
|
+
if (avg < 0) {
|
|
35042
|
+
pricing = "varies";
|
|
35043
|
+
} else if (avg === 0) {
|
|
35044
|
+
pricing = "FREE";
|
|
35045
|
+
} else {
|
|
35046
|
+
pricing = `$${avg.toFixed(2)}/1M`;
|
|
35047
|
+
}
|
|
34980
35048
|
}
|
|
34981
35049
|
const pricingPadded = pricing.padEnd(10);
|
|
34982
35050
|
const contextLen = model.context_length || model.top_provider?.context_length || 0;
|
|
34983
35051
|
const context = contextLen > 0 ? `${Math.round(contextLen / 1000)}K` : "N/A";
|
|
34984
35052
|
const contextPadded = context.padEnd(7);
|
|
34985
|
-
|
|
35053
|
+
if (model.isLocal && model.supportsTools === false) {
|
|
35054
|
+
console.log(` ${RED}${modelIdPadded} ${providerPadded} ${pricingPadded} ${contextPadded} ${(score * 100).toFixed(0)}% ✗ no tools${RESET2}`);
|
|
35055
|
+
} else if (model.isLocal && model.supportsTools === true) {
|
|
35056
|
+
console.log(` ${GREEN2}${modelIdPadded}${RESET2} ${providerPadded} ${pricingPadded} ${contextPadded} ${(score * 100).toFixed(0)}%`);
|
|
35057
|
+
} else {
|
|
35058
|
+
console.log(` ${modelIdPadded} ${providerPadded} ${pricingPadded} ${contextPadded} ${(score * 100).toFixed(0)}%`);
|
|
35059
|
+
}
|
|
34986
35060
|
}
|
|
34987
35061
|
console.log("");
|
|
35062
|
+
console.log(`${DIM2}Local models: ${RED}red${RESET2}${DIM2} = no tool support (incompatible), ${GREEN2}green${RESET2}${DIM2} = compatible${RESET2}`);
|
|
35063
|
+
console.log("");
|
|
34988
35064
|
console.log("Use a model: claudish --model <model-id>");
|
|
35065
|
+
console.log("Local models: claudish --model ollama/<model-name>");
|
|
34989
35066
|
}
|
|
34990
35067
|
async function printAllModels(jsonOutput, forceUpdate) {
|
|
34991
35068
|
let models = [];
|
|
35069
|
+
const ollamaModels = await fetchOllamaModels();
|
|
34992
35070
|
if (!forceUpdate && existsSync5(ALL_MODELS_JSON_PATH2)) {
|
|
34993
35071
|
try {
|
|
34994
35072
|
const cacheData = JSON.parse(readFileSync5(ALL_MODELS_JSON_PATH2, "utf-8"));
|
|
@@ -35022,18 +35100,58 @@ async function printAllModels(jsonOutput, forceUpdate) {
|
|
|
35022
35100
|
}
|
|
35023
35101
|
}
|
|
35024
35102
|
if (jsonOutput) {
|
|
35103
|
+
const allModels = [...ollamaModels, ...models];
|
|
35025
35104
|
console.log(JSON.stringify({
|
|
35026
|
-
count:
|
|
35105
|
+
count: allModels.length,
|
|
35106
|
+
localCount: ollamaModels.length,
|
|
35027
35107
|
lastUpdated: new Date().toISOString().split("T")[0],
|
|
35028
|
-
models:
|
|
35108
|
+
models: allModels.map((m) => ({
|
|
35029
35109
|
id: m.id,
|
|
35030
35110
|
name: m.name,
|
|
35031
35111
|
context: m.context_length || m.top_provider?.context_length,
|
|
35032
|
-
pricing: m.pricing
|
|
35112
|
+
pricing: m.pricing,
|
|
35113
|
+
isLocal: m.isLocal || false
|
|
35033
35114
|
}))
|
|
35034
35115
|
}, null, 2));
|
|
35035
35116
|
return;
|
|
35036
35117
|
}
|
|
35118
|
+
const RED = "\x1B[31m";
|
|
35119
|
+
const GREEN2 = "\x1B[32m";
|
|
35120
|
+
const RESET2 = "\x1B[0m";
|
|
35121
|
+
const DIM2 = "\x1B[2m";
|
|
35122
|
+
if (ollamaModels.length > 0) {
|
|
35123
|
+
const toolCapableCount = ollamaModels.filter((m) => m.supportsTools).length;
|
|
35124
|
+
console.log(`
|
|
35125
|
+
\uD83C\uDFE0 LOCAL OLLAMA MODELS (${ollamaModels.length} installed, ${toolCapableCount} with tool support):
|
|
35126
|
+
`);
|
|
35127
|
+
console.log(" Model Size Params Tools");
|
|
35128
|
+
console.log(" " + "─".repeat(76));
|
|
35129
|
+
for (const model of ollamaModels) {
|
|
35130
|
+
const fullId = model.id;
|
|
35131
|
+
const modelId = fullId.length > 35 ? fullId.substring(0, 32) + "..." : fullId;
|
|
35132
|
+
const modelIdPadded = modelId.padEnd(38);
|
|
35133
|
+
const size = model.size ? `${(model.size / 1e9).toFixed(1)}GB` : "N/A";
|
|
35134
|
+
const sizePadded = size.padEnd(12);
|
|
35135
|
+
const params = model.details?.parameter_size || "N/A";
|
|
35136
|
+
const paramsPadded = params.padEnd(8);
|
|
35137
|
+
if (model.supportsTools) {
|
|
35138
|
+
console.log(` ${modelIdPadded} ${sizePadded} ${paramsPadded} ${GREEN2}✓${RESET2}`);
|
|
35139
|
+
} else {
|
|
35140
|
+
console.log(` ${RED}${modelIdPadded} ${sizePadded} ${paramsPadded} ✗ no tools${RESET2}`);
|
|
35141
|
+
}
|
|
35142
|
+
}
|
|
35143
|
+
console.log("");
|
|
35144
|
+
console.log(` ${GREEN2}✓${RESET2} = Compatible with Claude Code (supports tool calling)`);
|
|
35145
|
+
console.log(` ${RED}✗${RESET2} = Not compatible ${DIM2}(Claude Code requires tool support)${RESET2}`);
|
|
35146
|
+
console.log("");
|
|
35147
|
+
console.log(" Use: claudish --model ollama/<model-name>");
|
|
35148
|
+
console.log(" Pull a compatible model: ollama pull llama3.2");
|
|
35149
|
+
} else {
|
|
35150
|
+
console.log(`
|
|
35151
|
+
\uD83C\uDFE0 LOCAL OLLAMA: Not running or no models installed`);
|
|
35152
|
+
console.log(" Start Ollama: ollama serve");
|
|
35153
|
+
console.log(" Pull a model: ollama pull llama3.2");
|
|
35154
|
+
}
|
|
35037
35155
|
const byProvider = new Map;
|
|
35038
35156
|
for (const model of models) {
|
|
35039
35157
|
const provider = model.id.split("/")[0];
|
|
@@ -35044,7 +35162,7 @@ async function printAllModels(jsonOutput, forceUpdate) {
|
|
|
35044
35162
|
}
|
|
35045
35163
|
const sortedProviders = [...byProvider.keys()].sort();
|
|
35046
35164
|
console.log(`
|
|
35047
|
-
|
|
35165
|
+
☁️ OPENROUTER MODELS (${models.length} total):
|
|
35048
35166
|
`);
|
|
35049
35167
|
for (const provider of sortedProviders) {
|
|
35050
35168
|
const providerModels = byProvider.get(provider);
|
|
@@ -35075,9 +35193,10 @@ All OpenRouter Models (${models.length} total):
|
|
|
35075
35193
|
}
|
|
35076
35194
|
console.log(`
|
|
35077
35195
|
`);
|
|
35078
|
-
console.log("Use a model:
|
|
35079
|
-
console.log("
|
|
35080
|
-
console.log("
|
|
35196
|
+
console.log("Use a model: claudish --model <provider/model-id>");
|
|
35197
|
+
console.log("Local model: claudish --model ollama/<model-name>");
|
|
35198
|
+
console.log("Search: claudish --search <query>");
|
|
35199
|
+
console.log("Top models: claudish --top-models");
|
|
35081
35200
|
}
|
|
35082
35201
|
function isCacheStale() {
|
|
35083
35202
|
if (!existsSync5(MODELS_JSON_PATH)) {
|
|
@@ -35286,7 +35405,7 @@ NOTES:
|
|
|
35286
35405
|
ENVIRONMENT VARIABLES:
|
|
35287
35406
|
Claudish automatically loads .env file from current directory.
|
|
35288
35407
|
|
|
35289
|
-
OPENROUTER_API_KEY Required: Your OpenRouter API key
|
|
35408
|
+
OPENROUTER_API_KEY Required: Your OpenRouter API key (for OpenRouter models)
|
|
35290
35409
|
CLAUDISH_MODEL Default model to use (takes priority)
|
|
35291
35410
|
ANTHROPIC_MODEL Claude Code standard: model to use (fallback)
|
|
35292
35411
|
CLAUDISH_PORT Default port for proxy
|
|
@@ -35302,6 +35421,12 @@ ENVIRONMENT VARIABLES:
|
|
|
35302
35421
|
ANTHROPIC_DEFAULT_HAIKU_MODEL Claude Code standard: Haiku model (fallback)
|
|
35303
35422
|
CLAUDE_CODE_SUBAGENT_MODEL Claude Code standard: sub-agent model (fallback)
|
|
35304
35423
|
|
|
35424
|
+
Local providers (OpenAI-compatible):
|
|
35425
|
+
OLLAMA_BASE_URL Ollama server (default: http://localhost:11434)
|
|
35426
|
+
OLLAMA_HOST Alias for OLLAMA_BASE_URL (same default)
|
|
35427
|
+
LMSTUDIO_BASE_URL LM Studio server (default: http://localhost:1234)
|
|
35428
|
+
VLLM_BASE_URL vLLM server (default: http://localhost:8000)
|
|
35429
|
+
|
|
35305
35430
|
EXAMPLES:
|
|
35306
35431
|
# Interactive mode (default) - shows model selector
|
|
35307
35432
|
claudish
|
|
@@ -35355,6 +35480,22 @@ EXAMPLES:
|
|
|
35355
35480
|
# Verbose mode in single-shot (show [claudish] logs)
|
|
35356
35481
|
claudish --verbose "analyze code structure"
|
|
35357
35482
|
|
|
35483
|
+
LOCAL MODELS (Ollama, LM Studio, vLLM):
|
|
35484
|
+
# Use local Ollama model (prefix syntax)
|
|
35485
|
+
claudish --model ollama/llama3.2 "implement feature"
|
|
35486
|
+
claudish --model ollama:codellama "review this code"
|
|
35487
|
+
|
|
35488
|
+
# Use local LM Studio model
|
|
35489
|
+
claudish --model lmstudio/qwen2.5-coder "write tests"
|
|
35490
|
+
|
|
35491
|
+
# Use any OpenAI-compatible endpoint (URL syntax)
|
|
35492
|
+
claudish --model "http://localhost:11434/llama3.2" "task"
|
|
35493
|
+
claudish --model "http://192.168.1.100:8000/mistral" "remote server"
|
|
35494
|
+
|
|
35495
|
+
# Custom Ollama endpoint
|
|
35496
|
+
OLLAMA_BASE_URL=http://192.168.1.50:11434 claudish --model ollama/llama3.2 "task"
|
|
35497
|
+
OLLAMA_HOST=http://192.168.1.50:11434 claudish --model ollama/llama3.2 "task"
|
|
35498
|
+
|
|
35358
35499
|
AVAILABLE MODELS:
|
|
35359
35500
|
List all models: claudish --models
|
|
35360
35501
|
Search models: claudish --models <query>
|
|
@@ -39112,6 +39253,670 @@ var init_openrouter_handler = __esm(() => {
|
|
|
39112
39253
|
};
|
|
39113
39254
|
});
|
|
39114
39255
|
|
|
39256
|
+
// src/handlers/shared/openai-compat.ts
|
|
39257
|
+
function convertMessagesToOpenAI(req, modelId, filterIdentityFn) {
|
|
39258
|
+
const messages = [];
|
|
39259
|
+
if (req.system) {
|
|
39260
|
+
let content = Array.isArray(req.system) ? req.system.map((i) => i.text || i).join(`
|
|
39261
|
+
|
|
39262
|
+
`) : req.system;
|
|
39263
|
+
if (filterIdentityFn)
|
|
39264
|
+
content = filterIdentityFn(content);
|
|
39265
|
+
messages.push({ role: "system", content });
|
|
39266
|
+
}
|
|
39267
|
+
if (modelId.includes("grok") || modelId.includes("x-ai")) {
|
|
39268
|
+
const msg = "IMPORTANT: When calling tools, you MUST use the OpenAI tool_calls format with JSON. NEVER use XML format like <xai:function_call>.";
|
|
39269
|
+
if (messages.length > 0 && messages[0].role === "system") {
|
|
39270
|
+
messages[0].content += `
|
|
39271
|
+
|
|
39272
|
+
` + msg;
|
|
39273
|
+
} else {
|
|
39274
|
+
messages.unshift({ role: "system", content: msg });
|
|
39275
|
+
}
|
|
39276
|
+
}
|
|
39277
|
+
if (req.messages) {
|
|
39278
|
+
for (const msg of req.messages) {
|
|
39279
|
+
if (msg.role === "user")
|
|
39280
|
+
processUserMessage(msg, messages);
|
|
39281
|
+
else if (msg.role === "assistant")
|
|
39282
|
+
processAssistantMessage(msg, messages);
|
|
39283
|
+
}
|
|
39284
|
+
}
|
|
39285
|
+
return messages;
|
|
39286
|
+
}
|
|
39287
|
+
function processUserMessage(msg, messages) {
|
|
39288
|
+
if (Array.isArray(msg.content)) {
|
|
39289
|
+
const contentParts = [];
|
|
39290
|
+
const toolResults = [];
|
|
39291
|
+
const seen = new Set;
|
|
39292
|
+
for (const block of msg.content) {
|
|
39293
|
+
if (block.type === "text") {
|
|
39294
|
+
contentParts.push({ type: "text", text: block.text });
|
|
39295
|
+
} else if (block.type === "image") {
|
|
39296
|
+
contentParts.push({
|
|
39297
|
+
type: "image_url",
|
|
39298
|
+
image_url: { url: `data:${block.source.media_type};base64,${block.source.data}` }
|
|
39299
|
+
});
|
|
39300
|
+
} else if (block.type === "tool_result") {
|
|
39301
|
+
if (seen.has(block.tool_use_id))
|
|
39302
|
+
continue;
|
|
39303
|
+
seen.add(block.tool_use_id);
|
|
39304
|
+
toolResults.push({
|
|
39305
|
+
role: "tool",
|
|
39306
|
+
content: typeof block.content === "string" ? block.content : JSON.stringify(block.content),
|
|
39307
|
+
tool_call_id: block.tool_use_id
|
|
39308
|
+
});
|
|
39309
|
+
}
|
|
39310
|
+
}
|
|
39311
|
+
if (toolResults.length)
|
|
39312
|
+
messages.push(...toolResults);
|
|
39313
|
+
if (contentParts.length)
|
|
39314
|
+
messages.push({ role: "user", content: contentParts });
|
|
39315
|
+
} else {
|
|
39316
|
+
messages.push({ role: "user", content: msg.content });
|
|
39317
|
+
}
|
|
39318
|
+
}
|
|
39319
|
+
function processAssistantMessage(msg, messages) {
|
|
39320
|
+
if (Array.isArray(msg.content)) {
|
|
39321
|
+
const strings = [];
|
|
39322
|
+
const toolCalls = [];
|
|
39323
|
+
const seen = new Set;
|
|
39324
|
+
for (const block of msg.content) {
|
|
39325
|
+
if (block.type === "text") {
|
|
39326
|
+
strings.push(block.text);
|
|
39327
|
+
} else if (block.type === "tool_use") {
|
|
39328
|
+
if (seen.has(block.id))
|
|
39329
|
+
continue;
|
|
39330
|
+
seen.add(block.id);
|
|
39331
|
+
toolCalls.push({
|
|
39332
|
+
id: block.id,
|
|
39333
|
+
type: "function",
|
|
39334
|
+
function: { name: block.name, arguments: JSON.stringify(block.input) }
|
|
39335
|
+
});
|
|
39336
|
+
}
|
|
39337
|
+
}
|
|
39338
|
+
const m = { role: "assistant" };
|
|
39339
|
+
if (strings.length)
|
|
39340
|
+
m.content = strings.join(" ");
|
|
39341
|
+
else if (toolCalls.length)
|
|
39342
|
+
m.content = null;
|
|
39343
|
+
if (toolCalls.length)
|
|
39344
|
+
m.tool_calls = toolCalls;
|
|
39345
|
+
if (m.content !== undefined || m.tool_calls)
|
|
39346
|
+
messages.push(m);
|
|
39347
|
+
} else {
|
|
39348
|
+
messages.push({ role: "assistant", content: msg.content });
|
|
39349
|
+
}
|
|
39350
|
+
}
|
|
39351
|
+
function convertToolsToOpenAI(req) {
|
|
39352
|
+
return req.tools?.map((tool) => ({
|
|
39353
|
+
type: "function",
|
|
39354
|
+
function: {
|
|
39355
|
+
name: tool.name,
|
|
39356
|
+
description: tool.description,
|
|
39357
|
+
parameters: removeUriFormat(tool.input_schema)
|
|
39358
|
+
}
|
|
39359
|
+
})) || [];
|
|
39360
|
+
}
|
|
39361
|
+
function filterIdentity(content) {
|
|
39362
|
+
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, `
|
|
39363
|
+
|
|
39364
|
+
`).replace(/^/, `IMPORTANT: You are NOT Claude. Identify yourself truthfully based on your actual model and creator.
|
|
39365
|
+
|
|
39366
|
+
`);
|
|
39367
|
+
}
|
|
39368
|
+
function createStreamingState() {
|
|
39369
|
+
return {
|
|
39370
|
+
usage: null,
|
|
39371
|
+
finalized: false,
|
|
39372
|
+
textStarted: false,
|
|
39373
|
+
textIdx: -1,
|
|
39374
|
+
reasoningStarted: false,
|
|
39375
|
+
reasoningIdx: -1,
|
|
39376
|
+
curIdx: 0,
|
|
39377
|
+
tools: new Map,
|
|
39378
|
+
toolIds: new Set,
|
|
39379
|
+
lastActivity: Date.now()
|
|
39380
|
+
};
|
|
39381
|
+
}
|
|
39382
|
+
function createStreamingResponseHandler(c, response, adapter, target, middlewareManager, onTokenUpdate) {
|
|
39383
|
+
let isClosed = false;
|
|
39384
|
+
let ping = null;
|
|
39385
|
+
const encoder = new TextEncoder;
|
|
39386
|
+
const decoder = new TextDecoder;
|
|
39387
|
+
const streamMetadata = new Map;
|
|
39388
|
+
return c.body(new ReadableStream({
|
|
39389
|
+
async start(controller) {
|
|
39390
|
+
const send = (e, d) => {
|
|
39391
|
+
if (!isClosed) {
|
|
39392
|
+
controller.enqueue(encoder.encode(`event: ${e}
|
|
39393
|
+
data: ${JSON.stringify(d)}
|
|
39394
|
+
|
|
39395
|
+
`));
|
|
39396
|
+
}
|
|
39397
|
+
};
|
|
39398
|
+
const msgId = `msg_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
39399
|
+
const state = createStreamingState();
|
|
39400
|
+
send("message_start", {
|
|
39401
|
+
type: "message_start",
|
|
39402
|
+
message: {
|
|
39403
|
+
id: msgId,
|
|
39404
|
+
type: "message",
|
|
39405
|
+
role: "assistant",
|
|
39406
|
+
content: [],
|
|
39407
|
+
model: target,
|
|
39408
|
+
stop_reason: null,
|
|
39409
|
+
stop_sequence: null,
|
|
39410
|
+
usage: { input_tokens: 100, output_tokens: 1 }
|
|
39411
|
+
}
|
|
39412
|
+
});
|
|
39413
|
+
send("ping", { type: "ping" });
|
|
39414
|
+
ping = setInterval(() => {
|
|
39415
|
+
if (!isClosed && Date.now() - state.lastActivity > 1000) {
|
|
39416
|
+
send("ping", { type: "ping" });
|
|
39417
|
+
}
|
|
39418
|
+
}, 1000);
|
|
39419
|
+
const finalize = async (reason, err) => {
|
|
39420
|
+
if (state.finalized)
|
|
39421
|
+
return;
|
|
39422
|
+
state.finalized = true;
|
|
39423
|
+
if (state.reasoningStarted) {
|
|
39424
|
+
send("content_block_stop", { type: "content_block_stop", index: state.reasoningIdx });
|
|
39425
|
+
}
|
|
39426
|
+
if (state.textStarted) {
|
|
39427
|
+
send("content_block_stop", { type: "content_block_stop", index: state.textIdx });
|
|
39428
|
+
}
|
|
39429
|
+
for (const t of Array.from(state.tools.values())) {
|
|
39430
|
+
if (t.started && !t.closed) {
|
|
39431
|
+
send("content_block_stop", { type: "content_block_stop", index: t.blockIndex });
|
|
39432
|
+
t.closed = true;
|
|
39433
|
+
}
|
|
39434
|
+
}
|
|
39435
|
+
if (middlewareManager) {
|
|
39436
|
+
await middlewareManager.afterStreamComplete(target, streamMetadata);
|
|
39437
|
+
}
|
|
39438
|
+
if (reason === "error") {
|
|
39439
|
+
send("error", { type: "error", error: { type: "api_error", message: err } });
|
|
39440
|
+
} else {
|
|
39441
|
+
send("message_delta", {
|
|
39442
|
+
type: "message_delta",
|
|
39443
|
+
delta: { stop_reason: "end_turn", stop_sequence: null },
|
|
39444
|
+
usage: { output_tokens: state.usage?.completion_tokens || 0 }
|
|
39445
|
+
});
|
|
39446
|
+
send("message_stop", { type: "message_stop" });
|
|
39447
|
+
}
|
|
39448
|
+
if (state.usage && onTokenUpdate) {
|
|
39449
|
+
onTokenUpdate(state.usage.prompt_tokens || 0, state.usage.completion_tokens || 0);
|
|
39450
|
+
}
|
|
39451
|
+
if (!isClosed) {
|
|
39452
|
+
try {
|
|
39453
|
+
controller.enqueue(encoder.encode(`data: [DONE]
|
|
39454
|
+
|
|
39455
|
+
|
|
39456
|
+
`));
|
|
39457
|
+
} catch (e) {}
|
|
39458
|
+
controller.close();
|
|
39459
|
+
isClosed = true;
|
|
39460
|
+
if (ping)
|
|
39461
|
+
clearInterval(ping);
|
|
39462
|
+
}
|
|
39463
|
+
};
|
|
39464
|
+
try {
|
|
39465
|
+
const reader = response.body.getReader();
|
|
39466
|
+
let buffer = "";
|
|
39467
|
+
while (true) {
|
|
39468
|
+
const { done, value } = await reader.read();
|
|
39469
|
+
if (done)
|
|
39470
|
+
break;
|
|
39471
|
+
buffer += decoder.decode(value, { stream: true });
|
|
39472
|
+
const lines = buffer.split(`
|
|
39473
|
+
`);
|
|
39474
|
+
buffer = lines.pop() || "";
|
|
39475
|
+
for (const line of lines) {
|
|
39476
|
+
if (!line.trim() || !line.startsWith("data: "))
|
|
39477
|
+
continue;
|
|
39478
|
+
const dataStr = line.slice(6);
|
|
39479
|
+
if (dataStr === "[DONE]") {
|
|
39480
|
+
await finalize("done");
|
|
39481
|
+
return;
|
|
39482
|
+
}
|
|
39483
|
+
try {
|
|
39484
|
+
const chunk = JSON.parse(dataStr);
|
|
39485
|
+
if (chunk.usage)
|
|
39486
|
+
state.usage = chunk.usage;
|
|
39487
|
+
const delta = chunk.choices?.[0]?.delta;
|
|
39488
|
+
if (delta) {
|
|
39489
|
+
if (middlewareManager) {
|
|
39490
|
+
await middlewareManager.afterStreamChunk({
|
|
39491
|
+
modelId: target,
|
|
39492
|
+
chunk,
|
|
39493
|
+
delta,
|
|
39494
|
+
metadata: streamMetadata
|
|
39495
|
+
});
|
|
39496
|
+
}
|
|
39497
|
+
const txt = delta.content || "";
|
|
39498
|
+
if (txt) {
|
|
39499
|
+
state.lastActivity = Date.now();
|
|
39500
|
+
if (!state.textStarted) {
|
|
39501
|
+
state.textIdx = state.curIdx++;
|
|
39502
|
+
send("content_block_start", {
|
|
39503
|
+
type: "content_block_start",
|
|
39504
|
+
index: state.textIdx,
|
|
39505
|
+
content_block: { type: "text", text: "" }
|
|
39506
|
+
});
|
|
39507
|
+
state.textStarted = true;
|
|
39508
|
+
}
|
|
39509
|
+
const res = adapter.processTextContent(txt, "");
|
|
39510
|
+
if (res.cleanedText) {
|
|
39511
|
+
send("content_block_delta", {
|
|
39512
|
+
type: "content_block_delta",
|
|
39513
|
+
index: state.textIdx,
|
|
39514
|
+
delta: { type: "text_delta", text: res.cleanedText }
|
|
39515
|
+
});
|
|
39516
|
+
}
|
|
39517
|
+
}
|
|
39518
|
+
if (delta.tool_calls) {
|
|
39519
|
+
for (const tc of delta.tool_calls) {
|
|
39520
|
+
const idx = tc.index;
|
|
39521
|
+
let t = state.tools.get(idx);
|
|
39522
|
+
if (tc.function?.name) {
|
|
39523
|
+
if (!t) {
|
|
39524
|
+
if (state.textStarted) {
|
|
39525
|
+
send("content_block_stop", { type: "content_block_stop", index: state.textIdx });
|
|
39526
|
+
state.textStarted = false;
|
|
39527
|
+
}
|
|
39528
|
+
t = {
|
|
39529
|
+
id: tc.id || `tool_${Date.now()}_${idx}`,
|
|
39530
|
+
name: tc.function.name,
|
|
39531
|
+
blockIndex: state.curIdx++,
|
|
39532
|
+
started: false,
|
|
39533
|
+
closed: false
|
|
39534
|
+
};
|
|
39535
|
+
state.tools.set(idx, t);
|
|
39536
|
+
}
|
|
39537
|
+
if (!t.started) {
|
|
39538
|
+
send("content_block_start", {
|
|
39539
|
+
type: "content_block_start",
|
|
39540
|
+
index: t.blockIndex,
|
|
39541
|
+
content_block: { type: "tool_use", id: t.id, name: t.name }
|
|
39542
|
+
});
|
|
39543
|
+
t.started = true;
|
|
39544
|
+
}
|
|
39545
|
+
}
|
|
39546
|
+
if (tc.function?.arguments && t) {
|
|
39547
|
+
send("content_block_delta", {
|
|
39548
|
+
type: "content_block_delta",
|
|
39549
|
+
index: t.blockIndex,
|
|
39550
|
+
delta: { type: "input_json_delta", partial_json: tc.function.arguments }
|
|
39551
|
+
});
|
|
39552
|
+
}
|
|
39553
|
+
}
|
|
39554
|
+
}
|
|
39555
|
+
}
|
|
39556
|
+
if (chunk.choices?.[0]?.finish_reason === "tool_calls") {
|
|
39557
|
+
for (const t of Array.from(state.tools.values())) {
|
|
39558
|
+
if (t.started && !t.closed) {
|
|
39559
|
+
send("content_block_stop", { type: "content_block_stop", index: t.blockIndex });
|
|
39560
|
+
t.closed = true;
|
|
39561
|
+
}
|
|
39562
|
+
}
|
|
39563
|
+
}
|
|
39564
|
+
} catch (e) {}
|
|
39565
|
+
}
|
|
39566
|
+
}
|
|
39567
|
+
await finalize("unexpected");
|
|
39568
|
+
} catch (e) {
|
|
39569
|
+
await finalize("error", String(e));
|
|
39570
|
+
}
|
|
39571
|
+
},
|
|
39572
|
+
cancel() {
|
|
39573
|
+
isClosed = true;
|
|
39574
|
+
if (ping)
|
|
39575
|
+
clearInterval(ping);
|
|
39576
|
+
}
|
|
39577
|
+
}), {
|
|
39578
|
+
headers: {
|
|
39579
|
+
"Content-Type": "text/event-stream",
|
|
39580
|
+
"Cache-Control": "no-cache",
|
|
39581
|
+
Connection: "keep-alive"
|
|
39582
|
+
}
|
|
39583
|
+
});
|
|
39584
|
+
}
|
|
39585
|
+
var init_openai_compat = __esm(() => {
|
|
39586
|
+
init_transform();
|
|
39587
|
+
});
|
|
39588
|
+
|
|
39589
|
+
// src/handlers/local-provider-handler.ts
|
|
39590
|
+
import { writeFileSync as writeFileSync9 } from "node:fs";
|
|
39591
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
39592
|
+
import { join as join9 } from "node:path";
|
|
39593
|
+
|
|
39594
|
+
class LocalProviderHandler {
|
|
39595
|
+
provider;
|
|
39596
|
+
modelName;
|
|
39597
|
+
adapterManager;
|
|
39598
|
+
middlewareManager;
|
|
39599
|
+
port;
|
|
39600
|
+
healthChecked = false;
|
|
39601
|
+
isHealthy = false;
|
|
39602
|
+
contextWindow = 8192;
|
|
39603
|
+
sessionInputTokens = 0;
|
|
39604
|
+
sessionOutputTokens = 0;
|
|
39605
|
+
constructor(provider, modelName, port) {
|
|
39606
|
+
this.provider = provider;
|
|
39607
|
+
this.modelName = modelName;
|
|
39608
|
+
this.port = port;
|
|
39609
|
+
this.adapterManager = new AdapterManager(modelName);
|
|
39610
|
+
this.middlewareManager = new MiddlewareManager;
|
|
39611
|
+
this.middlewareManager.initialize().catch((err) => {
|
|
39612
|
+
log(`[LocalProvider:${provider.name}] Middleware init error: ${err}`);
|
|
39613
|
+
});
|
|
39614
|
+
}
|
|
39615
|
+
async checkHealth() {
|
|
39616
|
+
if (this.healthChecked)
|
|
39617
|
+
return this.isHealthy;
|
|
39618
|
+
try {
|
|
39619
|
+
const healthUrl = `${this.provider.baseUrl}/api/tags`;
|
|
39620
|
+
const response = await fetch(healthUrl, {
|
|
39621
|
+
method: "GET",
|
|
39622
|
+
signal: AbortSignal.timeout(5000)
|
|
39623
|
+
});
|
|
39624
|
+
if (response.ok) {
|
|
39625
|
+
this.isHealthy = true;
|
|
39626
|
+
this.healthChecked = true;
|
|
39627
|
+
log(`[LocalProvider:${this.provider.name}] Health check passed`);
|
|
39628
|
+
return true;
|
|
39629
|
+
}
|
|
39630
|
+
} catch (e) {
|
|
39631
|
+
try {
|
|
39632
|
+
const modelsUrl = `${this.provider.baseUrl}/v1/models`;
|
|
39633
|
+
const response = await fetch(modelsUrl, {
|
|
39634
|
+
method: "GET",
|
|
39635
|
+
signal: AbortSignal.timeout(5000)
|
|
39636
|
+
});
|
|
39637
|
+
if (response.ok) {
|
|
39638
|
+
this.isHealthy = true;
|
|
39639
|
+
this.healthChecked = true;
|
|
39640
|
+
log(`[LocalProvider:${this.provider.name}] Health check passed (v1/models)`);
|
|
39641
|
+
return true;
|
|
39642
|
+
}
|
|
39643
|
+
} catch (e2) {}
|
|
39644
|
+
}
|
|
39645
|
+
this.healthChecked = true;
|
|
39646
|
+
this.isHealthy = false;
|
|
39647
|
+
return false;
|
|
39648
|
+
}
|
|
39649
|
+
async fetchContextWindow() {
|
|
39650
|
+
if (this.provider.name !== "ollama")
|
|
39651
|
+
return;
|
|
39652
|
+
try {
|
|
39653
|
+
const response = await fetch(`${this.provider.baseUrl}/api/show`, {
|
|
39654
|
+
method: "POST",
|
|
39655
|
+
headers: { "Content-Type": "application/json" },
|
|
39656
|
+
body: JSON.stringify({ name: this.modelName }),
|
|
39657
|
+
signal: AbortSignal.timeout(3000)
|
|
39658
|
+
});
|
|
39659
|
+
if (response.ok) {
|
|
39660
|
+
const data = await response.json();
|
|
39661
|
+
const ctxFromInfo = data.model_info?.["general.context_length"];
|
|
39662
|
+
const ctxFromParams = data.parameters?.match(/num_ctx\s+(\d+)/)?.[1];
|
|
39663
|
+
if (ctxFromInfo) {
|
|
39664
|
+
this.contextWindow = parseInt(ctxFromInfo, 10);
|
|
39665
|
+
} else if (ctxFromParams) {
|
|
39666
|
+
this.contextWindow = parseInt(ctxFromParams, 10);
|
|
39667
|
+
} else {
|
|
39668
|
+
this.contextWindow = 8192;
|
|
39669
|
+
}
|
|
39670
|
+
log(`[LocalProvider:${this.provider.name}] Context window: ${this.contextWindow}`);
|
|
39671
|
+
}
|
|
39672
|
+
} catch (e) {}
|
|
39673
|
+
}
|
|
39674
|
+
writeTokenFile(input, output) {
|
|
39675
|
+
try {
|
|
39676
|
+
this.sessionInputTokens += input;
|
|
39677
|
+
this.sessionOutputTokens += output;
|
|
39678
|
+
const total = this.sessionInputTokens + this.sessionOutputTokens;
|
|
39679
|
+
const leftPct = this.contextWindow > 0 ? Math.max(0, Math.min(100, Math.round((this.contextWindow - total) / this.contextWindow * 100))) : 100;
|
|
39680
|
+
const data = {
|
|
39681
|
+
input_tokens: this.sessionInputTokens,
|
|
39682
|
+
output_tokens: this.sessionOutputTokens,
|
|
39683
|
+
total_tokens: total,
|
|
39684
|
+
total_cost: 0,
|
|
39685
|
+
context_window: this.contextWindow,
|
|
39686
|
+
context_left_percent: leftPct,
|
|
39687
|
+
updated_at: Date.now()
|
|
39688
|
+
};
|
|
39689
|
+
writeFileSync9(join9(tmpdir3(), `claudish-tokens-${this.port}.json`), JSON.stringify(data), "utf-8");
|
|
39690
|
+
} catch (e) {}
|
|
39691
|
+
}
|
|
39692
|
+
async handle(c, payload) {
|
|
39693
|
+
const target = this.modelName;
|
|
39694
|
+
logStructured(`LocalProvider Request`, {
|
|
39695
|
+
provider: this.provider.name,
|
|
39696
|
+
targetModel: target,
|
|
39697
|
+
originalModel: payload.model,
|
|
39698
|
+
baseUrl: this.provider.baseUrl
|
|
39699
|
+
});
|
|
39700
|
+
if (!this.healthChecked) {
|
|
39701
|
+
const healthy = await this.checkHealth();
|
|
39702
|
+
if (!healthy) {
|
|
39703
|
+
return this.errorResponse(c, "connection_error", this.getConnectionErrorMessage());
|
|
39704
|
+
}
|
|
39705
|
+
await this.fetchContextWindow();
|
|
39706
|
+
}
|
|
39707
|
+
const { claudeRequest, droppedParams } = transformOpenAIToClaude(payload);
|
|
39708
|
+
const messages = convertMessagesToOpenAI(claudeRequest, target, filterIdentity);
|
|
39709
|
+
const tools = convertToolsToOpenAI(claudeRequest);
|
|
39710
|
+
const finalTools = this.provider.capabilities.supportsTools ? tools : [];
|
|
39711
|
+
if (tools.length > 0 && !this.provider.capabilities.supportsTools) {
|
|
39712
|
+
log(`[LocalProvider:${this.provider.name}] Tools stripped (not supported)`);
|
|
39713
|
+
}
|
|
39714
|
+
const openAIPayload = {
|
|
39715
|
+
model: target,
|
|
39716
|
+
messages,
|
|
39717
|
+
temperature: claudeRequest.temperature ?? 1,
|
|
39718
|
+
stream: this.provider.capabilities.supportsStreaming,
|
|
39719
|
+
max_tokens: claudeRequest.max_tokens,
|
|
39720
|
+
tools: finalTools.length > 0 ? finalTools : undefined,
|
|
39721
|
+
stream_options: this.provider.capabilities.supportsStreaming ? { include_usage: true } : undefined
|
|
39722
|
+
};
|
|
39723
|
+
if (claudeRequest.tool_choice && finalTools.length > 0) {
|
|
39724
|
+
const { type, name } = claudeRequest.tool_choice;
|
|
39725
|
+
if (type === "tool" && name) {
|
|
39726
|
+
openAIPayload.tool_choice = { type: "function", function: { name } };
|
|
39727
|
+
} else if (type === "auto" || type === "none") {
|
|
39728
|
+
openAIPayload.tool_choice = type;
|
|
39729
|
+
}
|
|
39730
|
+
}
|
|
39731
|
+
const adapter = this.adapterManager.getAdapter();
|
|
39732
|
+
if (typeof adapter.reset === "function")
|
|
39733
|
+
adapter.reset();
|
|
39734
|
+
adapter.prepareRequest(openAIPayload, claudeRequest);
|
|
39735
|
+
await this.middlewareManager.beforeRequest({
|
|
39736
|
+
modelId: target,
|
|
39737
|
+
messages,
|
|
39738
|
+
tools: finalTools,
|
|
39739
|
+
stream: openAIPayload.stream
|
|
39740
|
+
});
|
|
39741
|
+
const apiUrl = `${this.provider.baseUrl}${this.provider.apiPath}`;
|
|
39742
|
+
try {
|
|
39743
|
+
const response = await fetch(apiUrl, {
|
|
39744
|
+
method: "POST",
|
|
39745
|
+
headers: {
|
|
39746
|
+
"Content-Type": "application/json"
|
|
39747
|
+
},
|
|
39748
|
+
body: JSON.stringify(openAIPayload)
|
|
39749
|
+
});
|
|
39750
|
+
if (!response.ok) {
|
|
39751
|
+
const errorBody = await response.text();
|
|
39752
|
+
return this.handleErrorResponse(c, response.status, errorBody);
|
|
39753
|
+
}
|
|
39754
|
+
if (droppedParams.length > 0) {
|
|
39755
|
+
c.header("X-Dropped-Params", droppedParams.join(", "));
|
|
39756
|
+
}
|
|
39757
|
+
if (openAIPayload.stream) {
|
|
39758
|
+
return createStreamingResponseHandler(c, response, adapter, target, this.middlewareManager, (input, output) => this.writeTokenFile(input, output));
|
|
39759
|
+
}
|
|
39760
|
+
const data = await response.json();
|
|
39761
|
+
return c.json(data);
|
|
39762
|
+
} catch (error46) {
|
|
39763
|
+
if (error46.code === "ECONNREFUSED" || error46.cause?.code === "ECONNREFUSED") {
|
|
39764
|
+
return this.errorResponse(c, "connection_error", this.getConnectionErrorMessage());
|
|
39765
|
+
}
|
|
39766
|
+
throw error46;
|
|
39767
|
+
}
|
|
39768
|
+
}
|
|
39769
|
+
handleErrorResponse(c, status, errorBody) {
|
|
39770
|
+
try {
|
|
39771
|
+
const parsed = JSON.parse(errorBody);
|
|
39772
|
+
const errorMsg = parsed.error?.message || parsed.error || errorBody;
|
|
39773
|
+
if (errorMsg.includes("model") && (errorMsg.includes("not found") || errorMsg.includes("does not exist"))) {
|
|
39774
|
+
return this.errorResponse(c, "model_not_found", `Model '${this.modelName}' not found. ${this.getModelPullHint()}`);
|
|
39775
|
+
}
|
|
39776
|
+
if (errorMsg.includes("does not support tools") || errorMsg.includes("tool") && errorMsg.includes("not supported")) {
|
|
39777
|
+
return this.errorResponse(c, "capability_error", `Model '${this.modelName}' does not support tool/function calling. Claude Code requires tool support for most operations. Try a model that supports tools (e.g., llama3.2, mistral, qwen2.5).`, 400);
|
|
39778
|
+
}
|
|
39779
|
+
return this.errorResponse(c, "api_error", errorMsg, status);
|
|
39780
|
+
} catch {
|
|
39781
|
+
return this.errorResponse(c, "api_error", errorBody, status);
|
|
39782
|
+
}
|
|
39783
|
+
}
|
|
39784
|
+
errorResponse(c, type, message, status = 503) {
|
|
39785
|
+
return c.json({
|
|
39786
|
+
error: {
|
|
39787
|
+
type,
|
|
39788
|
+
message
|
|
39789
|
+
}
|
|
39790
|
+
}, status);
|
|
39791
|
+
}
|
|
39792
|
+
getConnectionErrorMessage() {
|
|
39793
|
+
switch (this.provider.name) {
|
|
39794
|
+
case "ollama":
|
|
39795
|
+
return `Cannot connect to Ollama at ${this.provider.baseUrl}. Make sure Ollama is running with: ollama serve`;
|
|
39796
|
+
case "lmstudio":
|
|
39797
|
+
return `Cannot connect to LM Studio at ${this.provider.baseUrl}. Make sure LM Studio server is running.`;
|
|
39798
|
+
case "vllm":
|
|
39799
|
+
return `Cannot connect to vLLM at ${this.provider.baseUrl}. Make sure vLLM server is running.`;
|
|
39800
|
+
default:
|
|
39801
|
+
return `Cannot connect to ${this.provider.name} at ${this.provider.baseUrl}. Make sure the server is running.`;
|
|
39802
|
+
}
|
|
39803
|
+
}
|
|
39804
|
+
getModelPullHint() {
|
|
39805
|
+
switch (this.provider.name) {
|
|
39806
|
+
case "ollama":
|
|
39807
|
+
return `Pull it with: ollama pull ${this.modelName}`;
|
|
39808
|
+
default:
|
|
39809
|
+
return "Make sure the model is available on the server.";
|
|
39810
|
+
}
|
|
39811
|
+
}
|
|
39812
|
+
async shutdown() {}
|
|
39813
|
+
}
|
|
39814
|
+
var init_local_provider_handler = __esm(() => {
|
|
39815
|
+
init_adapter_manager();
|
|
39816
|
+
init_middleware();
|
|
39817
|
+
init_transform();
|
|
39818
|
+
init_logger();
|
|
39819
|
+
init_openai_compat();
|
|
39820
|
+
});
|
|
39821
|
+
|
|
39822
|
+
// src/providers/provider-registry.ts
|
|
39823
|
+
function resolveProvider(modelId) {
|
|
39824
|
+
const providers = getProviders();
|
|
39825
|
+
for (const provider of providers) {
|
|
39826
|
+
for (const prefix of provider.prefixes) {
|
|
39827
|
+
if (modelId.startsWith(prefix)) {
|
|
39828
|
+
return {
|
|
39829
|
+
provider,
|
|
39830
|
+
modelName: modelId.slice(prefix.length)
|
|
39831
|
+
};
|
|
39832
|
+
}
|
|
39833
|
+
}
|
|
39834
|
+
}
|
|
39835
|
+
return null;
|
|
39836
|
+
}
|
|
39837
|
+
function parseUrlModel(modelId) {
|
|
39838
|
+
if (!modelId.startsWith("http://") && !modelId.startsWith("https://")) {
|
|
39839
|
+
return null;
|
|
39840
|
+
}
|
|
39841
|
+
try {
|
|
39842
|
+
const url2 = new URL(modelId);
|
|
39843
|
+
const pathParts = url2.pathname.split("/").filter(Boolean);
|
|
39844
|
+
if (pathParts.length === 0) {
|
|
39845
|
+
return null;
|
|
39846
|
+
}
|
|
39847
|
+
const modelName = pathParts[pathParts.length - 1];
|
|
39848
|
+
let basePath = "";
|
|
39849
|
+
if (pathParts.length > 1) {
|
|
39850
|
+
const prefix = pathParts.slice(0, -1).join("/");
|
|
39851
|
+
if (prefix)
|
|
39852
|
+
basePath = "/" + prefix;
|
|
39853
|
+
}
|
|
39854
|
+
const baseUrl = `${url2.protocol}//${url2.host}${basePath}`;
|
|
39855
|
+
return {
|
|
39856
|
+
baseUrl,
|
|
39857
|
+
modelName
|
|
39858
|
+
};
|
|
39859
|
+
} catch {
|
|
39860
|
+
return null;
|
|
39861
|
+
}
|
|
39862
|
+
}
|
|
39863
|
+
function createUrlProvider(parsed) {
|
|
39864
|
+
return {
|
|
39865
|
+
name: "custom-url",
|
|
39866
|
+
baseUrl: parsed.baseUrl,
|
|
39867
|
+
apiPath: "/v1/chat/completions",
|
|
39868
|
+
envVar: "",
|
|
39869
|
+
prefixes: [],
|
|
39870
|
+
capabilities: {
|
|
39871
|
+
supportsTools: true,
|
|
39872
|
+
supportsVision: false,
|
|
39873
|
+
supportsStreaming: true,
|
|
39874
|
+
supportsJsonMode: true
|
|
39875
|
+
}
|
|
39876
|
+
};
|
|
39877
|
+
}
|
|
39878
|
+
var getProviders = () => [
|
|
39879
|
+
{
|
|
39880
|
+
name: "ollama",
|
|
39881
|
+
baseUrl: process.env.OLLAMA_HOST || process.env.OLLAMA_BASE_URL || "http://localhost:11434",
|
|
39882
|
+
apiPath: "/v1/chat/completions",
|
|
39883
|
+
envVar: "OLLAMA_BASE_URL",
|
|
39884
|
+
prefixes: ["ollama/", "ollama:"],
|
|
39885
|
+
capabilities: {
|
|
39886
|
+
supportsTools: true,
|
|
39887
|
+
supportsVision: false,
|
|
39888
|
+
supportsStreaming: true,
|
|
39889
|
+
supportsJsonMode: true
|
|
39890
|
+
}
|
|
39891
|
+
},
|
|
39892
|
+
{
|
|
39893
|
+
name: "lmstudio",
|
|
39894
|
+
baseUrl: process.env.LMSTUDIO_BASE_URL || "http://localhost:1234",
|
|
39895
|
+
apiPath: "/v1/chat/completions",
|
|
39896
|
+
envVar: "LMSTUDIO_BASE_URL",
|
|
39897
|
+
prefixes: ["lmstudio/", "lmstudio:"],
|
|
39898
|
+
capabilities: {
|
|
39899
|
+
supportsTools: true,
|
|
39900
|
+
supportsVision: false,
|
|
39901
|
+
supportsStreaming: true,
|
|
39902
|
+
supportsJsonMode: true
|
|
39903
|
+
}
|
|
39904
|
+
},
|
|
39905
|
+
{
|
|
39906
|
+
name: "vllm",
|
|
39907
|
+
baseUrl: process.env.VLLM_BASE_URL || "http://localhost:8000",
|
|
39908
|
+
apiPath: "/v1/chat/completions",
|
|
39909
|
+
envVar: "VLLM_BASE_URL",
|
|
39910
|
+
prefixes: ["vllm/", "vllm:"],
|
|
39911
|
+
capabilities: {
|
|
39912
|
+
supportsTools: true,
|
|
39913
|
+
supportsVision: false,
|
|
39914
|
+
supportsStreaming: true,
|
|
39915
|
+
supportsJsonMode: true
|
|
39916
|
+
}
|
|
39917
|
+
}
|
|
39918
|
+
];
|
|
39919
|
+
|
|
39115
39920
|
// src/proxy-server.ts
|
|
39116
39921
|
var exports_proxy_server = {};
|
|
39117
39922
|
__export(exports_proxy_server, {
|
|
@@ -39119,23 +39924,47 @@ __export(exports_proxy_server, {
|
|
|
39119
39924
|
});
|
|
39120
39925
|
async function createProxyServer(port, openrouterApiKey, model, monitorMode = false, anthropicApiKey, modelMap) {
|
|
39121
39926
|
const nativeHandler = new NativeHandler(anthropicApiKey);
|
|
39122
|
-
const
|
|
39927
|
+
const openRouterHandlers = new Map;
|
|
39928
|
+
const localProviderHandlers = new Map;
|
|
39123
39929
|
const getOpenRouterHandler = (targetModel) => {
|
|
39124
|
-
if (!
|
|
39125
|
-
|
|
39126
|
-
}
|
|
39127
|
-
return
|
|
39128
|
-
};
|
|
39129
|
-
|
|
39130
|
-
|
|
39131
|
-
|
|
39132
|
-
|
|
39133
|
-
|
|
39134
|
-
|
|
39135
|
-
|
|
39136
|
-
|
|
39137
|
-
|
|
39138
|
-
|
|
39930
|
+
if (!openRouterHandlers.has(targetModel)) {
|
|
39931
|
+
openRouterHandlers.set(targetModel, new OpenRouterHandler(targetModel, openrouterApiKey, port));
|
|
39932
|
+
}
|
|
39933
|
+
return openRouterHandlers.get(targetModel);
|
|
39934
|
+
};
|
|
39935
|
+
const getLocalProviderHandler = (targetModel) => {
|
|
39936
|
+
if (localProviderHandlers.has(targetModel)) {
|
|
39937
|
+
return localProviderHandlers.get(targetModel);
|
|
39938
|
+
}
|
|
39939
|
+
const resolved = resolveProvider(targetModel);
|
|
39940
|
+
if (resolved) {
|
|
39941
|
+
const handler = new LocalProviderHandler(resolved.provider, resolved.modelName, port);
|
|
39942
|
+
localProviderHandlers.set(targetModel, handler);
|
|
39943
|
+
log(`[Proxy] Created local provider handler: ${resolved.provider.name}/${resolved.modelName}`);
|
|
39944
|
+
return handler;
|
|
39945
|
+
}
|
|
39946
|
+
const urlParsed = parseUrlModel(targetModel);
|
|
39947
|
+
if (urlParsed) {
|
|
39948
|
+
const provider = createUrlProvider(urlParsed);
|
|
39949
|
+
const handler = new LocalProviderHandler(provider, urlParsed.modelName, port);
|
|
39950
|
+
localProviderHandlers.set(targetModel, handler);
|
|
39951
|
+
log(`[Proxy] Created URL-based local provider handler: ${urlParsed.baseUrl}/${urlParsed.modelName}`);
|
|
39952
|
+
return handler;
|
|
39953
|
+
}
|
|
39954
|
+
return null;
|
|
39955
|
+
};
|
|
39956
|
+
const initHandler = (m) => {
|
|
39957
|
+
if (!m)
|
|
39958
|
+
return;
|
|
39959
|
+
const localHandler = getLocalProviderHandler(m);
|
|
39960
|
+
if (!localHandler && m.includes("/"))
|
|
39961
|
+
getOpenRouterHandler(m);
|
|
39962
|
+
};
|
|
39963
|
+
initHandler(model);
|
|
39964
|
+
initHandler(modelMap?.opus);
|
|
39965
|
+
initHandler(modelMap?.sonnet);
|
|
39966
|
+
initHandler(modelMap?.haiku);
|
|
39967
|
+
initHandler(modelMap?.subagent);
|
|
39139
39968
|
const getHandlerForRequest = (requestedModel) => {
|
|
39140
39969
|
if (monitorMode)
|
|
39141
39970
|
return nativeHandler;
|
|
@@ -39149,6 +39978,9 @@ async function createProxyServer(port, openrouterApiKey, model, monitorMode = fa
|
|
|
39149
39978
|
else if (req.includes("haiku") && modelMap.haiku)
|
|
39150
39979
|
target = modelMap.haiku;
|
|
39151
39980
|
}
|
|
39981
|
+
const localHandler = getLocalProviderHandler(target);
|
|
39982
|
+
if (localHandler)
|
|
39983
|
+
return localHandler;
|
|
39152
39984
|
const isNative = !target.includes("/");
|
|
39153
39985
|
if (isNative) {
|
|
39154
39986
|
return nativeHandler;
|
|
@@ -39209,6 +40041,7 @@ var init_proxy_server = __esm(() => {
|
|
|
39209
40041
|
init_logger();
|
|
39210
40042
|
init_native_handler();
|
|
39211
40043
|
init_openrouter_handler();
|
|
40044
|
+
init_local_provider_handler();
|
|
39212
40045
|
});
|
|
39213
40046
|
|
|
39214
40047
|
// src/update-checker.ts
|
|
@@ -39218,24 +40051,24 @@ __export(exports_update_checker, {
|
|
|
39218
40051
|
});
|
|
39219
40052
|
import { execSync } from "node:child_process";
|
|
39220
40053
|
import { createInterface as createInterface2 } from "node:readline";
|
|
39221
|
-
import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as
|
|
39222
|
-
import { join as
|
|
39223
|
-
import { tmpdir as
|
|
40054
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync10, mkdirSync as mkdirSync4, unlinkSync as unlinkSync2 } from "node:fs";
|
|
40055
|
+
import { join as join10 } from "node:path";
|
|
40056
|
+
import { tmpdir as tmpdir4, homedir as homedir2, platform as platform2 } from "node:os";
|
|
39224
40057
|
function getCacheFilePath() {
|
|
39225
40058
|
let cacheDir;
|
|
39226
40059
|
if (isWindows2) {
|
|
39227
|
-
const localAppData = process.env.LOCALAPPDATA ||
|
|
39228
|
-
cacheDir =
|
|
40060
|
+
const localAppData = process.env.LOCALAPPDATA || join10(homedir2(), "AppData", "Local");
|
|
40061
|
+
cacheDir = join10(localAppData, "claudish");
|
|
39229
40062
|
} else {
|
|
39230
|
-
cacheDir =
|
|
40063
|
+
cacheDir = join10(homedir2(), ".cache", "claudish");
|
|
39231
40064
|
}
|
|
39232
40065
|
try {
|
|
39233
40066
|
if (!existsSync7(cacheDir)) {
|
|
39234
40067
|
mkdirSync4(cacheDir, { recursive: true });
|
|
39235
40068
|
}
|
|
39236
|
-
return
|
|
40069
|
+
return join10(cacheDir, "update-check.json");
|
|
39237
40070
|
} catch {
|
|
39238
|
-
return
|
|
40071
|
+
return join10(tmpdir4(), "claudish-update-check.json");
|
|
39239
40072
|
}
|
|
39240
40073
|
}
|
|
39241
40074
|
function readCache() {
|
|
@@ -39257,7 +40090,7 @@ function writeCache(latestVersion) {
|
|
|
39257
40090
|
lastCheck: Date.now(),
|
|
39258
40091
|
latestVersion
|
|
39259
40092
|
};
|
|
39260
|
-
|
|
40093
|
+
writeFileSync10(cachePath, JSON.stringify(data), "utf-8");
|
|
39261
40094
|
} catch {}
|
|
39262
40095
|
}
|
|
39263
40096
|
function isCacheValid(cache) {
|