claudish 2.8.1 → 2.9.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 +763 -33
  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",
@@ -34912,6 +34916,31 @@ async function parseArgs(args) {
34912
34916
  }
34913
34917
  return config3;
34914
34918
  }
34919
+ async function fetchOllamaModels() {
34920
+ const ollamaHost = process.env.OLLAMA_HOST || process.env.OLLAMA_BASE_URL || "http://localhost:11434";
34921
+ try {
34922
+ const response = await fetch(`${ollamaHost}/api/tags`, {
34923
+ signal: AbortSignal.timeout(3000)
34924
+ });
34925
+ if (!response.ok)
34926
+ return [];
34927
+ const data = await response.json();
34928
+ const models = data.models || [];
34929
+ return models.map((m) => ({
34930
+ id: `ollama/${m.name}`,
34931
+ name: m.name,
34932
+ description: `Local Ollama model (${m.details?.parameter_size || "unknown size"})`,
34933
+ provider: "ollama",
34934
+ context_length: null,
34935
+ pricing: { prompt: "0", completion: "0" },
34936
+ isLocal: true,
34937
+ details: m.details,
34938
+ size: m.size
34939
+ }));
34940
+ } catch (e) {
34941
+ return [];
34942
+ }
34943
+ }
34915
34944
  async function searchAndPrintModels(query, forceUpdate) {
34916
34945
  let models = [];
34917
34946
  if (!forceUpdate && existsSync5(ALL_MODELS_JSON_PATH2)) {
@@ -34943,6 +34972,11 @@ async function searchAndPrintModels(query, forceUpdate) {
34943
34972
  process.exit(1);
34944
34973
  }
34945
34974
  }
34975
+ const ollamaModels = await fetchOllamaModels();
34976
+ if (ollamaModels.length > 0) {
34977
+ console.error(`\uD83C\uDFE0 Found ${ollamaModels.length} local Ollama models`);
34978
+ models = [...ollamaModels, ...models];
34979
+ }
34946
34980
  const results = models.map((model) => {
34947
34981
  const nameScore = fuzzyScore2(model.name || "", query);
34948
34982
  const idScore = fuzzyScore2(model.id || "", query);
@@ -34967,16 +35001,20 @@ Found ${results.length} matching models:
34967
35001
  const providerName = model.id.split("/")[0];
34968
35002
  const provider = providerName.length > 10 ? providerName.substring(0, 7) + "..." : providerName;
34969
35003
  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
35004
  let pricing;
34974
- if (avg < 0) {
34975
- pricing = "varies";
34976
- } else if (avg === 0) {
34977
- pricing = "FREE";
35005
+ if (model.isLocal) {
35006
+ pricing = "LOCAL";
34978
35007
  } else {
34979
- pricing = `$${avg.toFixed(2)}/1M`;
35008
+ const promptPrice = parseFloat(model.pricing?.prompt || "0") * 1e6;
35009
+ const completionPrice = parseFloat(model.pricing?.completion || "0") * 1e6;
35010
+ const avg = (promptPrice + completionPrice) / 2;
35011
+ if (avg < 0) {
35012
+ pricing = "varies";
35013
+ } else if (avg === 0) {
35014
+ pricing = "FREE";
35015
+ } else {
35016
+ pricing = `$${avg.toFixed(2)}/1M`;
35017
+ }
34980
35018
  }
34981
35019
  const pricingPadded = pricing.padEnd(10);
34982
35020
  const contextLen = model.context_length || model.top_provider?.context_length || 0;
@@ -34986,9 +35024,11 @@ Found ${results.length} matching models:
34986
35024
  }
34987
35025
  console.log("");
34988
35026
  console.log("Use a model: claudish --model <model-id>");
35027
+ console.log("Local models: claudish --model ollama/<model-name>");
34989
35028
  }
34990
35029
  async function printAllModels(jsonOutput, forceUpdate) {
34991
35030
  let models = [];
35031
+ const ollamaModels = await fetchOllamaModels();
34992
35032
  if (!forceUpdate && existsSync5(ALL_MODELS_JSON_PATH2)) {
34993
35033
  try {
34994
35034
  const cacheData = JSON.parse(readFileSync5(ALL_MODELS_JSON_PATH2, "utf-8"));
@@ -35022,18 +35062,44 @@ async function printAllModels(jsonOutput, forceUpdate) {
35022
35062
  }
35023
35063
  }
35024
35064
  if (jsonOutput) {
35065
+ const allModels = [...ollamaModels, ...models];
35025
35066
  console.log(JSON.stringify({
35026
- count: models.length,
35067
+ count: allModels.length,
35068
+ localCount: ollamaModels.length,
35027
35069
  lastUpdated: new Date().toISOString().split("T")[0],
35028
- models: models.map((m) => ({
35070
+ models: allModels.map((m) => ({
35029
35071
  id: m.id,
35030
35072
  name: m.name,
35031
35073
  context: m.context_length || m.top_provider?.context_length,
35032
- pricing: m.pricing
35074
+ pricing: m.pricing,
35075
+ isLocal: m.isLocal || false
35033
35076
  }))
35034
35077
  }, null, 2));
35035
35078
  return;
35036
35079
  }
35080
+ if (ollamaModels.length > 0) {
35081
+ console.log(`
35082
+ \uD83C\uDFE0 LOCAL OLLAMA MODELS (${ollamaModels.length} installed):
35083
+ `);
35084
+ console.log(" " + "─".repeat(70));
35085
+ for (const model of ollamaModels) {
35086
+ const shortId = model.name;
35087
+ const modelId = shortId.length > 40 ? shortId.substring(0, 37) + "..." : shortId;
35088
+ const modelIdPadded = modelId.padEnd(42);
35089
+ const size = model.size ? `${(model.size / 1e9).toFixed(1)}GB` : "N/A";
35090
+ const sizePadded = size.padEnd(12);
35091
+ const params = model.details?.parameter_size || "N/A";
35092
+ const paramsPadded = params.padEnd(8);
35093
+ console.log(` ${modelIdPadded} ${sizePadded} ${paramsPadded}`);
35094
+ }
35095
+ console.log("");
35096
+ console.log(" Use: claudish --model ollama/<model-name>");
35097
+ } else {
35098
+ console.log(`
35099
+ \uD83C\uDFE0 LOCAL OLLAMA: Not running or no models installed`);
35100
+ console.log(" Start Ollama: ollama serve");
35101
+ console.log(" Pull a model: ollama pull llama3.2");
35102
+ }
35037
35103
  const byProvider = new Map;
35038
35104
  for (const model of models) {
35039
35105
  const provider = model.id.split("/")[0];
@@ -35044,7 +35110,7 @@ async function printAllModels(jsonOutput, forceUpdate) {
35044
35110
  }
35045
35111
  const sortedProviders = [...byProvider.keys()].sort();
35046
35112
  console.log(`
35047
- All OpenRouter Models (${models.length} total):
35113
+ ☁️ OPENROUTER MODELS (${models.length} total):
35048
35114
  `);
35049
35115
  for (const provider of sortedProviders) {
35050
35116
  const providerModels = byProvider.get(provider);
@@ -35075,9 +35141,10 @@ All OpenRouter Models (${models.length} total):
35075
35141
  }
35076
35142
  console.log(`
35077
35143
  `);
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");
35144
+ console.log("Use a model: claudish --model <provider/model-id>");
35145
+ console.log("Local model: claudish --model ollama/<model-name>");
35146
+ console.log("Search: claudish --search <query>");
35147
+ console.log("Top models: claudish --top-models");
35081
35148
  }
35082
35149
  function isCacheStale() {
35083
35150
  if (!existsSync5(MODELS_JSON_PATH)) {
@@ -35286,7 +35353,7 @@ NOTES:
35286
35353
  ENVIRONMENT VARIABLES:
35287
35354
  Claudish automatically loads .env file from current directory.
35288
35355
 
35289
- OPENROUTER_API_KEY Required: Your OpenRouter API key
35356
+ OPENROUTER_API_KEY Required: Your OpenRouter API key (for OpenRouter models)
35290
35357
  CLAUDISH_MODEL Default model to use (takes priority)
35291
35358
  ANTHROPIC_MODEL Claude Code standard: model to use (fallback)
35292
35359
  CLAUDISH_PORT Default port for proxy
@@ -35302,6 +35369,12 @@ ENVIRONMENT VARIABLES:
35302
35369
  ANTHROPIC_DEFAULT_HAIKU_MODEL Claude Code standard: Haiku model (fallback)
35303
35370
  CLAUDE_CODE_SUBAGENT_MODEL Claude Code standard: sub-agent model (fallback)
35304
35371
 
35372
+ Local providers (OpenAI-compatible):
35373
+ OLLAMA_BASE_URL Ollama server (default: http://localhost:11434)
35374
+ OLLAMA_HOST Alias for OLLAMA_BASE_URL (same default)
35375
+ LMSTUDIO_BASE_URL LM Studio server (default: http://localhost:1234)
35376
+ VLLM_BASE_URL vLLM server (default: http://localhost:8000)
35377
+
35305
35378
  EXAMPLES:
35306
35379
  # Interactive mode (default) - shows model selector
35307
35380
  claudish
@@ -35355,6 +35428,22 @@ EXAMPLES:
35355
35428
  # Verbose mode in single-shot (show [claudish] logs)
35356
35429
  claudish --verbose "analyze code structure"
35357
35430
 
35431
+ LOCAL MODELS (Ollama, LM Studio, vLLM):
35432
+ # Use local Ollama model (prefix syntax)
35433
+ claudish --model ollama/llama3.2 "implement feature"
35434
+ claudish --model ollama:codellama "review this code"
35435
+
35436
+ # Use local LM Studio model
35437
+ claudish --model lmstudio/qwen2.5-coder "write tests"
35438
+
35439
+ # Use any OpenAI-compatible endpoint (URL syntax)
35440
+ claudish --model "http://localhost:11434/llama3.2" "task"
35441
+ claudish --model "http://192.168.1.100:8000/mistral" "remote server"
35442
+
35443
+ # Custom Ollama endpoint
35444
+ OLLAMA_BASE_URL=http://192.168.1.50:11434 claudish --model ollama/llama3.2 "task"
35445
+ OLLAMA_HOST=http://192.168.1.50:11434 claudish --model ollama/llama3.2 "task"
35446
+
35358
35447
  AVAILABLE MODELS:
35359
35448
  List all models: claudish --models
35360
35449
  Search models: claudish --models <query>
@@ -39112,6 +39201,619 @@ var init_openrouter_handler = __esm(() => {
39112
39201
  };
39113
39202
  });
39114
39203
 
39204
+ // src/handlers/shared/openai-compat.ts
39205
+ function convertMessagesToOpenAI(req, modelId, filterIdentityFn) {
39206
+ const messages = [];
39207
+ if (req.system) {
39208
+ let content = Array.isArray(req.system) ? req.system.map((i) => i.text || i).join(`
39209
+
39210
+ `) : req.system;
39211
+ if (filterIdentityFn)
39212
+ content = filterIdentityFn(content);
39213
+ messages.push({ role: "system", content });
39214
+ }
39215
+ if (modelId.includes("grok") || modelId.includes("x-ai")) {
39216
+ const msg = "IMPORTANT: When calling tools, you MUST use the OpenAI tool_calls format with JSON. NEVER use XML format like <xai:function_call>.";
39217
+ if (messages.length > 0 && messages[0].role === "system") {
39218
+ messages[0].content += `
39219
+
39220
+ ` + msg;
39221
+ } else {
39222
+ messages.unshift({ role: "system", content: msg });
39223
+ }
39224
+ }
39225
+ if (req.messages) {
39226
+ for (const msg of req.messages) {
39227
+ if (msg.role === "user")
39228
+ processUserMessage(msg, messages);
39229
+ else if (msg.role === "assistant")
39230
+ processAssistantMessage(msg, messages);
39231
+ }
39232
+ }
39233
+ return messages;
39234
+ }
39235
+ function processUserMessage(msg, messages) {
39236
+ if (Array.isArray(msg.content)) {
39237
+ const contentParts = [];
39238
+ const toolResults = [];
39239
+ const seen = new Set;
39240
+ for (const block of msg.content) {
39241
+ if (block.type === "text") {
39242
+ contentParts.push({ type: "text", text: block.text });
39243
+ } else if (block.type === "image") {
39244
+ contentParts.push({
39245
+ type: "image_url",
39246
+ image_url: { url: `data:${block.source.media_type};base64,${block.source.data}` }
39247
+ });
39248
+ } else if (block.type === "tool_result") {
39249
+ if (seen.has(block.tool_use_id))
39250
+ continue;
39251
+ seen.add(block.tool_use_id);
39252
+ toolResults.push({
39253
+ role: "tool",
39254
+ content: typeof block.content === "string" ? block.content : JSON.stringify(block.content),
39255
+ tool_call_id: block.tool_use_id
39256
+ });
39257
+ }
39258
+ }
39259
+ if (toolResults.length)
39260
+ messages.push(...toolResults);
39261
+ if (contentParts.length)
39262
+ messages.push({ role: "user", content: contentParts });
39263
+ } else {
39264
+ messages.push({ role: "user", content: msg.content });
39265
+ }
39266
+ }
39267
+ function processAssistantMessage(msg, messages) {
39268
+ if (Array.isArray(msg.content)) {
39269
+ const strings = [];
39270
+ const toolCalls = [];
39271
+ const seen = new Set;
39272
+ for (const block of msg.content) {
39273
+ if (block.type === "text") {
39274
+ strings.push(block.text);
39275
+ } else if (block.type === "tool_use") {
39276
+ if (seen.has(block.id))
39277
+ continue;
39278
+ seen.add(block.id);
39279
+ toolCalls.push({
39280
+ id: block.id,
39281
+ type: "function",
39282
+ function: { name: block.name, arguments: JSON.stringify(block.input) }
39283
+ });
39284
+ }
39285
+ }
39286
+ const m = { role: "assistant" };
39287
+ if (strings.length)
39288
+ m.content = strings.join(" ");
39289
+ else if (toolCalls.length)
39290
+ m.content = null;
39291
+ if (toolCalls.length)
39292
+ m.tool_calls = toolCalls;
39293
+ if (m.content !== undefined || m.tool_calls)
39294
+ messages.push(m);
39295
+ } else {
39296
+ messages.push({ role: "assistant", content: msg.content });
39297
+ }
39298
+ }
39299
+ function convertToolsToOpenAI(req) {
39300
+ return req.tools?.map((tool) => ({
39301
+ type: "function",
39302
+ function: {
39303
+ name: tool.name,
39304
+ description: tool.description,
39305
+ parameters: removeUriFormat(tool.input_schema)
39306
+ }
39307
+ })) || [];
39308
+ }
39309
+ function filterIdentity(content) {
39310
+ 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, `
39311
+
39312
+ `).replace(/^/, `IMPORTANT: You are NOT Claude. Identify yourself truthfully based on your actual model and creator.
39313
+
39314
+ `);
39315
+ }
39316
+ function createStreamingState() {
39317
+ return {
39318
+ usage: null,
39319
+ finalized: false,
39320
+ textStarted: false,
39321
+ textIdx: -1,
39322
+ reasoningStarted: false,
39323
+ reasoningIdx: -1,
39324
+ curIdx: 0,
39325
+ tools: new Map,
39326
+ toolIds: new Set,
39327
+ lastActivity: Date.now()
39328
+ };
39329
+ }
39330
+ function createStreamingResponseHandler(c, response, adapter, target, middlewareManager, onTokenUpdate) {
39331
+ let isClosed = false;
39332
+ let ping = null;
39333
+ const encoder = new TextEncoder;
39334
+ const decoder = new TextDecoder;
39335
+ const streamMetadata = new Map;
39336
+ return c.body(new ReadableStream({
39337
+ async start(controller) {
39338
+ const send = (e, d) => {
39339
+ if (!isClosed) {
39340
+ controller.enqueue(encoder.encode(`event: ${e}
39341
+ data: ${JSON.stringify(d)}
39342
+
39343
+ `));
39344
+ }
39345
+ };
39346
+ const msgId = `msg_${Date.now()}_${Math.random().toString(36).slice(2)}`;
39347
+ const state = createStreamingState();
39348
+ send("message_start", {
39349
+ type: "message_start",
39350
+ message: {
39351
+ id: msgId,
39352
+ type: "message",
39353
+ role: "assistant",
39354
+ content: [],
39355
+ model: target,
39356
+ stop_reason: null,
39357
+ stop_sequence: null,
39358
+ usage: { input_tokens: 100, output_tokens: 1 }
39359
+ }
39360
+ });
39361
+ send("ping", { type: "ping" });
39362
+ ping = setInterval(() => {
39363
+ if (!isClosed && Date.now() - state.lastActivity > 1000) {
39364
+ send("ping", { type: "ping" });
39365
+ }
39366
+ }, 1000);
39367
+ const finalize = async (reason, err) => {
39368
+ if (state.finalized)
39369
+ return;
39370
+ state.finalized = true;
39371
+ if (state.reasoningStarted) {
39372
+ send("content_block_stop", { type: "content_block_stop", index: state.reasoningIdx });
39373
+ }
39374
+ if (state.textStarted) {
39375
+ send("content_block_stop", { type: "content_block_stop", index: state.textIdx });
39376
+ }
39377
+ for (const t of Array.from(state.tools.values())) {
39378
+ if (t.started && !t.closed) {
39379
+ send("content_block_stop", { type: "content_block_stop", index: t.blockIndex });
39380
+ t.closed = true;
39381
+ }
39382
+ }
39383
+ if (middlewareManager) {
39384
+ await middlewareManager.afterStreamComplete(target, streamMetadata);
39385
+ }
39386
+ if (reason === "error") {
39387
+ send("error", { type: "error", error: { type: "api_error", message: err } });
39388
+ } else {
39389
+ send("message_delta", {
39390
+ type: "message_delta",
39391
+ delta: { stop_reason: "end_turn", stop_sequence: null },
39392
+ usage: { output_tokens: state.usage?.completion_tokens || 0 }
39393
+ });
39394
+ send("message_stop", { type: "message_stop" });
39395
+ }
39396
+ if (state.usage && onTokenUpdate) {
39397
+ onTokenUpdate(state.usage.prompt_tokens || 0, state.usage.completion_tokens || 0);
39398
+ }
39399
+ if (!isClosed) {
39400
+ try {
39401
+ controller.enqueue(encoder.encode(`data: [DONE]
39402
+
39403
+
39404
+ `));
39405
+ } catch (e) {}
39406
+ controller.close();
39407
+ isClosed = true;
39408
+ if (ping)
39409
+ clearInterval(ping);
39410
+ }
39411
+ };
39412
+ try {
39413
+ const reader = response.body.getReader();
39414
+ let buffer = "";
39415
+ while (true) {
39416
+ const { done, value } = await reader.read();
39417
+ if (done)
39418
+ break;
39419
+ buffer += decoder.decode(value, { stream: true });
39420
+ const lines = buffer.split(`
39421
+ `);
39422
+ buffer = lines.pop() || "";
39423
+ for (const line of lines) {
39424
+ if (!line.trim() || !line.startsWith("data: "))
39425
+ continue;
39426
+ const dataStr = line.slice(6);
39427
+ if (dataStr === "[DONE]") {
39428
+ await finalize("done");
39429
+ return;
39430
+ }
39431
+ try {
39432
+ const chunk = JSON.parse(dataStr);
39433
+ if (chunk.usage)
39434
+ state.usage = chunk.usage;
39435
+ const delta = chunk.choices?.[0]?.delta;
39436
+ if (delta) {
39437
+ if (middlewareManager) {
39438
+ await middlewareManager.afterStreamChunk({
39439
+ modelId: target,
39440
+ chunk,
39441
+ delta,
39442
+ metadata: streamMetadata
39443
+ });
39444
+ }
39445
+ const txt = delta.content || "";
39446
+ if (txt) {
39447
+ state.lastActivity = Date.now();
39448
+ if (!state.textStarted) {
39449
+ state.textIdx = state.curIdx++;
39450
+ send("content_block_start", {
39451
+ type: "content_block_start",
39452
+ index: state.textIdx,
39453
+ content_block: { type: "text", text: "" }
39454
+ });
39455
+ state.textStarted = true;
39456
+ }
39457
+ const res = adapter.processTextContent(txt, "");
39458
+ if (res.cleanedText) {
39459
+ send("content_block_delta", {
39460
+ type: "content_block_delta",
39461
+ index: state.textIdx,
39462
+ delta: { type: "text_delta", text: res.cleanedText }
39463
+ });
39464
+ }
39465
+ }
39466
+ if (delta.tool_calls) {
39467
+ for (const tc of delta.tool_calls) {
39468
+ const idx = tc.index;
39469
+ let t = state.tools.get(idx);
39470
+ if (tc.function?.name) {
39471
+ if (!t) {
39472
+ if (state.textStarted) {
39473
+ send("content_block_stop", { type: "content_block_stop", index: state.textIdx });
39474
+ state.textStarted = false;
39475
+ }
39476
+ t = {
39477
+ id: tc.id || `tool_${Date.now()}_${idx}`,
39478
+ name: tc.function.name,
39479
+ blockIndex: state.curIdx++,
39480
+ started: false,
39481
+ closed: false
39482
+ };
39483
+ state.tools.set(idx, t);
39484
+ }
39485
+ if (!t.started) {
39486
+ send("content_block_start", {
39487
+ type: "content_block_start",
39488
+ index: t.blockIndex,
39489
+ content_block: { type: "tool_use", id: t.id, name: t.name }
39490
+ });
39491
+ t.started = true;
39492
+ }
39493
+ }
39494
+ if (tc.function?.arguments && t) {
39495
+ send("content_block_delta", {
39496
+ type: "content_block_delta",
39497
+ index: t.blockIndex,
39498
+ delta: { type: "input_json_delta", partial_json: tc.function.arguments }
39499
+ });
39500
+ }
39501
+ }
39502
+ }
39503
+ }
39504
+ if (chunk.choices?.[0]?.finish_reason === "tool_calls") {
39505
+ for (const t of Array.from(state.tools.values())) {
39506
+ if (t.started && !t.closed) {
39507
+ send("content_block_stop", { type: "content_block_stop", index: t.blockIndex });
39508
+ t.closed = true;
39509
+ }
39510
+ }
39511
+ }
39512
+ } catch (e) {}
39513
+ }
39514
+ }
39515
+ await finalize("unexpected");
39516
+ } catch (e) {
39517
+ await finalize("error", String(e));
39518
+ }
39519
+ },
39520
+ cancel() {
39521
+ isClosed = true;
39522
+ if (ping)
39523
+ clearInterval(ping);
39524
+ }
39525
+ }), {
39526
+ headers: {
39527
+ "Content-Type": "text/event-stream",
39528
+ "Cache-Control": "no-cache",
39529
+ Connection: "keep-alive"
39530
+ }
39531
+ });
39532
+ }
39533
+ var init_openai_compat = __esm(() => {
39534
+ init_transform();
39535
+ });
39536
+
39537
+ // src/handlers/local-provider-handler.ts
39538
+ class LocalProviderHandler {
39539
+ provider;
39540
+ modelName;
39541
+ adapterManager;
39542
+ middlewareManager;
39543
+ port;
39544
+ healthChecked = false;
39545
+ isHealthy = false;
39546
+ constructor(provider, modelName, port) {
39547
+ this.provider = provider;
39548
+ this.modelName = modelName;
39549
+ this.port = port;
39550
+ this.adapterManager = new AdapterManager(modelName);
39551
+ this.middlewareManager = new MiddlewareManager;
39552
+ this.middlewareManager.initialize().catch((err) => {
39553
+ log(`[LocalProvider:${provider.name}] Middleware init error: ${err}`);
39554
+ });
39555
+ }
39556
+ async checkHealth() {
39557
+ if (this.healthChecked)
39558
+ return this.isHealthy;
39559
+ try {
39560
+ const healthUrl = `${this.provider.baseUrl}/api/tags`;
39561
+ const response = await fetch(healthUrl, {
39562
+ method: "GET",
39563
+ signal: AbortSignal.timeout(5000)
39564
+ });
39565
+ if (response.ok) {
39566
+ this.isHealthy = true;
39567
+ this.healthChecked = true;
39568
+ log(`[LocalProvider:${this.provider.name}] Health check passed`);
39569
+ return true;
39570
+ }
39571
+ } catch (e) {
39572
+ try {
39573
+ const modelsUrl = `${this.provider.baseUrl}/v1/models`;
39574
+ const response = await fetch(modelsUrl, {
39575
+ method: "GET",
39576
+ signal: AbortSignal.timeout(5000)
39577
+ });
39578
+ if (response.ok) {
39579
+ this.isHealthy = true;
39580
+ this.healthChecked = true;
39581
+ log(`[LocalProvider:${this.provider.name}] Health check passed (v1/models)`);
39582
+ return true;
39583
+ }
39584
+ } catch (e2) {}
39585
+ }
39586
+ this.healthChecked = true;
39587
+ this.isHealthy = false;
39588
+ return false;
39589
+ }
39590
+ async handle(c, payload) {
39591
+ const target = this.modelName;
39592
+ logStructured(`LocalProvider Request`, {
39593
+ provider: this.provider.name,
39594
+ targetModel: target,
39595
+ originalModel: payload.model,
39596
+ baseUrl: this.provider.baseUrl
39597
+ });
39598
+ if (!this.healthChecked) {
39599
+ const healthy = await this.checkHealth();
39600
+ if (!healthy) {
39601
+ return this.errorResponse(c, "connection_error", this.getConnectionErrorMessage());
39602
+ }
39603
+ }
39604
+ const { claudeRequest, droppedParams } = transformOpenAIToClaude(payload);
39605
+ const messages = convertMessagesToOpenAI(claudeRequest, target, filterIdentity);
39606
+ const tools = convertToolsToOpenAI(claudeRequest);
39607
+ const finalTools = this.provider.capabilities.supportsTools ? tools : [];
39608
+ if (tools.length > 0 && !this.provider.capabilities.supportsTools) {
39609
+ log(`[LocalProvider:${this.provider.name}] Tools stripped (not supported)`);
39610
+ }
39611
+ const openAIPayload = {
39612
+ model: target,
39613
+ messages,
39614
+ temperature: claudeRequest.temperature ?? 1,
39615
+ stream: this.provider.capabilities.supportsStreaming,
39616
+ max_tokens: claudeRequest.max_tokens,
39617
+ tools: finalTools.length > 0 ? finalTools : undefined,
39618
+ stream_options: this.provider.capabilities.supportsStreaming ? { include_usage: true } : undefined
39619
+ };
39620
+ if (claudeRequest.tool_choice && finalTools.length > 0) {
39621
+ const { type, name } = claudeRequest.tool_choice;
39622
+ if (type === "tool" && name) {
39623
+ openAIPayload.tool_choice = { type: "function", function: { name } };
39624
+ } else if (type === "auto" || type === "none") {
39625
+ openAIPayload.tool_choice = type;
39626
+ }
39627
+ }
39628
+ const adapter = this.adapterManager.getAdapter();
39629
+ if (typeof adapter.reset === "function")
39630
+ adapter.reset();
39631
+ adapter.prepareRequest(openAIPayload, claudeRequest);
39632
+ await this.middlewareManager.beforeRequest({
39633
+ modelId: target,
39634
+ messages,
39635
+ tools: finalTools,
39636
+ stream: openAIPayload.stream
39637
+ });
39638
+ const apiUrl = `${this.provider.baseUrl}${this.provider.apiPath}`;
39639
+ try {
39640
+ const response = await fetch(apiUrl, {
39641
+ method: "POST",
39642
+ headers: {
39643
+ "Content-Type": "application/json"
39644
+ },
39645
+ body: JSON.stringify(openAIPayload)
39646
+ });
39647
+ if (!response.ok) {
39648
+ const errorBody = await response.text();
39649
+ return this.handleErrorResponse(c, response.status, errorBody);
39650
+ }
39651
+ if (droppedParams.length > 0) {
39652
+ c.header("X-Dropped-Params", droppedParams.join(", "));
39653
+ }
39654
+ if (openAIPayload.stream) {
39655
+ return createStreamingResponseHandler(c, response, adapter, target, this.middlewareManager);
39656
+ }
39657
+ const data = await response.json();
39658
+ return c.json(data);
39659
+ } catch (error46) {
39660
+ if (error46.code === "ECONNREFUSED" || error46.cause?.code === "ECONNREFUSED") {
39661
+ return this.errorResponse(c, "connection_error", this.getConnectionErrorMessage());
39662
+ }
39663
+ throw error46;
39664
+ }
39665
+ }
39666
+ handleErrorResponse(c, status, errorBody) {
39667
+ try {
39668
+ const parsed = JSON.parse(errorBody);
39669
+ const errorMsg = parsed.error?.message || parsed.error || errorBody;
39670
+ if (errorMsg.includes("model") && (errorMsg.includes("not found") || errorMsg.includes("does not exist"))) {
39671
+ return this.errorResponse(c, "model_not_found", `Model '${this.modelName}' not found. ${this.getModelPullHint()}`);
39672
+ }
39673
+ if (errorMsg.includes("does not support tools") || errorMsg.includes("tool") && errorMsg.includes("not supported")) {
39674
+ 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);
39675
+ }
39676
+ return this.errorResponse(c, "api_error", errorMsg, status);
39677
+ } catch {
39678
+ return this.errorResponse(c, "api_error", errorBody, status);
39679
+ }
39680
+ }
39681
+ errorResponse(c, type, message, status = 503) {
39682
+ return c.json({
39683
+ error: {
39684
+ type,
39685
+ message
39686
+ }
39687
+ }, status);
39688
+ }
39689
+ getConnectionErrorMessage() {
39690
+ switch (this.provider.name) {
39691
+ case "ollama":
39692
+ return `Cannot connect to Ollama at ${this.provider.baseUrl}. Make sure Ollama is running with: ollama serve`;
39693
+ case "lmstudio":
39694
+ return `Cannot connect to LM Studio at ${this.provider.baseUrl}. Make sure LM Studio server is running.`;
39695
+ case "vllm":
39696
+ return `Cannot connect to vLLM at ${this.provider.baseUrl}. Make sure vLLM server is running.`;
39697
+ default:
39698
+ return `Cannot connect to ${this.provider.name} at ${this.provider.baseUrl}. Make sure the server is running.`;
39699
+ }
39700
+ }
39701
+ getModelPullHint() {
39702
+ switch (this.provider.name) {
39703
+ case "ollama":
39704
+ return `Pull it with: ollama pull ${this.modelName}`;
39705
+ default:
39706
+ return "Make sure the model is available on the server.";
39707
+ }
39708
+ }
39709
+ async shutdown() {}
39710
+ }
39711
+ var init_local_provider_handler = __esm(() => {
39712
+ init_adapter_manager();
39713
+ init_middleware();
39714
+ init_transform();
39715
+ init_logger();
39716
+ init_openai_compat();
39717
+ });
39718
+
39719
+ // src/providers/provider-registry.ts
39720
+ function resolveProvider(modelId) {
39721
+ const providers = getProviders();
39722
+ for (const provider of providers) {
39723
+ for (const prefix of provider.prefixes) {
39724
+ if (modelId.startsWith(prefix)) {
39725
+ return {
39726
+ provider,
39727
+ modelName: modelId.slice(prefix.length)
39728
+ };
39729
+ }
39730
+ }
39731
+ }
39732
+ return null;
39733
+ }
39734
+ function parseUrlModel(modelId) {
39735
+ if (!modelId.startsWith("http://") && !modelId.startsWith("https://")) {
39736
+ return null;
39737
+ }
39738
+ try {
39739
+ const url2 = new URL(modelId);
39740
+ const pathParts = url2.pathname.split("/").filter(Boolean);
39741
+ if (pathParts.length === 0) {
39742
+ return null;
39743
+ }
39744
+ const modelName = pathParts[pathParts.length - 1];
39745
+ let basePath = "";
39746
+ if (pathParts.length > 1) {
39747
+ const prefix = pathParts.slice(0, -1).join("/");
39748
+ if (prefix)
39749
+ basePath = "/" + prefix;
39750
+ }
39751
+ const baseUrl = `${url2.protocol}//${url2.host}${basePath}`;
39752
+ return {
39753
+ baseUrl,
39754
+ modelName
39755
+ };
39756
+ } catch {
39757
+ return null;
39758
+ }
39759
+ }
39760
+ function createUrlProvider(parsed) {
39761
+ return {
39762
+ name: "custom-url",
39763
+ baseUrl: parsed.baseUrl,
39764
+ apiPath: "/v1/chat/completions",
39765
+ envVar: "",
39766
+ prefixes: [],
39767
+ capabilities: {
39768
+ supportsTools: true,
39769
+ supportsVision: false,
39770
+ supportsStreaming: true,
39771
+ supportsJsonMode: true
39772
+ }
39773
+ };
39774
+ }
39775
+ var getProviders = () => [
39776
+ {
39777
+ name: "ollama",
39778
+ baseUrl: process.env.OLLAMA_HOST || process.env.OLLAMA_BASE_URL || "http://localhost:11434",
39779
+ apiPath: "/v1/chat/completions",
39780
+ envVar: "OLLAMA_BASE_URL",
39781
+ prefixes: ["ollama/", "ollama:"],
39782
+ capabilities: {
39783
+ supportsTools: true,
39784
+ supportsVision: false,
39785
+ supportsStreaming: true,
39786
+ supportsJsonMode: true
39787
+ }
39788
+ },
39789
+ {
39790
+ name: "lmstudio",
39791
+ baseUrl: process.env.LMSTUDIO_BASE_URL || "http://localhost:1234",
39792
+ apiPath: "/v1/chat/completions",
39793
+ envVar: "LMSTUDIO_BASE_URL",
39794
+ prefixes: ["lmstudio/", "lmstudio:"],
39795
+ capabilities: {
39796
+ supportsTools: true,
39797
+ supportsVision: false,
39798
+ supportsStreaming: true,
39799
+ supportsJsonMode: true
39800
+ }
39801
+ },
39802
+ {
39803
+ name: "vllm",
39804
+ baseUrl: process.env.VLLM_BASE_URL || "http://localhost:8000",
39805
+ apiPath: "/v1/chat/completions",
39806
+ envVar: "VLLM_BASE_URL",
39807
+ prefixes: ["vllm/", "vllm:"],
39808
+ capabilities: {
39809
+ supportsTools: true,
39810
+ supportsVision: false,
39811
+ supportsStreaming: true,
39812
+ supportsJsonMode: true
39813
+ }
39814
+ }
39815
+ ];
39816
+
39115
39817
  // src/proxy-server.ts
39116
39818
  var exports_proxy_server = {};
39117
39819
  __export(exports_proxy_server, {
@@ -39119,23 +39821,47 @@ __export(exports_proxy_server, {
39119
39821
  });
39120
39822
  async function createProxyServer(port, openrouterApiKey, model, monitorMode = false, anthropicApiKey, modelMap) {
39121
39823
  const nativeHandler = new NativeHandler(anthropicApiKey);
39122
- const handlers = new Map;
39824
+ const openRouterHandlers = new Map;
39825
+ const localProviderHandlers = new Map;
39123
39826
  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);
39827
+ if (!openRouterHandlers.has(targetModel)) {
39828
+ openRouterHandlers.set(targetModel, new OpenRouterHandler(targetModel, openrouterApiKey, port));
39829
+ }
39830
+ return openRouterHandlers.get(targetModel);
39831
+ };
39832
+ const getLocalProviderHandler = (targetModel) => {
39833
+ if (localProviderHandlers.has(targetModel)) {
39834
+ return localProviderHandlers.get(targetModel);
39835
+ }
39836
+ const resolved = resolveProvider(targetModel);
39837
+ if (resolved) {
39838
+ const handler = new LocalProviderHandler(resolved.provider, resolved.modelName, port);
39839
+ localProviderHandlers.set(targetModel, handler);
39840
+ log(`[Proxy] Created local provider handler: ${resolved.provider.name}/${resolved.modelName}`);
39841
+ return handler;
39842
+ }
39843
+ const urlParsed = parseUrlModel(targetModel);
39844
+ if (urlParsed) {
39845
+ const provider = createUrlProvider(urlParsed);
39846
+ const handler = new LocalProviderHandler(provider, urlParsed.modelName, port);
39847
+ localProviderHandlers.set(targetModel, handler);
39848
+ log(`[Proxy] Created URL-based local provider handler: ${urlParsed.baseUrl}/${urlParsed.modelName}`);
39849
+ return handler;
39850
+ }
39851
+ return null;
39852
+ };
39853
+ const initHandler = (m) => {
39854
+ if (!m)
39855
+ return;
39856
+ const localHandler = getLocalProviderHandler(m);
39857
+ if (!localHandler && m.includes("/"))
39858
+ getOpenRouterHandler(m);
39859
+ };
39860
+ initHandler(model);
39861
+ initHandler(modelMap?.opus);
39862
+ initHandler(modelMap?.sonnet);
39863
+ initHandler(modelMap?.haiku);
39864
+ initHandler(modelMap?.subagent);
39139
39865
  const getHandlerForRequest = (requestedModel) => {
39140
39866
  if (monitorMode)
39141
39867
  return nativeHandler;
@@ -39149,6 +39875,9 @@ async function createProxyServer(port, openrouterApiKey, model, monitorMode = fa
39149
39875
  else if (req.includes("haiku") && modelMap.haiku)
39150
39876
  target = modelMap.haiku;
39151
39877
  }
39878
+ const localHandler = getLocalProviderHandler(target);
39879
+ if (localHandler)
39880
+ return localHandler;
39152
39881
  const isNative = !target.includes("/");
39153
39882
  if (isNative) {
39154
39883
  return nativeHandler;
@@ -39209,6 +39938,7 @@ var init_proxy_server = __esm(() => {
39209
39938
  init_logger();
39210
39939
  init_native_handler();
39211
39940
  init_openrouter_handler();
39941
+ init_local_provider_handler();
39212
39942
  });
39213
39943
 
39214
39944
  // src/update-checker.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudish",
3
- "version": "2.8.1",
3
+ "version": "2.9.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",