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.
Files changed (3) hide show
  1. package/README.md +23 -26
  2. package/dist/index.js +879 -46
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -24,47 +24,44 @@
24
24
 
25
25
  ## Installation
26
26
 
27
- ### Prerequisites
27
+ ### Quick Install
28
28
 
29
- - **Node.js 18+** or **Bun 1.0+** - JavaScript runtime (either works!)
30
- - [Claude Code](https://claude.com/claude-code) - Claude CLI must be installed
31
- - [OpenRouter API Key](https://openrouter.ai/keys) - Free tier available
29
+ ```bash
30
+ # Shell script (Linux/macOS)
31
+ curl -fsSL https://raw.githubusercontent.com/MadAppGang/claudish/main/install.sh | bash
32
32
 
33
- ### Install Claudish
33
+ # Homebrew (macOS)
34
+ brew tap MadAppGang/claudish && brew install claudish
34
35
 
35
- **✨ NEW in v1.3.0: Universal compatibility! Works with both Node.js and Bun.**
36
+ # npm
37
+ npm install -g claudish
36
38
 
37
- **Option 1: Use without installing (recommended)**
39
+ # Bun
40
+ bun install -g claudish
41
+ ```
38
42
 
39
- ```bash
40
- # With Node.js (works everywhere)
41
- npx claudish@latest --model x-ai/grok-code-fast-1 "your prompt"
43
+ ### Prerequisites
42
44
 
43
- # With Bun (faster execution)
44
- bunx claudish@latest --model openai/gpt-5-codex "your prompt"
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
- **Option 2: Install globally**
48
+ ### Other Install Options
48
49
 
49
- ```bash
50
- # With npm (Node.js)
51
- npm install -g claudish
50
+ **Use without installing:**
52
51
 
53
- # With Bun (faster)
54
- bun install -g claudish
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
- **Option 3: Install from source**
57
+ **Install from source:**
58
58
 
59
59
  ```bash
60
- cd mcp/claudish
61
- bun install # or: npm install
62
- bun run build # or: npm 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 costStr = cost.toFixed(3);
34365
- console.log(\`\${CYAN}\${BOLD}\${dir}\${RESET} \${DIM}•\${RESET} \${YELLOW}\${model}\${RESET} \${DIM}•\${RESET} \${GREEN}$\${costStr}\${RESET} \${DIM}•\${RESET} \${MAGENTA}\${ctx}%\${RESET}\`);
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}\\$%.3f${RESET2} ${DIM2}•${RESET2} ${MAGENTA}%s%%${RESET2}\\n" "$DIR" "$CLAUDISH_ACTIVE_MODEL_NAME" "$COST" "$CTX"`;
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 (avg < 0) {
34975
- pricing = "varies";
34976
- } else if (avg === 0) {
34977
- pricing = "FREE";
35035
+ if (model.isLocal) {
35036
+ pricing = "LOCAL";
34978
35037
  } else {
34979
- pricing = `$${avg.toFixed(2)}/1M`;
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
- console.log(` ${modelIdPadded} ${providerPadded} ${pricingPadded} ${contextPadded} ${(score * 100).toFixed(0)}%`);
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: models.length,
35105
+ count: allModels.length,
35106
+ localCount: ollamaModels.length,
35027
35107
  lastUpdated: new Date().toISOString().split("T")[0],
35028
- models: models.map((m) => ({
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
- All OpenRouter Models (${models.length} total):
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: claudish --model <provider/model-id>");
35079
- console.log("Search: claudish --search <query>");
35080
- console.log("Top models: claudish --top-models");
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 handlers = new Map;
39927
+ const openRouterHandlers = new Map;
39928
+ const localProviderHandlers = new Map;
39123
39929
  const getOpenRouterHandler = (targetModel) => {
39124
- if (!handlers.has(targetModel)) {
39125
- handlers.set(targetModel, new OpenRouterHandler(targetModel, openrouterApiKey, port));
39126
- }
39127
- return handlers.get(targetModel);
39128
- };
39129
- if (model)
39130
- getOpenRouterHandler(model);
39131
- if (modelMap?.opus)
39132
- getOpenRouterHandler(modelMap.opus);
39133
- if (modelMap?.sonnet)
39134
- getOpenRouterHandler(modelMap.sonnet);
39135
- if (modelMap?.haiku)
39136
- getOpenRouterHandler(modelMap.haiku);
39137
- if (modelMap?.subagent)
39138
- getOpenRouterHandler(modelMap.subagent);
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 writeFileSync9, mkdirSync as mkdirSync4, unlinkSync as unlinkSync2 } from "node:fs";
39222
- import { join as join9 } from "node:path";
39223
- import { tmpdir as tmpdir3, homedir as homedir2, platform as platform2 } from "node:os";
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 || join9(homedir2(), "AppData", "Local");
39228
- cacheDir = join9(localAppData, "claudish");
40060
+ const localAppData = process.env.LOCALAPPDATA || join10(homedir2(), "AppData", "Local");
40061
+ cacheDir = join10(localAppData, "claudish");
39229
40062
  } else {
39230
- cacheDir = join9(homedir2(), ".cache", "claudish");
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 join9(cacheDir, "update-check.json");
40069
+ return join10(cacheDir, "update-check.json");
39237
40070
  } catch {
39238
- return join9(tmpdir3(), "claudish-update-check.json");
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
- writeFileSync9(cachePath, JSON.stringify(data), "utf-8");
40093
+ writeFileSync10(cachePath, JSON.stringify(data), "utf-8");
39261
40094
  } catch {}
39262
40095
  }
39263
40096
  function isCacheValid(cache) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudish",
3
- "version": "2.8.1",
3
+ "version": "2.10.0",
4
4
  "description": "Run Claude Code with any OpenRouter model - CLI tool and MCP server",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",