claudish 2.10.0 → 2.11.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 (2) hide show
  1. package/dist/index.js +1075 -537
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -34308,7 +34308,8 @@ var init_config = __esm(() => {
34308
34308
  OLLAMA_BASE_URL: "OLLAMA_BASE_URL",
34309
34309
  OLLAMA_HOST: "OLLAMA_HOST",
34310
34310
  LMSTUDIO_BASE_URL: "LMSTUDIO_BASE_URL",
34311
- VLLM_BASE_URL: "VLLM_BASE_URL"
34311
+ VLLM_BASE_URL: "VLLM_BASE_URL",
34312
+ CLAUDISH_SUMMARIZE_TOOLS: "CLAUDISH_SUMMARIZE_TOOLS"
34312
34313
  };
34313
34314
  OPENROUTER_HEADERS = {
34314
34315
  "HTTP-Referer": "https://github.com/MadAppGang/claude-code",
@@ -34324,8 +34325,11 @@ __export(exports_claude_runner, {
34324
34325
  });
34325
34326
  import { spawn } from "node:child_process";
34326
34327
  import { writeFileSync as writeFileSync4, unlinkSync } from "node:fs";
34327
- import { tmpdir, platform } from "node:os";
34328
+ import { tmpdir } from "node:os";
34328
34329
  import { join as join4 } from "node:path";
34330
+ function isWindows() {
34331
+ return process.platform === "win32";
34332
+ }
34329
34333
  function createStatusLineScript(tokenFilePath) {
34330
34334
  const tempDir = tmpdir();
34331
34335
  const timestamp = Date.now();
@@ -34382,7 +34386,7 @@ function createTempSettingsFile(modelDisplay, port) {
34382
34386
  const tempPath = join4(tempDir, `claudish-settings-${timestamp}.json`);
34383
34387
  const tokenFilePath = join4(tempDir, `claudish-tokens-${port}.json`);
34384
34388
  let statusCommand;
34385
- if (isWindows) {
34389
+ if (isWindows()) {
34386
34390
  const scriptPath = createStatusLineScript(tokenFilePath);
34387
34391
  statusCommand = `node "${scriptPath}"`;
34388
34392
  } else {
@@ -34471,7 +34475,7 @@ async function runClaudeWithProxy(config3, proxyUrl) {
34471
34475
  const proc = spawn("claude", claudeArgs, {
34472
34476
  env,
34473
34477
  stdio: "inherit",
34474
- shell: isWindows
34478
+ shell: isWindows()
34475
34479
  });
34476
34480
  setupSignalHandlers(proc, tempSettingsPath, config3.quiet);
34477
34481
  const exitCode = await new Promise((resolve) => {
@@ -34485,7 +34489,7 @@ async function runClaudeWithProxy(config3, proxyUrl) {
34485
34489
  return exitCode;
34486
34490
  }
34487
34491
  function setupSignalHandlers(proc, tempSettingsPath, quiet) {
34488
- const signals2 = isWindows ? ["SIGINT", "SIGTERM"] : ["SIGINT", "SIGTERM", "SIGHUP"];
34492
+ const signals2 = isWindows() ? ["SIGINT", "SIGTERM"] : ["SIGINT", "SIGTERM", "SIGHUP"];
34489
34493
  for (const signal of signals2) {
34490
34494
  process.on(signal, () => {
34491
34495
  if (!quiet) {
@@ -34518,10 +34522,8 @@ async function checkClaudeInstalled() {
34518
34522
  return false;
34519
34523
  }
34520
34524
  }
34521
- var isWindows;
34522
34525
  var init_claude_runner = __esm(() => {
34523
34526
  init_config();
34524
- isWindows = platform() === "win32";
34525
34527
  });
34526
34528
 
34527
34529
  // src/types.ts
@@ -34742,6 +34744,10 @@ async function parseArgs(args) {
34742
34744
  config3.port = port;
34743
34745
  }
34744
34746
  }
34747
+ const envSummarizeTools = process.env[ENV.CLAUDISH_SUMMARIZE_TOOLS];
34748
+ if (envSummarizeTools === "true" || envSummarizeTools === "1") {
34749
+ config3.summarizeTools = true;
34750
+ }
34745
34751
  let i = 0;
34746
34752
  while (i < args.length) {
34747
34753
  const arg = args[i];
@@ -34858,6 +34864,8 @@ async function parseArgs(args) {
34858
34864
  await printAllModels(hasJsonFlag, forceUpdate);
34859
34865
  }
34860
34866
  process.exit(0);
34867
+ } else if (arg === "--summarize-tools") {
34868
+ config3.summarizeTools = true;
34861
34869
  } else {
34862
34870
  config3.claudeArgs = args.slice(i);
34863
34871
  break;
@@ -38866,408 +38874,238 @@ function transformOpenAIToClaude(claudeRequestInput) {
38866
38874
  }
38867
38875
  var init_transform = () => {};
38868
38876
 
38869
- // src/handlers/openrouter-handler.ts
38870
- import { writeFileSync as writeFileSync8 } from "node:fs";
38871
- import { tmpdir as tmpdir2 } from "node:os";
38872
- import { join as join8 } from "node:path";
38873
-
38874
- class OpenRouterHandler {
38875
- targetModel;
38876
- apiKey;
38877
- adapterManager;
38878
- middlewareManager;
38879
- contextWindowCache = new Map;
38880
- port;
38881
- sessionTotalCost = 0;
38882
- CLAUDE_INTERNAL_CONTEXT_MAX = 200000;
38883
- constructor(targetModel, apiKey, port) {
38884
- this.targetModel = targetModel;
38885
- this.apiKey = apiKey;
38886
- this.port = port;
38887
- this.adapterManager = new AdapterManager(targetModel);
38888
- this.middlewareManager = new MiddlewareManager;
38889
- this.middlewareManager.register(new GeminiThoughtSignatureMiddleware);
38890
- this.middlewareManager.initialize().catch((err) => log(`[Handler:${targetModel}] Middleware init error: ${err}`));
38891
- this.fetchContextWindow(targetModel);
38877
+ // src/handlers/shared/tool-call-recovery.ts
38878
+ function extractToolCallsFromText(text) {
38879
+ const extracted = [];
38880
+ const qwenPattern = /<function=([^>]+)>([\s\S]*?)(?=<function=|$)/gi;
38881
+ let match2;
38882
+ while ((match2 = qwenPattern.exec(text)) !== null) {
38883
+ const funcName = match2[1];
38884
+ const paramsText = match2[2];
38885
+ const args = {};
38886
+ const paramPattern = /<parameter=([^>]+)>\s*([\s\S]*?)(?=<parameter=|<function=|$)/gi;
38887
+ let paramMatch;
38888
+ while ((paramMatch = paramPattern.exec(paramsText)) !== null) {
38889
+ const paramName = paramMatch[1];
38890
+ const paramValue = paramMatch[2].trim();
38891
+ args[paramName] = paramValue;
38892
+ }
38893
+ if (funcName) {
38894
+ extracted.push({
38895
+ name: funcName,
38896
+ arguments: args,
38897
+ source: "xml_text"
38898
+ });
38899
+ log(`[ToolRecovery] Extracted Qwen-style tool call: ${funcName}`);
38900
+ }
38892
38901
  }
38893
- async fetchContextWindow(model) {
38894
- if (this.contextWindowCache.has(model))
38895
- return;
38902
+ const xmlPattern = /<tool_call>\s*(\{[\s\S]*?\})\s*<\/tool_call>/gi;
38903
+ while ((match2 = xmlPattern.exec(text)) !== null) {
38896
38904
  try {
38897
- const limit = await fetchModelContextWindow(model);
38898
- this.contextWindowCache.set(model, limit);
38905
+ const parsed = JSON.parse(match2[1]);
38906
+ if (parsed.name) {
38907
+ extracted.push({
38908
+ name: parsed.name,
38909
+ arguments: parsed.arguments || parsed.input || parsed.parameters || {},
38910
+ source: "xml_text"
38911
+ });
38912
+ }
38899
38913
  } catch (e) {}
38900
38914
  }
38901
- getTokenScaleFactor(model) {
38902
- const limit = this.contextWindowCache.get(model) || 200000;
38903
- return limit === 0 ? 1 : this.CLAUDE_INTERNAL_CONTEXT_MAX / limit;
38915
+ const funcCallPattern = /\{\s*"name"\s*:\s*"([^"]+)"\s*,\s*"(?:arguments|input|parameters)"\s*:\s*(\{[\s\S]*?\})\s*\}/gi;
38916
+ while ((match2 = funcCallPattern.exec(text)) !== null) {
38917
+ try {
38918
+ const args = JSON.parse(match2[2]);
38919
+ extracted.push({
38920
+ name: match2[1],
38921
+ arguments: args,
38922
+ source: "json_text"
38923
+ });
38924
+ } catch (e) {}
38904
38925
  }
38905
- writeTokenFile(input, output) {
38926
+ const anthropicPattern = /\{\s*"type"\s*:\s*"tool_use"\s*,\s*"id"\s*:\s*"[^"]*"\s*,\s*"name"\s*:\s*"([^"]+)"\s*,\s*"input"\s*:\s*(\{[\s\S]*?\})\s*\}/gi;
38927
+ while ((match2 = anthropicPattern.exec(text)) !== null) {
38906
38928
  try {
38907
- const total = input + output;
38908
- const limit = this.contextWindowCache.get(this.targetModel) || 200000;
38909
- const leftPct = limit > 0 ? Math.max(0, Math.min(100, Math.round((limit - total) / limit * 100))) : 100;
38910
- const data = {
38911
- input_tokens: input,
38912
- output_tokens: output,
38913
- total_tokens: total,
38914
- total_cost: this.sessionTotalCost,
38915
- context_window: limit,
38916
- context_left_percent: leftPct,
38917
- updated_at: Date.now()
38918
- };
38919
- writeFileSync8(join8(tmpdir2(), `claudish-tokens-${this.port}.json`), JSON.stringify(data), "utf-8");
38929
+ const args = JSON.parse(match2[2]);
38930
+ extracted.push({
38931
+ name: match2[1],
38932
+ arguments: args,
38933
+ source: "json_text"
38934
+ });
38920
38935
  } catch (e) {}
38921
38936
  }
38922
- async handle(c, payload) {
38923
- const claudePayload = payload;
38924
- const target = this.targetModel;
38925
- await this.fetchContextWindow(target);
38926
- logStructured(`OpenRouter Request`, { targetModel: target, originalModel: claudePayload.model });
38927
- const { claudeRequest, droppedParams } = transformOpenAIToClaude(claudePayload);
38928
- const messages = this.convertMessages(claudeRequest, target);
38929
- const tools = this.convertTools(claudeRequest);
38930
- const supportsReasoning = await doesModelSupportReasoning(target);
38931
- const openRouterPayload = {
38932
- model: target,
38933
- messages,
38934
- temperature: claudeRequest.temperature ?? 1,
38935
- stream: true,
38936
- max_tokens: claudeRequest.max_tokens,
38937
- tools: tools.length > 0 ? tools : undefined,
38938
- stream_options: { include_usage: true }
38939
- };
38940
- if (supportsReasoning)
38941
- openRouterPayload.include_reasoning = true;
38942
- if (claudeRequest.thinking)
38943
- openRouterPayload.thinking = claudeRequest.thinking;
38944
- if (claudeRequest.tool_choice) {
38945
- const { type, name } = claudeRequest.tool_choice;
38946
- if (type === "tool" && name)
38947
- openRouterPayload.tool_choice = { type: "function", function: { name } };
38948
- else if (type === "auto" || type === "none")
38949
- openRouterPayload.tool_choice = type;
38950
- }
38951
- const adapter = this.adapterManager.getAdapter();
38952
- if (typeof adapter.reset === "function")
38953
- adapter.reset();
38954
- adapter.prepareRequest(openRouterPayload, claudeRequest);
38955
- await this.middlewareManager.beforeRequest({ modelId: target, messages, tools, stream: true });
38956
- const response = await fetch(OPENROUTER_API_URL2, {
38957
- method: "POST",
38958
- headers: {
38959
- "Content-Type": "application/json",
38960
- Authorization: `Bearer ${this.apiKey}`,
38961
- ...OPENROUTER_HEADERS2
38962
- },
38963
- body: JSON.stringify(openRouterPayload)
38964
- });
38965
- if (!response.ok)
38966
- return c.json({ error: await response.text() }, response.status);
38967
- if (droppedParams.length > 0)
38968
- c.header("X-Dropped-Params", droppedParams.join(", "));
38969
- return this.handleStreamingResponse(c, response, adapter, target, claudeRequest);
38937
+ const jsonBlockPattern = /```(?:json)?\s*(\{[\s\S]*?\})\s*```/gi;
38938
+ while ((match2 = jsonBlockPattern.exec(text)) !== null) {
38939
+ try {
38940
+ const parsed = JSON.parse(match2[1]);
38941
+ if (parsed.name && (parsed.arguments || parsed.input || parsed.parameters)) {
38942
+ extracted.push({
38943
+ name: parsed.name,
38944
+ arguments: parsed.arguments || parsed.input || parsed.parameters,
38945
+ source: "json_text"
38946
+ });
38947
+ }
38948
+ } catch (e) {}
38970
38949
  }
38971
- convertMessages(req, modelId) {
38972
- const messages = [];
38973
- if (req.system) {
38974
- let content = Array.isArray(req.system) ? req.system.map((i) => i.text || i).join(`
38975
-
38976
- `) : req.system;
38977
- content = this.filterIdentity(content);
38978
- messages.push({ role: "system", content });
38950
+ return extracted;
38951
+ }
38952
+ function inferMissingParameters(toolName, args, missingParams, context) {
38953
+ const inferred = { ...args };
38954
+ if (toolName === "Task") {
38955
+ if (missingParams.includes("subagent_type") && !inferred.subagent_type) {
38956
+ inferred.subagent_type = "general-purpose";
38957
+ log(`[ToolRecovery] Inferred subagent_type: general-purpose`);
38958
+ }
38959
+ let extractedTask = "";
38960
+ if (context) {
38961
+ const patterns = [
38962
+ /(?:I(?:'ll| will| need to| want to| am going to)|Let me|Going to)\s+([^.!?\n]+)/i,
38963
+ /(?:help you|assist with)\s+([^.!?\n]+)/i,
38964
+ /(?:explore|search|find|look for|investigate)\s+([^.!?\n]+)/i,
38965
+ /(?:implement|create|build|add|fix|update)\s+([^.!?\n]+)/i
38966
+ ];
38967
+ for (const pattern of patterns) {
38968
+ const match2 = context.match(pattern);
38969
+ if (match2 && match2[1] && match2[1].length > 10) {
38970
+ extractedTask = match2[1].trim();
38971
+ log(`[ToolRecovery] Extracted task from context: "${extractedTask.substring(0, 50)}..."`);
38972
+ break;
38973
+ }
38974
+ }
38975
+ if (!extractedTask && context.length > 20) {
38976
+ const sentences = context.split(/[.!?\n]+/).filter((s) => s.trim().length > 15);
38977
+ if (sentences.length > 0) {
38978
+ extractedTask = sentences[sentences.length - 1].trim();
38979
+ }
38980
+ }
38979
38981
  }
38980
- if (modelId.includes("grok") || modelId.includes("x-ai")) {
38981
- const msg = "IMPORTANT: When calling tools, you MUST use the OpenAI tool_calls format with JSON. NEVER use XML format like <xai:function_call>.";
38982
- if (messages.length > 0 && messages[0].role === "system")
38983
- messages[0].content += `
38984
-
38985
- ` + msg;
38986
- else
38987
- messages.unshift({ role: "system", content: msg });
38982
+ if (missingParams.includes("prompt") && !inferred.prompt) {
38983
+ if (inferred.description && inferred.description !== "Execute task") {
38984
+ inferred.prompt = inferred.description;
38985
+ } else if (inferred.task) {
38986
+ inferred.prompt = inferred.task;
38987
+ } else if (extractedTask) {
38988
+ inferred.prompt = extractedTask;
38989
+ } else if (context && context.length > 20) {
38990
+ inferred.prompt = context.substring(0, 500).trim();
38991
+ }
38992
+ if (inferred.prompt) {
38993
+ log(`[ToolRecovery] Inferred prompt: "${inferred.prompt.substring(0, 50)}..."`);
38994
+ }
38988
38995
  }
38989
- if (req.messages) {
38990
- for (const msg of req.messages) {
38991
- if (msg.role === "user")
38992
- this.processUserMessage(msg, messages);
38993
- else if (msg.role === "assistant")
38994
- this.processAssistantMessage(msg, messages);
38996
+ if (missingParams.includes("description") && !inferred.description) {
38997
+ if (inferred.prompt) {
38998
+ inferred.description = inferred.prompt.substring(0, 50).replace(/\s+/g, " ").trim();
38999
+ if (inferred.description.length < inferred.prompt.length) {
39000
+ inferred.description += "...";
39001
+ }
39002
+ } else if (extractedTask) {
39003
+ inferred.description = extractedTask.substring(0, 50).trim();
39004
+ } else {
39005
+ inferred.description = "Execute task";
38995
39006
  }
39007
+ log(`[ToolRecovery] Inferred description: ${inferred.description}`);
38996
39008
  }
38997
- return messages;
38998
39009
  }
38999
- processUserMessage(msg, messages) {
39000
- if (Array.isArray(msg.content)) {
39001
- const contentParts = [];
39002
- const toolResults = [];
39003
- const seen = new Set;
39004
- for (const block of msg.content) {
39005
- if (block.type === "text")
39006
- contentParts.push({ type: "text", text: block.text });
39007
- else if (block.type === "image")
39008
- contentParts.push({ type: "image_url", image_url: { url: `data:${block.source.media_type};base64,${block.source.data}` } });
39009
- else if (block.type === "tool_result") {
39010
- if (seen.has(block.tool_use_id))
39011
- continue;
39012
- seen.add(block.tool_use_id);
39013
- toolResults.push({ role: "tool", content: typeof block.content === "string" ? block.content : JSON.stringify(block.content), tool_call_id: block.tool_use_id });
39014
- }
39010
+ if (toolName === "Bash") {
39011
+ if (missingParams.includes("command") && !inferred.command) {
39012
+ inferred.command = inferred.cmd || inferred.shell || inferred.script || "";
39013
+ }
39014
+ if (missingParams.includes("description") && !inferred.description) {
39015
+ if (inferred.command) {
39016
+ const cmd = inferred.command.split(" ")[0];
39017
+ inferred.description = `Run ${cmd} command`;
39015
39018
  }
39016
- if (toolResults.length)
39017
- messages.push(...toolResults);
39018
- if (contentParts.length)
39019
- messages.push({ role: "user", content: contentParts });
39020
- } else {
39021
- messages.push({ role: "user", content: msg.content });
39022
39019
  }
39023
39020
  }
39024
- processAssistantMessage(msg, messages) {
39025
- if (Array.isArray(msg.content)) {
39026
- const strings = [];
39027
- const toolCalls = [];
39028
- const seen = new Set;
39029
- for (const block of msg.content) {
39030
- if (block.type === "text")
39031
- strings.push(block.text);
39032
- else if (block.type === "tool_use") {
39033
- if (seen.has(block.id))
39034
- continue;
39035
- seen.add(block.id);
39036
- toolCalls.push({ id: block.id, type: "function", function: { name: block.name, arguments: JSON.stringify(block.input) } });
39037
- }
39021
+ if (toolName === "Read") {
39022
+ if (missingParams.includes("file_path") && !inferred.file_path) {
39023
+ inferred.file_path = inferred.path || inferred.file || inferred.filename || "";
39024
+ }
39025
+ }
39026
+ if (toolName === "Write") {
39027
+ if (missingParams.includes("file_path") && !inferred.file_path) {
39028
+ inferred.file_path = inferred.path || inferred.file || inferred.filename || "";
39029
+ }
39030
+ if (missingParams.includes("content") && !inferred.content) {
39031
+ inferred.content = inferred.text || inferred.data || inferred.body || "";
39032
+ }
39033
+ }
39034
+ if (toolName === "Grep") {
39035
+ if (missingParams.includes("pattern") && !inferred.pattern) {
39036
+ inferred.pattern = inferred.query || inferred.search || inferred.regex || "";
39037
+ }
39038
+ }
39039
+ if (toolName === "Glob") {
39040
+ if (missingParams.includes("pattern") && !inferred.pattern) {
39041
+ inferred.pattern = inferred.glob || inferred.path || inferred.search || "**/*";
39042
+ }
39043
+ }
39044
+ return inferred;
39045
+ }
39046
+ function validateAndRepairToolCall(toolName, argsStr, toolSchemas, textContent) {
39047
+ const schema = toolSchemas.find((t) => t.name === toolName);
39048
+ if (!schema?.input_schema) {
39049
+ return { valid: true, args: {}, repaired: false, missingParams: [] };
39050
+ }
39051
+ let parsedArgs = {};
39052
+ try {
39053
+ parsedArgs = argsStr ? JSON.parse(argsStr) : {};
39054
+ } catch (e) {
39055
+ if (textContent) {
39056
+ const extracted = extractToolCallsFromText(textContent);
39057
+ const matching = extracted.find((tc) => tc.name === toolName);
39058
+ if (matching) {
39059
+ parsedArgs = matching.arguments;
39060
+ log(`[ToolRecovery] Extracted tool args from text for ${toolName}`);
39038
39061
  }
39039
- const m = { role: "assistant" };
39040
- if (strings.length)
39041
- m.content = strings.join(" ");
39042
- else if (toolCalls.length)
39043
- m.content = null;
39044
- if (toolCalls.length)
39045
- m.tool_calls = toolCalls;
39046
- if (m.content !== undefined || m.tool_calls)
39047
- messages.push(m);
39048
- } else {
39049
- messages.push({ role: "assistant", content: msg.content });
39050
39062
  }
39051
39063
  }
39052
- filterIdentity(content) {
39053
- 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, `
39054
-
39055
- `).replace(/^/, `IMPORTANT: You are NOT Claude. Identify yourself truthfully based on your actual model and creator.
39064
+ const required2 = schema.input_schema.required || [];
39065
+ const missingParams = required2.filter((param) => parsedArgs[param] === undefined || parsedArgs[param] === null || parsedArgs[param] === "");
39066
+ if (missingParams.length === 0) {
39067
+ return { valid: true, args: parsedArgs, repaired: false, missingParams: [] };
39068
+ }
39069
+ const repairedArgs = inferMissingParameters(toolName, parsedArgs, missingParams, textContent);
39070
+ const stillMissing = required2.filter((param) => repairedArgs[param] === undefined || repairedArgs[param] === null || repairedArgs[param] === "");
39071
+ if (stillMissing.length === 0) {
39072
+ log(`[ToolRecovery] Successfully repaired tool call ${toolName}`);
39073
+ return { valid: true, args: repairedArgs, repaired: true, missingParams: [] };
39074
+ }
39075
+ return { valid: false, args: repairedArgs, repaired: false, missingParams: stillMissing };
39076
+ }
39077
+ var init_tool_call_recovery = __esm(() => {
39078
+ init_logger();
39079
+ });
39056
39080
 
39057
- `);
39081
+ // src/handlers/shared/openai-compat.ts
39082
+ function validateToolArguments(toolName, argsStr, toolSchemas, textContent) {
39083
+ const result = validateAndRepairToolCall(toolName, argsStr, toolSchemas, textContent);
39084
+ if (result.repaired) {
39085
+ log(`[ToolValidation] Repaired tool call ${toolName} - inferred missing parameters`);
39058
39086
  }
39059
- convertTools(req) {
39060
- return req.tools?.map((tool) => ({
39061
- type: "function",
39062
- function: {
39063
- name: tool.name,
39064
- description: tool.description,
39065
- parameters: removeUriFormat(tool.input_schema)
39066
- }
39067
- })) || [];
39087
+ return {
39088
+ valid: result.valid,
39089
+ missingParams: result.missingParams,
39090
+ parsedArgs: result.args,
39091
+ repaired: result.repaired,
39092
+ repairedArgs: result.repaired ? result.args : undefined
39093
+ };
39094
+ }
39095
+ function convertMessagesToOpenAI(req, modelId, filterIdentityFn) {
39096
+ const messages = [];
39097
+ if (req.system) {
39098
+ let content = Array.isArray(req.system) ? req.system.map((i) => i.text || i).join(`
39099
+
39100
+ `) : req.system;
39101
+ if (filterIdentityFn)
39102
+ content = filterIdentityFn(content);
39103
+ messages.push({ role: "system", content });
39068
39104
  }
39069
- handleStreamingResponse(c, response, adapter, target, request) {
39070
- let isClosed = false;
39071
- let ping = null;
39072
- const encoder = new TextEncoder;
39073
- const decoder = new TextDecoder;
39074
- const middlewareManager = this.middlewareManager;
39075
- const streamMetadata = new Map;
39076
- return c.body(new ReadableStream({
39077
- async start(controller) {
39078
- const send = (e, d) => {
39079
- if (!isClosed)
39080
- controller.enqueue(encoder.encode(`event: ${e}
39081
- data: ${JSON.stringify(d)}
39082
-
39083
- `));
39084
- };
39085
- const msgId = `msg_${Date.now()}_${Math.random().toString(36).slice(2)}`;
39086
- let usage = null;
39087
- let finalized = false;
39088
- let textStarted = false;
39089
- let textIdx = -1;
39090
- let reasoningStarted = false;
39091
- let reasoningIdx = -1;
39092
- let curIdx = 0;
39093
- const tools = new Map;
39094
- const toolIds = new Set;
39095
- let accTxt = 0;
39096
- let lastActivity = Date.now();
39097
- send("message_start", {
39098
- type: "message_start",
39099
- message: {
39100
- id: msgId,
39101
- type: "message",
39102
- role: "assistant",
39103
- content: [],
39104
- model: target,
39105
- stop_reason: null,
39106
- stop_sequence: null,
39107
- usage: { input_tokens: 100, output_tokens: 1 }
39108
- }
39109
- });
39110
- send("ping", { type: "ping" });
39111
- ping = setInterval(() => {
39112
- if (!isClosed && Date.now() - lastActivity > 1000)
39113
- send("ping", { type: "ping" });
39114
- }, 1000);
39115
- const finalize = async (reason, err) => {
39116
- if (finalized)
39117
- return;
39118
- finalized = true;
39119
- if (reasoningStarted) {
39120
- send("content_block_stop", { type: "content_block_stop", index: reasoningIdx });
39121
- reasoningStarted = false;
39122
- }
39123
- if (textStarted) {
39124
- send("content_block_stop", { type: "content_block_stop", index: textIdx });
39125
- textStarted = false;
39126
- }
39127
- for (const [_, t] of tools)
39128
- if (t.started && !t.closed) {
39129
- send("content_block_stop", { type: "content_block_stop", index: t.blockIndex });
39130
- t.closed = true;
39131
- }
39132
- await middlewareManager.afterStreamComplete(target, streamMetadata);
39133
- if (reason === "error") {
39134
- send("error", { type: "error", error: { type: "api_error", message: err } });
39135
- } else {
39136
- send("message_delta", { type: "message_delta", delta: { stop_reason: "end_turn", stop_sequence: null }, usage: { output_tokens: usage?.completion_tokens || 0 } });
39137
- send("message_stop", { type: "message_stop" });
39138
- }
39139
- if (!isClosed) {
39140
- try {
39141
- controller.enqueue(encoder.encode(`data: [DONE]
39142
-
39143
-
39144
- `));
39145
- } catch (e) {}
39146
- controller.close();
39147
- isClosed = true;
39148
- if (ping)
39149
- clearInterval(ping);
39150
- }
39151
- };
39152
- try {
39153
- const reader = response.body.getReader();
39154
- let buffer = "";
39155
- while (true) {
39156
- const { done, value } = await reader.read();
39157
- if (done)
39158
- break;
39159
- buffer += decoder.decode(value, { stream: true });
39160
- const lines = buffer.split(`
39161
- `);
39162
- buffer = lines.pop() || "";
39163
- for (const line of lines) {
39164
- if (!line.trim() || !line.startsWith("data: "))
39165
- continue;
39166
- const dataStr = line.slice(6);
39167
- if (dataStr === "[DONE]") {
39168
- await finalize("done");
39169
- return;
39170
- }
39171
- try {
39172
- const chunk = JSON.parse(dataStr);
39173
- if (chunk.usage)
39174
- usage = chunk.usage;
39175
- const delta = chunk.choices?.[0]?.delta;
39176
- if (delta) {
39177
- await middlewareManager.afterStreamChunk({
39178
- modelId: target,
39179
- chunk,
39180
- delta,
39181
- metadata: streamMetadata
39182
- });
39183
- const txt = delta.content || "";
39184
- if (txt) {
39185
- lastActivity = Date.now();
39186
- if (!textStarted) {
39187
- textIdx = curIdx++;
39188
- send("content_block_start", { type: "content_block_start", index: textIdx, content_block: { type: "text", text: "" } });
39189
- textStarted = true;
39190
- }
39191
- const res = adapter.processTextContent(txt, "");
39192
- if (res.cleanedText)
39193
- send("content_block_delta", { type: "content_block_delta", index: textIdx, delta: { type: "text_delta", text: res.cleanedText } });
39194
- }
39195
- if (delta.tool_calls) {
39196
- for (const tc of delta.tool_calls) {
39197
- const idx = tc.index;
39198
- let t = tools.get(idx);
39199
- if (tc.function?.name) {
39200
- if (!t) {
39201
- if (textStarted) {
39202
- send("content_block_stop", { type: "content_block_stop", index: textIdx });
39203
- textStarted = false;
39204
- }
39205
- t = { id: tc.id || `tool_${Date.now()}_${idx}`, name: tc.function.name, blockIndex: curIdx++, started: false, closed: false };
39206
- tools.set(idx, t);
39207
- }
39208
- if (!t.started) {
39209
- send("content_block_start", { type: "content_block_start", index: t.blockIndex, content_block: { type: "tool_use", id: t.id, name: t.name } });
39210
- t.started = true;
39211
- }
39212
- }
39213
- if (tc.function?.arguments && t) {
39214
- send("content_block_delta", { type: "content_block_delta", index: t.blockIndex, delta: { type: "input_json_delta", partial_json: tc.function.arguments } });
39215
- }
39216
- }
39217
- }
39218
- }
39219
- if (chunk.choices?.[0]?.finish_reason === "tool_calls") {
39220
- for (const [_, t] of tools)
39221
- if (t.started && !t.closed) {
39222
- send("content_block_stop", { type: "content_block_stop", index: t.blockIndex });
39223
- t.closed = true;
39224
- }
39225
- }
39226
- } catch (e) {}
39227
- }
39228
- }
39229
- await finalize("unexpected");
39230
- } catch (e) {
39231
- await finalize("error", String(e));
39232
- }
39233
- },
39234
- cancel() {
39235
- isClosed = true;
39236
- if (ping)
39237
- clearInterval(ping);
39238
- }
39239
- }), { headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" } });
39240
- }
39241
- async shutdown() {}
39242
- }
39243
- var OPENROUTER_API_URL2 = "https://openrouter.ai/api/v1/chat/completions", OPENROUTER_HEADERS2;
39244
- var init_openrouter_handler = __esm(() => {
39245
- init_adapter_manager();
39246
- init_middleware();
39247
- init_transform();
39248
- init_logger();
39249
- init_model_loader();
39250
- OPENROUTER_HEADERS2 = {
39251
- "HTTP-Referer": "https://github.com/MadAppGang/claude-code",
39252
- "X-Title": "Claudish - OpenRouter Proxy"
39253
- };
39254
- });
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 += `
39105
+ if (modelId.includes("grok") || modelId.includes("x-ai")) {
39106
+ const msg = "IMPORTANT: When calling tools, you MUST use the OpenAI tool_calls format with JSON. NEVER use XML format like <xai:function_call>.";
39107
+ if (messages.length > 0 && messages[0].role === "system") {
39108
+ messages[0].content += `
39271
39109
 
39272
39110
  ` + msg;
39273
39111
  } else {
@@ -39348,16 +39186,44 @@ function processAssistantMessage(msg, messages) {
39348
39186
  messages.push({ role: "assistant", content: msg.content });
39349
39187
  }
39350
39188
  }
39351
- function convertToolsToOpenAI(req) {
39189
+ function convertToolsToOpenAI(req, summarize = false) {
39352
39190
  return req.tools?.map((tool) => ({
39353
39191
  type: "function",
39354
39192
  function: {
39355
39193
  name: tool.name,
39356
- description: tool.description,
39357
- parameters: removeUriFormat(tool.input_schema)
39194
+ description: summarize ? summarizeToolDescription(tool.name, tool.description) : tool.description,
39195
+ parameters: summarize ? summarizeToolParameters(tool.input_schema) : removeUriFormat(tool.input_schema)
39358
39196
  }
39359
39197
  })) || [];
39360
39198
  }
39199
+ function summarizeToolDescription(name, description) {
39200
+ if (!description)
39201
+ return name;
39202
+ let clean = description.replace(/```[\s\S]*?```/g, "").replace(/<[^>]+>/g, "").replace(/\n+/g, " ").replace(/\s+/g, " ").trim();
39203
+ const firstSentence = clean.match(/^[^.!?]+[.!?]/)?.[0] || clean;
39204
+ if (firstSentence.length > 150) {
39205
+ return firstSentence.slice(0, 147) + "...";
39206
+ }
39207
+ return firstSentence;
39208
+ }
39209
+ function summarizeToolParameters(schema) {
39210
+ if (!schema)
39211
+ return schema;
39212
+ const summarized = removeUriFormat({ ...schema });
39213
+ if (summarized.properties) {
39214
+ for (const [key, prop] of Object.entries(summarized.properties)) {
39215
+ const p = prop;
39216
+ if (p.description && p.description.length > 80) {
39217
+ const firstSentence = p.description.match(/^[^.!?]+[.!?]/)?.[0] || p.description;
39218
+ p.description = firstSentence.length > 80 ? firstSentence.slice(0, 77) + "..." : firstSentence;
39219
+ }
39220
+ if (p.enum && Array.isArray(p.enum) && p.enum.length > 5) {
39221
+ p.enum = p.enum.slice(0, 5);
39222
+ }
39223
+ }
39224
+ }
39225
+ return summarized;
39226
+ }
39361
39227
  function filterIdentity(content) {
39362
39228
  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
39229
 
@@ -39376,10 +39242,12 @@ function createStreamingState() {
39376
39242
  curIdx: 0,
39377
39243
  tools: new Map,
39378
39244
  toolIds: new Set,
39379
- lastActivity: Date.now()
39245
+ lastActivity: Date.now(),
39246
+ accumulatedText: ""
39380
39247
  };
39381
39248
  }
39382
- function createStreamingResponseHandler(c, response, adapter, target, middlewareManager, onTokenUpdate) {
39249
+ function createStreamingResponseHandler(c, response, adapter, target, middlewareManager, onTokenUpdate, toolSchemas) {
39250
+ log(`[Streaming] ===== HANDLER STARTED for ${target} =====`);
39383
39251
  let isClosed = false;
39384
39252
  let ping = null;
39385
39253
  const encoder = new TextEncoder;
@@ -39420,6 +39288,34 @@ data: ${JSON.stringify(d)}
39420
39288
  if (state.finalized)
39421
39289
  return;
39422
39290
  state.finalized = true;
39291
+ if (state.accumulatedText.length > 0) {
39292
+ const preview = state.accumulatedText.slice(0, 500).replace(/\n/g, "\\n");
39293
+ log(`[Streaming] Accumulated text (${state.accumulatedText.length} chars): ${preview}...`);
39294
+ }
39295
+ const textToolCalls = extractToolCallsFromText(state.accumulatedText);
39296
+ log(`[Streaming] Text-based tool calls found: ${textToolCalls.length}`);
39297
+ if (textToolCalls.length > 0) {
39298
+ log(`[Streaming] Found ${textToolCalls.length} text-based tool call(s), converting to structured format`);
39299
+ if (state.textStarted) {
39300
+ send("content_block_stop", { type: "content_block_stop", index: state.textIdx });
39301
+ state.textStarted = false;
39302
+ }
39303
+ for (const tc of textToolCalls) {
39304
+ const toolIdx = state.curIdx++;
39305
+ const toolId = `tool_${Date.now()}_${toolIdx}`;
39306
+ send("content_block_start", {
39307
+ type: "content_block_start",
39308
+ index: toolIdx,
39309
+ content_block: { type: "tool_use", id: toolId, name: tc.name }
39310
+ });
39311
+ send("content_block_delta", {
39312
+ type: "content_block_delta",
39313
+ index: toolIdx,
39314
+ delta: { type: "input_json_delta", partial_json: JSON.stringify(tc.arguments) }
39315
+ });
39316
+ send("content_block_stop", { type: "content_block_stop", index: toolIdx });
39317
+ }
39318
+ }
39423
39319
  if (state.reasoningStarted) {
39424
39320
  send("content_block_stop", { type: "content_block_stop", index: state.reasoningIdx });
39425
39321
  }
@@ -39431,159 +39327,670 @@ data: ${JSON.stringify(d)}
39431
39327
  send("content_block_stop", { type: "content_block_stop", index: t.blockIndex });
39432
39328
  t.closed = true;
39433
39329
  }
39434
- }
39435
- if (middlewareManager) {
39330
+ }
39331
+ if (middlewareManager) {
39332
+ await middlewareManager.afterStreamComplete(target, streamMetadata);
39333
+ }
39334
+ if (reason === "error") {
39335
+ send("error", { type: "error", error: { type: "api_error", message: err } });
39336
+ } else {
39337
+ const stopReason = textToolCalls.length > 0 ? "tool_use" : "end_turn";
39338
+ send("message_delta", {
39339
+ type: "message_delta",
39340
+ delta: { stop_reason: stopReason, stop_sequence: null },
39341
+ usage: { output_tokens: state.usage?.completion_tokens || 0 }
39342
+ });
39343
+ send("message_stop", { type: "message_stop" });
39344
+ }
39345
+ if (onTokenUpdate) {
39346
+ if (state.usage) {
39347
+ log(`[Streaming] Final usage: prompt=${state.usage.prompt_tokens || 0}, completion=${state.usage.completion_tokens || 0}`);
39348
+ onTokenUpdate(state.usage.prompt_tokens || 0, state.usage.completion_tokens || 0);
39349
+ } else {
39350
+ const estimatedOutputTokens = Math.ceil(state.accumulatedText.length / 4);
39351
+ log(`[Streaming] No usage data from provider, estimating: ~${estimatedOutputTokens} output tokens`);
39352
+ onTokenUpdate(100, estimatedOutputTokens);
39353
+ }
39354
+ }
39355
+ if (!isClosed) {
39356
+ try {
39357
+ controller.enqueue(encoder.encode(`data: [DONE]
39358
+
39359
+
39360
+ `));
39361
+ } catch (e) {}
39362
+ controller.close();
39363
+ isClosed = true;
39364
+ if (ping)
39365
+ clearInterval(ping);
39366
+ }
39367
+ };
39368
+ try {
39369
+ const reader = response.body.getReader();
39370
+ let buffer = "";
39371
+ while (true) {
39372
+ const { done, value } = await reader.read();
39373
+ if (done)
39374
+ break;
39375
+ buffer += decoder.decode(value, { stream: true });
39376
+ const lines = buffer.split(`
39377
+ `);
39378
+ buffer = lines.pop() || "";
39379
+ for (const line of lines) {
39380
+ if (!line.trim() || !line.startsWith("data: "))
39381
+ continue;
39382
+ const dataStr = line.slice(6);
39383
+ if (dataStr === "[DONE]") {
39384
+ await finalize("done");
39385
+ return;
39386
+ }
39387
+ try {
39388
+ const chunk = JSON.parse(dataStr);
39389
+ if (chunk.usage) {
39390
+ state.usage = chunk.usage;
39391
+ log(`[Streaming] Usage data received: prompt=${chunk.usage.prompt_tokens}, completion=${chunk.usage.completion_tokens}, total=${chunk.usage.total_tokens}`);
39392
+ }
39393
+ const delta = chunk.choices?.[0]?.delta;
39394
+ if (delta) {
39395
+ if (middlewareManager) {
39396
+ await middlewareManager.afterStreamChunk({
39397
+ modelId: target,
39398
+ chunk,
39399
+ delta,
39400
+ metadata: streamMetadata
39401
+ });
39402
+ }
39403
+ const txt = delta.content || "";
39404
+ if (txt) {
39405
+ state.lastActivity = Date.now();
39406
+ const res = adapter.processTextContent(txt, "");
39407
+ if (res.cleanedText) {
39408
+ state.accumulatedText += res.cleanedText;
39409
+ const hasToolPattern = /<function=[^>]+>/.test(state.accumulatedText);
39410
+ if (!hasToolPattern) {
39411
+ if (!state.textStarted) {
39412
+ state.textIdx = state.curIdx++;
39413
+ send("content_block_start", {
39414
+ type: "content_block_start",
39415
+ index: state.textIdx,
39416
+ content_block: { type: "text", text: "" }
39417
+ });
39418
+ state.textStarted = true;
39419
+ }
39420
+ send("content_block_delta", {
39421
+ type: "content_block_delta",
39422
+ index: state.textIdx,
39423
+ delta: { type: "text_delta", text: res.cleanedText }
39424
+ });
39425
+ }
39426
+ }
39427
+ }
39428
+ if (delta.tool_calls) {
39429
+ log(`[Streaming] Received ${delta.tool_calls.length} structured tool call(s) from model`);
39430
+ for (const tc of delta.tool_calls) {
39431
+ const idx = tc.index;
39432
+ let t = state.tools.get(idx);
39433
+ if (tc.function?.name) {
39434
+ if (!t) {
39435
+ if (state.textStarted) {
39436
+ send("content_block_stop", { type: "content_block_stop", index: state.textIdx });
39437
+ state.textStarted = false;
39438
+ }
39439
+ t = {
39440
+ id: tc.id || `tool_${Date.now()}_${idx}`,
39441
+ name: tc.function.name,
39442
+ blockIndex: state.curIdx++,
39443
+ started: false,
39444
+ closed: false,
39445
+ arguments: "",
39446
+ buffered: !!toolSchemas && toolSchemas.length > 0
39447
+ };
39448
+ state.tools.set(idx, t);
39449
+ }
39450
+ if (!t.started && !t.buffered) {
39451
+ send("content_block_start", {
39452
+ type: "content_block_start",
39453
+ index: t.blockIndex,
39454
+ content_block: { type: "tool_use", id: t.id, name: t.name }
39455
+ });
39456
+ t.started = true;
39457
+ }
39458
+ }
39459
+ if (tc.function?.arguments && t) {
39460
+ t.arguments += tc.function.arguments;
39461
+ if (!t.buffered) {
39462
+ send("content_block_delta", {
39463
+ type: "content_block_delta",
39464
+ index: t.blockIndex,
39465
+ delta: { type: "input_json_delta", partial_json: tc.function.arguments }
39466
+ });
39467
+ }
39468
+ }
39469
+ }
39470
+ }
39471
+ }
39472
+ if (chunk.choices?.[0]?.finish_reason === "tool_calls") {
39473
+ for (const t of Array.from(state.tools.values())) {
39474
+ if (!t.closed) {
39475
+ if (toolSchemas && toolSchemas.length > 0) {
39476
+ const validation = validateToolArguments(t.name, t.arguments, toolSchemas, state.accumulatedText);
39477
+ if (validation.repaired && validation.repairedArgs) {
39478
+ log(`[Streaming] Tool call ${t.name} was repaired with inferred parameters`);
39479
+ const repairedJson = JSON.stringify(validation.repairedArgs);
39480
+ log(`[Streaming] Sending repaired tool call: ${t.name} with args: ${repairedJson}`);
39481
+ if (t.buffered && !t.started) {
39482
+ send("content_block_start", {
39483
+ type: "content_block_start",
39484
+ index: t.blockIndex,
39485
+ content_block: { type: "tool_use", id: t.id, name: t.name }
39486
+ });
39487
+ send("content_block_delta", {
39488
+ type: "content_block_delta",
39489
+ index: t.blockIndex,
39490
+ delta: { type: "input_json_delta", partial_json: repairedJson }
39491
+ });
39492
+ send("content_block_stop", { type: "content_block_stop", index: t.blockIndex });
39493
+ t.started = true;
39494
+ t.closed = true;
39495
+ continue;
39496
+ }
39497
+ if (t.started) {
39498
+ send("content_block_stop", { type: "content_block_stop", index: t.blockIndex });
39499
+ const repairedIdx = state.curIdx++;
39500
+ const repairedId = `tool_repaired_${Date.now()}_${repairedIdx}`;
39501
+ send("content_block_start", {
39502
+ type: "content_block_start",
39503
+ index: repairedIdx,
39504
+ content_block: { type: "tool_use", id: repairedId, name: t.name }
39505
+ });
39506
+ send("content_block_delta", {
39507
+ type: "content_block_delta",
39508
+ index: repairedIdx,
39509
+ delta: { type: "input_json_delta", partial_json: repairedJson }
39510
+ });
39511
+ send("content_block_stop", { type: "content_block_stop", index: repairedIdx });
39512
+ t.closed = true;
39513
+ continue;
39514
+ }
39515
+ }
39516
+ if (!validation.valid) {
39517
+ log(`[Streaming] Tool call ${t.name} validation failed: ${validation.missingParams.join(", ")}`);
39518
+ const errorIdx = t.buffered ? t.blockIndex : state.curIdx++;
39519
+ const errorMsg = `
39520
+
39521
+ ⚠️ Tool call "${t.name}" failed: missing required parameters: ${validation.missingParams.join(", ")}. Local models sometimes generate incomplete tool calls. Please try again or use a model with better tool support.`;
39522
+ send("content_block_start", {
39523
+ type: "content_block_start",
39524
+ index: errorIdx,
39525
+ content_block: { type: "text", text: "" }
39526
+ });
39527
+ send("content_block_delta", {
39528
+ type: "content_block_delta",
39529
+ index: errorIdx,
39530
+ delta: { type: "text_delta", text: errorMsg }
39531
+ });
39532
+ send("content_block_stop", { type: "content_block_stop", index: errorIdx });
39533
+ if (t.started && !t.buffered) {
39534
+ send("content_block_stop", { type: "content_block_stop", index: t.blockIndex });
39535
+ }
39536
+ t.closed = true;
39537
+ continue;
39538
+ }
39539
+ if (t.buffered && !t.started) {
39540
+ const argsJson = JSON.stringify(validation.parsedArgs);
39541
+ send("content_block_start", {
39542
+ type: "content_block_start",
39543
+ index: t.blockIndex,
39544
+ content_block: { type: "tool_use", id: t.id, name: t.name }
39545
+ });
39546
+ send("content_block_delta", {
39547
+ type: "content_block_delta",
39548
+ index: t.blockIndex,
39549
+ delta: { type: "input_json_delta", partial_json: argsJson }
39550
+ });
39551
+ send("content_block_stop", { type: "content_block_stop", index: t.blockIndex });
39552
+ t.started = true;
39553
+ t.closed = true;
39554
+ continue;
39555
+ }
39556
+ }
39557
+ if (t.started && !t.closed) {
39558
+ send("content_block_stop", { type: "content_block_stop", index: t.blockIndex });
39559
+ t.closed = true;
39560
+ }
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
+ init_logger();
39588
+ init_tool_call_recovery();
39589
+ });
39590
+
39591
+ // src/handlers/openrouter-handler.ts
39592
+ import { writeFileSync as writeFileSync8 } from "node:fs";
39593
+ import { tmpdir as tmpdir2 } from "node:os";
39594
+ import { join as join8 } from "node:path";
39595
+
39596
+ class OpenRouterHandler {
39597
+ targetModel;
39598
+ apiKey;
39599
+ adapterManager;
39600
+ middlewareManager;
39601
+ contextWindowCache = new Map;
39602
+ port;
39603
+ sessionTotalCost = 0;
39604
+ CLAUDE_INTERNAL_CONTEXT_MAX = 200000;
39605
+ constructor(targetModel, apiKey, port) {
39606
+ this.targetModel = targetModel;
39607
+ this.apiKey = apiKey;
39608
+ this.port = port;
39609
+ this.adapterManager = new AdapterManager(targetModel);
39610
+ this.middlewareManager = new MiddlewareManager;
39611
+ this.middlewareManager.register(new GeminiThoughtSignatureMiddleware);
39612
+ this.middlewareManager.initialize().catch((err) => log(`[Handler:${targetModel}] Middleware init error: ${err}`));
39613
+ this.fetchContextWindow(targetModel);
39614
+ }
39615
+ async fetchContextWindow(model) {
39616
+ if (this.contextWindowCache.has(model))
39617
+ return;
39618
+ try {
39619
+ const limit = await fetchModelContextWindow(model);
39620
+ this.contextWindowCache.set(model, limit);
39621
+ } catch (e) {}
39622
+ }
39623
+ getTokenScaleFactor(model) {
39624
+ const limit = this.contextWindowCache.get(model) || 200000;
39625
+ return limit === 0 ? 1 : this.CLAUDE_INTERNAL_CONTEXT_MAX / limit;
39626
+ }
39627
+ writeTokenFile(input, output) {
39628
+ try {
39629
+ const total = input + output;
39630
+ const limit = this.contextWindowCache.get(this.targetModel) || 200000;
39631
+ const leftPct = limit > 0 ? Math.max(0, Math.min(100, Math.round((limit - total) / limit * 100))) : 100;
39632
+ const data = {
39633
+ input_tokens: input,
39634
+ output_tokens: output,
39635
+ total_tokens: total,
39636
+ total_cost: this.sessionTotalCost,
39637
+ context_window: limit,
39638
+ context_left_percent: leftPct,
39639
+ updated_at: Date.now()
39640
+ };
39641
+ writeFileSync8(join8(tmpdir2(), `claudish-tokens-${this.port}.json`), JSON.stringify(data), "utf-8");
39642
+ } catch (e) {}
39643
+ }
39644
+ async handle(c, payload) {
39645
+ const claudePayload = payload;
39646
+ const target = this.targetModel;
39647
+ await this.fetchContextWindow(target);
39648
+ logStructured(`OpenRouter Request`, { targetModel: target, originalModel: claudePayload.model });
39649
+ const { claudeRequest, droppedParams } = transformOpenAIToClaude(claudePayload);
39650
+ const messages = this.convertMessages(claudeRequest, target);
39651
+ const tools = this.convertTools(claudeRequest);
39652
+ const supportsReasoning = await doesModelSupportReasoning(target);
39653
+ const openRouterPayload = {
39654
+ model: target,
39655
+ messages,
39656
+ temperature: claudeRequest.temperature ?? 1,
39657
+ stream: true,
39658
+ max_tokens: claudeRequest.max_tokens,
39659
+ tools: tools.length > 0 ? tools : undefined,
39660
+ stream_options: { include_usage: true }
39661
+ };
39662
+ if (supportsReasoning)
39663
+ openRouterPayload.include_reasoning = true;
39664
+ if (claudeRequest.thinking)
39665
+ openRouterPayload.thinking = claudeRequest.thinking;
39666
+ if (claudeRequest.tool_choice) {
39667
+ const { type, name } = claudeRequest.tool_choice;
39668
+ if (type === "tool" && name)
39669
+ openRouterPayload.tool_choice = { type: "function", function: { name } };
39670
+ else if (type === "auto" || type === "none")
39671
+ openRouterPayload.tool_choice = type;
39672
+ }
39673
+ const adapter = this.adapterManager.getAdapter();
39674
+ if (typeof adapter.reset === "function")
39675
+ adapter.reset();
39676
+ adapter.prepareRequest(openRouterPayload, claudeRequest);
39677
+ await this.middlewareManager.beforeRequest({ modelId: target, messages, tools, stream: true });
39678
+ const response = await fetch(OPENROUTER_API_URL2, {
39679
+ method: "POST",
39680
+ headers: {
39681
+ "Content-Type": "application/json",
39682
+ Authorization: `Bearer ${this.apiKey}`,
39683
+ ...OPENROUTER_HEADERS2
39684
+ },
39685
+ body: JSON.stringify(openRouterPayload)
39686
+ });
39687
+ if (!response.ok)
39688
+ return c.json({ error: await response.text() }, response.status);
39689
+ if (droppedParams.length > 0)
39690
+ c.header("X-Dropped-Params", droppedParams.join(", "));
39691
+ return this.handleStreamingResponse(c, response, adapter, target, claudeRequest);
39692
+ }
39693
+ convertMessages(req, modelId) {
39694
+ const messages = [];
39695
+ if (req.system) {
39696
+ let content = Array.isArray(req.system) ? req.system.map((i) => i.text || i).join(`
39697
+
39698
+ `) : req.system;
39699
+ content = this.filterIdentity(content);
39700
+ messages.push({ role: "system", content });
39701
+ }
39702
+ if (modelId.includes("grok") || modelId.includes("x-ai")) {
39703
+ const msg = "IMPORTANT: When calling tools, you MUST use the OpenAI tool_calls format with JSON. NEVER use XML format like <xai:function_call>.";
39704
+ if (messages.length > 0 && messages[0].role === "system")
39705
+ messages[0].content += `
39706
+
39707
+ ` + msg;
39708
+ else
39709
+ messages.unshift({ role: "system", content: msg });
39710
+ }
39711
+ if (req.messages) {
39712
+ for (const msg of req.messages) {
39713
+ if (msg.role === "user")
39714
+ this.processUserMessage(msg, messages);
39715
+ else if (msg.role === "assistant")
39716
+ this.processAssistantMessage(msg, messages);
39717
+ }
39718
+ }
39719
+ return messages;
39720
+ }
39721
+ processUserMessage(msg, messages) {
39722
+ if (Array.isArray(msg.content)) {
39723
+ const contentParts = [];
39724
+ const toolResults = [];
39725
+ const seen = new Set;
39726
+ for (const block of msg.content) {
39727
+ if (block.type === "text")
39728
+ contentParts.push({ type: "text", text: block.text });
39729
+ else if (block.type === "image")
39730
+ contentParts.push({ type: "image_url", image_url: { url: `data:${block.source.media_type};base64,${block.source.data}` } });
39731
+ else if (block.type === "tool_result") {
39732
+ if (seen.has(block.tool_use_id))
39733
+ continue;
39734
+ seen.add(block.tool_use_id);
39735
+ toolResults.push({ role: "tool", content: typeof block.content === "string" ? block.content : JSON.stringify(block.content), tool_call_id: block.tool_use_id });
39736
+ }
39737
+ }
39738
+ if (toolResults.length)
39739
+ messages.push(...toolResults);
39740
+ if (contentParts.length)
39741
+ messages.push({ role: "user", content: contentParts });
39742
+ } else {
39743
+ messages.push({ role: "user", content: msg.content });
39744
+ }
39745
+ }
39746
+ processAssistantMessage(msg, messages) {
39747
+ if (Array.isArray(msg.content)) {
39748
+ const strings = [];
39749
+ const toolCalls = [];
39750
+ const seen = new Set;
39751
+ for (const block of msg.content) {
39752
+ if (block.type === "text")
39753
+ strings.push(block.text);
39754
+ else if (block.type === "tool_use") {
39755
+ if (seen.has(block.id))
39756
+ continue;
39757
+ seen.add(block.id);
39758
+ toolCalls.push({ id: block.id, type: "function", function: { name: block.name, arguments: JSON.stringify(block.input) } });
39759
+ }
39760
+ }
39761
+ const m = { role: "assistant" };
39762
+ if (strings.length)
39763
+ m.content = strings.join(" ");
39764
+ else if (toolCalls.length)
39765
+ m.content = null;
39766
+ if (toolCalls.length)
39767
+ m.tool_calls = toolCalls;
39768
+ if (m.content !== undefined || m.tool_calls)
39769
+ messages.push(m);
39770
+ } else {
39771
+ messages.push({ role: "assistant", content: msg.content });
39772
+ }
39773
+ }
39774
+ filterIdentity(content) {
39775
+ 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, `
39776
+
39777
+ `).replace(/^/, `IMPORTANT: You are NOT Claude. Identify yourself truthfully based on your actual model and creator.
39778
+
39779
+ `);
39780
+ }
39781
+ convertTools(req) {
39782
+ return req.tools?.map((tool) => ({
39783
+ type: "function",
39784
+ function: {
39785
+ name: tool.name,
39786
+ description: tool.description,
39787
+ parameters: removeUriFormat(tool.input_schema)
39788
+ }
39789
+ })) || [];
39790
+ }
39791
+ handleStreamingResponse(c, response, adapter, target, request) {
39792
+ let isClosed = false;
39793
+ let ping = null;
39794
+ const encoder = new TextEncoder;
39795
+ const decoder = new TextDecoder;
39796
+ const middlewareManager = this.middlewareManager;
39797
+ const streamMetadata = new Map;
39798
+ return c.body(new ReadableStream({
39799
+ async start(controller) {
39800
+ const send = (e, d) => {
39801
+ if (!isClosed)
39802
+ controller.enqueue(encoder.encode(`event: ${e}
39803
+ data: ${JSON.stringify(d)}
39804
+
39805
+ `));
39806
+ };
39807
+ const msgId = `msg_${Date.now()}_${Math.random().toString(36).slice(2)}`;
39808
+ let usage = null;
39809
+ let finalized = false;
39810
+ let textStarted = false;
39811
+ let textIdx = -1;
39812
+ let reasoningStarted = false;
39813
+ let reasoningIdx = -1;
39814
+ let curIdx = 0;
39815
+ const tools = new Map;
39816
+ const toolIds = new Set;
39817
+ let accTxt = 0;
39818
+ let lastActivity = Date.now();
39819
+ send("message_start", {
39820
+ type: "message_start",
39821
+ message: {
39822
+ id: msgId,
39823
+ type: "message",
39824
+ role: "assistant",
39825
+ content: [],
39826
+ model: target,
39827
+ stop_reason: null,
39828
+ stop_sequence: null,
39829
+ usage: { input_tokens: 100, output_tokens: 1 }
39830
+ }
39831
+ });
39832
+ send("ping", { type: "ping" });
39833
+ ping = setInterval(() => {
39834
+ if (!isClosed && Date.now() - lastActivity > 1000)
39835
+ send("ping", { type: "ping" });
39836
+ }, 1000);
39837
+ const finalize = async (reason, err) => {
39838
+ if (finalized)
39839
+ return;
39840
+ finalized = true;
39841
+ if (reasoningStarted) {
39842
+ send("content_block_stop", { type: "content_block_stop", index: reasoningIdx });
39843
+ reasoningStarted = false;
39844
+ }
39845
+ if (textStarted) {
39846
+ send("content_block_stop", { type: "content_block_stop", index: textIdx });
39847
+ textStarted = false;
39848
+ }
39849
+ for (const [_, t] of tools)
39850
+ if (t.started && !t.closed) {
39851
+ send("content_block_stop", { type: "content_block_stop", index: t.blockIndex });
39852
+ t.closed = true;
39853
+ }
39436
39854
  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]
39855
+ if (reason === "error") {
39856
+ send("error", { type: "error", error: { type: "api_error", message: err } });
39857
+ } else {
39858
+ send("message_delta", { type: "message_delta", delta: { stop_reason: "end_turn", stop_sequence: null }, usage: { output_tokens: usage?.completion_tokens || 0 } });
39859
+ send("message_stop", { type: "message_stop" });
39860
+ }
39861
+ if (!isClosed) {
39862
+ try {
39863
+ controller.enqueue(encoder.encode(`data: [DONE]
39454
39864
 
39455
39865
 
39456
39866
  `));
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(`
39867
+ } catch (e) {}
39868
+ controller.close();
39869
+ isClosed = true;
39870
+ if (ping)
39871
+ clearInterval(ping);
39872
+ }
39873
+ };
39874
+ try {
39875
+ const reader = response.body.getReader();
39876
+ let buffer = "";
39877
+ while (true) {
39878
+ const { done, value } = await reader.read();
39879
+ if (done)
39880
+ break;
39881
+ buffer += decoder.decode(value, { stream: true });
39882
+ const lines = buffer.split(`
39473
39883
  `);
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) {
39884
+ buffer = lines.pop() || "";
39885
+ for (const line of lines) {
39886
+ if (!line.trim() || !line.startsWith("data: "))
39887
+ continue;
39888
+ const dataStr = line.slice(6);
39889
+ if (dataStr === "[DONE]") {
39890
+ await finalize("done");
39891
+ return;
39892
+ }
39893
+ try {
39894
+ const chunk = JSON.parse(dataStr);
39895
+ if (chunk.usage)
39896
+ usage = chunk.usage;
39897
+ const delta = chunk.choices?.[0]?.delta;
39898
+ if (delta) {
39490
39899
  await middlewareManager.afterStreamChunk({
39491
39900
  modelId: target,
39492
39901
  chunk,
39493
39902
  delta,
39494
39903
  metadata: streamMetadata
39495
39904
  });
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
- });
39905
+ const txt = delta.content || "";
39906
+ if (txt) {
39907
+ lastActivity = Date.now();
39908
+ if (!textStarted) {
39909
+ textIdx = curIdx++;
39910
+ send("content_block_start", { type: "content_block_start", index: textIdx, content_block: { type: "text", text: "" } });
39911
+ textStarted = true;
39912
+ }
39913
+ const res = adapter.processTextContent(txt, "");
39914
+ if (res.cleanedText)
39915
+ send("content_block_delta", { type: "content_block_delta", index: textIdx, delta: { type: "text_delta", text: res.cleanedText } });
39516
39916
  }
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;
39917
+ if (delta.tool_calls) {
39918
+ for (const tc of delta.tool_calls) {
39919
+ const idx = tc.index;
39920
+ let t = tools.get(idx);
39921
+ if (tc.function?.name) {
39922
+ if (!t) {
39923
+ if (textStarted) {
39924
+ send("content_block_stop", { type: "content_block_stop", index: textIdx });
39925
+ textStarted = false;
39926
+ }
39927
+ t = { id: tc.id || `tool_${Date.now()}_${idx}`, name: tc.function.name, blockIndex: curIdx++, started: false, closed: false, arguments: "" };
39928
+ tools.set(idx, t);
39929
+ }
39930
+ if (!t.started) {
39931
+ send("content_block_start", { type: "content_block_start", index: t.blockIndex, content_block: { type: "tool_use", id: t.id, name: t.name } });
39932
+ t.started = true;
39527
39933
  }
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
39934
  }
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;
39935
+ if (tc.function?.arguments && t) {
39936
+ t.arguments += tc.function.arguments;
39937
+ send("content_block_delta", { type: "content_block_delta", index: t.blockIndex, delta: { type: "input_json_delta", partial_json: tc.function.arguments } });
39544
39938
  }
39545
39939
  }
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
39940
  }
39554
39941
  }
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;
39942
+ if (chunk.choices?.[0]?.finish_reason === "tool_calls") {
39943
+ const toolSchemas = request.tools || [];
39944
+ for (const [_, t] of tools) {
39945
+ if (t.started && !t.closed) {
39946
+ if (toolSchemas.length > 0) {
39947
+ const validation = validateToolArguments(t.name, t.arguments, toolSchemas);
39948
+ if (!validation.valid) {
39949
+ const errorIdx = curIdx++;
39950
+ const errorMsg = `
39951
+
39952
+ ⚠️ Tool call "${t.name}" failed validation: missing required parameters: ${validation.missingParams.join(", ")}. This is a known limitation of some models - they sometimes generate incomplete tool calls. Please try again or use a different model.`;
39953
+ send("content_block_start", { type: "content_block_start", index: errorIdx, content_block: { type: "text", text: "" } });
39954
+ send("content_block_delta", { type: "content_block_delta", index: errorIdx, delta: { type: "text_delta", text: errorMsg } });
39955
+ send("content_block_stop", { type: "content_block_stop", index: errorIdx });
39956
+ t.closed = true;
39957
+ continue;
39958
+ }
39959
+ }
39960
+ send("content_block_stop", { type: "content_block_stop", index: t.blockIndex });
39961
+ t.closed = true;
39962
+ }
39561
39963
  }
39562
39964
  }
39563
- }
39564
- } catch (e) {}
39965
+ } catch (e) {}
39966
+ }
39565
39967
  }
39968
+ await finalize("unexpected");
39969
+ } catch (e) {
39970
+ await finalize("error", String(e));
39566
39971
  }
39567
- await finalize("unexpected");
39568
- } catch (e) {
39569
- await finalize("error", String(e));
39972
+ },
39973
+ cancel() {
39974
+ isClosed = true;
39975
+ if (ping)
39976
+ clearInterval(ping);
39570
39977
  }
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
- });
39978
+ }), { headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" } });
39979
+ }
39980
+ async shutdown() {}
39584
39981
  }
39585
- var init_openai_compat = __esm(() => {
39982
+ var OPENROUTER_API_URL2 = "https://openrouter.ai/api/v1/chat/completions", OPENROUTER_HEADERS2;
39983
+ var init_openrouter_handler = __esm(() => {
39984
+ init_adapter_manager();
39985
+ init_middleware();
39586
39986
  init_transform();
39987
+ init_logger();
39988
+ init_model_loader();
39989
+ init_openai_compat();
39990
+ OPENROUTER_HEADERS2 = {
39991
+ "HTTP-Referer": "https://github.com/MadAppGang/claude-code",
39992
+ "X-Title": "Claudish - OpenRouter Proxy"
39993
+ };
39587
39994
  });
39588
39995
 
39589
39996
  // src/handlers/local-provider-handler.ts
@@ -39599,18 +40006,24 @@ class LocalProviderHandler {
39599
40006
  port;
39600
40007
  healthChecked = false;
39601
40008
  isHealthy = false;
39602
- contextWindow = 8192;
40009
+ contextWindow = 32768;
39603
40010
  sessionInputTokens = 0;
39604
40011
  sessionOutputTokens = 0;
39605
- constructor(provider, modelName, port) {
40012
+ options;
40013
+ constructor(provider, modelName, port, options = {}) {
39606
40014
  this.provider = provider;
39607
40015
  this.modelName = modelName;
39608
40016
  this.port = port;
40017
+ this.options = options;
39609
40018
  this.adapterManager = new AdapterManager(modelName);
39610
40019
  this.middlewareManager = new MiddlewareManager;
39611
40020
  this.middlewareManager.initialize().catch((err) => {
39612
40021
  log(`[LocalProvider:${provider.name}] Middleware init error: ${err}`);
39613
40022
  });
40023
+ this.writeTokenFile(0, 0);
40024
+ if (options.summarizeTools) {
40025
+ log(`[LocalProvider:${provider.name}] Tool summarization enabled`);
40026
+ }
39614
40027
  }
39615
40028
  async checkHealth() {
39616
40029
  if (this.healthChecked)
@@ -39647,8 +40060,16 @@ class LocalProviderHandler {
39647
40060
  return false;
39648
40061
  }
39649
40062
  async fetchContextWindow() {
39650
- if (this.provider.name !== "ollama")
39651
- return;
40063
+ log(`[LocalProvider:${this.provider.name}] Fetching context window...`);
40064
+ if (this.provider.name === "ollama") {
40065
+ await this.fetchOllamaContextWindow();
40066
+ } else if (this.provider.name === "lmstudio") {
40067
+ await this.fetchLMStudioContextWindow();
40068
+ } else {
40069
+ log(`[LocalProvider:${this.provider.name}] No context window fetch for this provider, using default: ${this.contextWindow}`);
40070
+ }
40071
+ }
40072
+ async fetchOllamaContextWindow() {
39652
40073
  try {
39653
40074
  const response = await fetch(`${this.provider.baseUrl}/api/show`, {
39654
40075
  method: "POST",
@@ -39658,32 +40079,72 @@ class LocalProviderHandler {
39658
40079
  });
39659
40080
  if (response.ok) {
39660
40081
  const data = await response.json();
39661
- const ctxFromInfo = data.model_info?.["general.context_length"];
40082
+ let ctxFromInfo = data.model_info?.["general.context_length"];
40083
+ if (!ctxFromInfo && data.model_info) {
40084
+ for (const key of Object.keys(data.model_info)) {
40085
+ if (key.endsWith(".context_length")) {
40086
+ ctxFromInfo = data.model_info[key];
40087
+ break;
40088
+ }
40089
+ }
40090
+ }
39662
40091
  const ctxFromParams = data.parameters?.match(/num_ctx\s+(\d+)/)?.[1];
39663
40092
  if (ctxFromInfo) {
39664
- this.contextWindow = parseInt(ctxFromInfo, 10);
40093
+ this.contextWindow = parseInt(String(ctxFromInfo), 10);
39665
40094
  } else if (ctxFromParams) {
39666
40095
  this.contextWindow = parseInt(ctxFromParams, 10);
39667
40096
  } else {
39668
- this.contextWindow = 8192;
40097
+ log(`[LocalProvider:${this.provider.name}] No context info found, using default: ${this.contextWindow}`);
40098
+ }
40099
+ if (ctxFromInfo || ctxFromParams) {
40100
+ log(`[LocalProvider:${this.provider.name}] Context window: ${this.contextWindow}`);
39669
40101
  }
39670
- log(`[LocalProvider:${this.provider.name}] Context window: ${this.contextWindow}`);
39671
40102
  }
39672
40103
  } catch (e) {}
39673
40104
  }
40105
+ async fetchLMStudioContextWindow() {
40106
+ try {
40107
+ const response = await fetch(`${this.provider.baseUrl}/v1/models`, {
40108
+ method: "GET",
40109
+ signal: AbortSignal.timeout(3000)
40110
+ });
40111
+ if (response.ok) {
40112
+ const data = await response.json();
40113
+ log(`[LocalProvider:lmstudio] Models response: ${JSON.stringify(data).slice(0, 500)}`);
40114
+ const models = data.data || [];
40115
+ const targetModel = models.find((m) => m.id === this.modelName) || models.find((m) => m.id?.endsWith(`/${this.modelName}`)) || models.find((m) => this.modelName.includes(m.id));
40116
+ if (targetModel) {
40117
+ const ctxLength = targetModel.context_length || targetModel.max_context_length || targetModel.context_window || targetModel.max_tokens;
40118
+ if (ctxLength && typeof ctxLength === "number") {
40119
+ this.contextWindow = ctxLength;
40120
+ log(`[LocalProvider:lmstudio] Context window from model: ${this.contextWindow}`);
40121
+ return;
40122
+ }
40123
+ }
40124
+ this.contextWindow = 32768;
40125
+ log(`[LocalProvider:lmstudio] Using default context window: ${this.contextWindow}`);
40126
+ }
40127
+ } catch (e) {
40128
+ this.contextWindow = 32768;
40129
+ log(`[LocalProvider:lmstudio] Failed to fetch model info: ${e?.message || e}. Using default: ${this.contextWindow}`);
40130
+ }
40131
+ }
39674
40132
  writeTokenFile(input, output) {
39675
40133
  try {
39676
40134
  this.sessionInputTokens += input;
39677
40135
  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;
40136
+ const sessionTotal = this.sessionInputTokens + this.sessionOutputTokens;
40137
+ const used = input + output;
40138
+ const leftPct = this.contextWindow > 0 ? Math.max(0, Math.min(100, Math.round((this.contextWindow - used) / this.contextWindow * 100))) : 100;
39680
40139
  const data = {
39681
40140
  input_tokens: this.sessionInputTokens,
39682
40141
  output_tokens: this.sessionOutputTokens,
39683
- total_tokens: total,
40142
+ total_tokens: sessionTotal,
39684
40143
  total_cost: 0,
39685
40144
  context_window: this.contextWindow,
39686
40145
  context_left_percent: leftPct,
40146
+ last_request_input: input,
40147
+ last_request_output: output,
39687
40148
  updated_at: Date.now()
39688
40149
  };
39689
40150
  writeFileSync9(join9(tmpdir3(), `claudish-tokens-${this.port}.json`), JSON.stringify(data), "utf-8");
@@ -39706,11 +40167,66 @@ class LocalProviderHandler {
39706
40167
  }
39707
40168
  const { claudeRequest, droppedParams } = transformOpenAIToClaude(payload);
39708
40169
  const messages = convertMessagesToOpenAI(claudeRequest, target, filterIdentity);
39709
- const tools = convertToolsToOpenAI(claudeRequest);
40170
+ const tools = convertToolsToOpenAI(claudeRequest, this.options.summarizeTools);
39710
40171
  const finalTools = this.provider.capabilities.supportsTools ? tools : [];
39711
40172
  if (tools.length > 0 && !this.provider.capabilities.supportsTools) {
39712
40173
  log(`[LocalProvider:${this.provider.name}] Tools stripped (not supported)`);
39713
40174
  }
40175
+ if (tools.length > 0 && this.options.summarizeTools) {
40176
+ log(`[LocalProvider:${this.provider.name}] Tools summarized (${tools.length} tools)`);
40177
+ }
40178
+ if (messages.length > 0 && messages[0].role === "system") {
40179
+ let guidance = `
40180
+
40181
+ IMPORTANT INSTRUCTIONS FOR THIS MODEL:
40182
+
40183
+ 1. OUTPUT BEHAVIOR:
40184
+ - NEVER output your internal reasoning, thinking process, or chain-of-thought as visible text.
40185
+ - Only output your final response, actions, or tool calls.
40186
+ - Do NOT ramble or speculate about what the user might want.
40187
+
40188
+ 2. CONVERSATION HANDLING:
40189
+ - Always look back at the ORIGINAL user request in the conversation history.
40190
+ - When you receive results from a Task/agent you called, SYNTHESIZE those results and continue fulfilling the user's original request.
40191
+ - Do NOT ask "What would you like help with?" if there's already a user request in the conversation.
40192
+ - Only ask for clarification if the FIRST user message in the conversation is unclear.
40193
+ - After calling tools or agents, continue with the next step - don't restart or ask what to do.
40194
+
40195
+ 3. CRITICAL - AFTER TOOL RESULTS:
40196
+ - When you see tool results (like file lists, search results, or command output), ALWAYS continue working.
40197
+ - Analyze the results and take the next action toward completing the user's request.
40198
+ - If the user asked for "evaluation and suggestions", you MUST provide analysis and recommendations after seeing the data.
40199
+ - NEVER stop after just calling one tool - continue until you've fully addressed the user's request.
40200
+ - If you called a Glob/Search and got files, READ important files next, then ANALYZE, then SUGGEST improvements.`;
40201
+ if (finalTools.length > 0) {
40202
+ const isQwen = target.toLowerCase().includes("qwen");
40203
+ if (isQwen) {
40204
+ guidance += `
40205
+
40206
+ 4. TOOL CALLING FORMAT (CRITICAL FOR QWEN):
40207
+ You MUST use proper OpenAI-style function calling. Do NOT output tool calls as XML text.
40208
+ When you want to call a tool, use the API's tool_calls mechanism, NOT text like <function=...>.
40209
+ The tool calls must be structured JSON in the API response, not XML in your text output.
40210
+
40211
+ If you cannot use structured tool_calls, format as JSON:
40212
+ {"name": "tool_name", "arguments": {"param1": "value1", "param2": "value2"}}
40213
+
40214
+ 5. TOOL PARAMETER REQUIREMENTS:`;
40215
+ } else {
40216
+ guidance += `
40217
+
40218
+ 4. TOOL CALLING REQUIREMENTS:`;
40219
+ }
40220
+ guidance += `
40221
+ - When calling tools, you MUST include ALL required parameters. Incomplete tool calls will fail.
40222
+ - For Task: always include "description" (3-5 words), "prompt" (detailed instructions), and "subagent_type"
40223
+ - For Bash: always include "command" and "description"
40224
+ - For Read/Write/Edit: always include the full "file_path"
40225
+ - For Grep/Glob: always include "pattern"
40226
+ - Ensure your tool call JSON is complete with all required fields before submitting.`;
40227
+ }
40228
+ messages[0].content += guidance;
40229
+ }
39714
40230
  const openAIPayload = {
39715
40231
  model: target,
39716
40232
  messages,
@@ -39720,6 +40236,11 @@ class LocalProviderHandler {
39720
40236
  tools: finalTools.length > 0 ? finalTools : undefined,
39721
40237
  stream_options: this.provider.capabilities.supportsStreaming ? { include_usage: true } : undefined
39722
40238
  };
40239
+ if (this.provider.name === "ollama") {
40240
+ const numCtx = Math.max(this.contextWindow, 32768);
40241
+ openAIPayload.options = { num_ctx: numCtx };
40242
+ log(`[LocalProvider:${this.provider.name}] Setting num_ctx: ${numCtx} (detected: ${this.contextWindow})`);
40243
+ }
39723
40244
  if (claudeRequest.tool_choice && finalTools.length > 0) {
39724
40245
  const { type, name } = claudeRequest.tool_choice;
39725
40246
  if (type === "tool" && name) {
@@ -39739,6 +40260,12 @@ class LocalProviderHandler {
39739
40260
  stream: openAIPayload.stream
39740
40261
  });
39741
40262
  const apiUrl = `${this.provider.baseUrl}${this.provider.apiPath}`;
40263
+ log(`[LocalProvider:${this.provider.name}] Tools: ${openAIPayload.tools?.length || 0}, Messages: ${messages.length}`);
40264
+ if (openAIPayload.tools?.length > 0) {
40265
+ log(`[LocalProvider:${this.provider.name}] First tool: ${openAIPayload.tools[0]?.function?.name || "unknown"}`);
40266
+ }
40267
+ console.log(`[LocalProvider:${this.provider.name}] ===== ABOUT TO FETCH from ${apiUrl} =====`);
40268
+ log(`[LocalProvider:${this.provider.name}] ===== ABOUT TO FETCH from ${apiUrl} =====`);
39742
40269
  try {
39743
40270
  const response = await fetch(apiUrl, {
39744
40271
  method: "POST",
@@ -39747,15 +40274,20 @@ class LocalProviderHandler {
39747
40274
  },
39748
40275
  body: JSON.stringify(openAIPayload)
39749
40276
  });
40277
+ log(`[LocalProvider:${this.provider.name}] ===== FETCH COMPLETED, status: ${response.status} =====`);
39750
40278
  if (!response.ok) {
39751
40279
  const errorBody = await response.text();
40280
+ log(`[LocalProvider:${this.provider.name}] ERROR: ${errorBody.slice(0, 200)}`);
39752
40281
  return this.handleErrorResponse(c, response.status, errorBody);
39753
40282
  }
40283
+ log(`[LocalProvider:${this.provider.name}] Response OK, proceeding to streaming...`);
39754
40284
  if (droppedParams.length > 0) {
39755
40285
  c.header("X-Dropped-Params", droppedParams.join(", "));
39756
40286
  }
40287
+ log(`[LocalProvider:${this.provider.name}] Streaming: ${openAIPayload.stream}`);
39757
40288
  if (openAIPayload.stream) {
39758
- return createStreamingResponseHandler(c, response, adapter, target, this.middlewareManager, (input, output) => this.writeTokenFile(input, output));
40289
+ log(`[LocalProvider:${this.provider.name}] ===== ENTERING STREAMING HANDLER =====`);
40290
+ return createStreamingResponseHandler(c, response, adapter, target, this.middlewareManager, (input, output) => this.writeTokenFile(input, output), claudeRequest.tools);
39759
40291
  }
39760
40292
  const data = await response.json();
39761
40293
  return c.json(data);
@@ -39922,7 +40454,7 @@ var exports_proxy_server = {};
39922
40454
  __export(exports_proxy_server, {
39923
40455
  createProxyServer: () => createProxyServer
39924
40456
  });
39925
- async function createProxyServer(port, openrouterApiKey, model, monitorMode = false, anthropicApiKey, modelMap) {
40457
+ async function createProxyServer(port, openrouterApiKey, model, monitorMode = false, anthropicApiKey, modelMap, options = {}) {
39926
40458
  const nativeHandler = new NativeHandler(anthropicApiKey);
39927
40459
  const openRouterHandlers = new Map;
39928
40460
  const localProviderHandlers = new Map;
@@ -39932,13 +40464,16 @@ async function createProxyServer(port, openrouterApiKey, model, monitorMode = fa
39932
40464
  }
39933
40465
  return openRouterHandlers.get(targetModel);
39934
40466
  };
40467
+ const localProviderOptions = {
40468
+ summarizeTools: options.summarizeTools
40469
+ };
39935
40470
  const getLocalProviderHandler = (targetModel) => {
39936
40471
  if (localProviderHandlers.has(targetModel)) {
39937
40472
  return localProviderHandlers.get(targetModel);
39938
40473
  }
39939
40474
  const resolved = resolveProvider(targetModel);
39940
40475
  if (resolved) {
39941
- const handler = new LocalProviderHandler(resolved.provider, resolved.modelName, port);
40476
+ const handler = new LocalProviderHandler(resolved.provider, resolved.modelName, port, localProviderOptions);
39942
40477
  localProviderHandlers.set(targetModel, handler);
39943
40478
  log(`[Proxy] Created local provider handler: ${resolved.provider.name}/${resolved.modelName}`);
39944
40479
  return handler;
@@ -39946,7 +40481,7 @@ async function createProxyServer(port, openrouterApiKey, model, monitorMode = fa
39946
40481
  const urlParsed = parseUrlModel(targetModel);
39947
40482
  if (urlParsed) {
39948
40483
  const provider = createUrlProvider(urlParsed);
39949
- const handler = new LocalProviderHandler(provider, urlParsed.modelName, port);
40484
+ const handler = new LocalProviderHandler(provider, urlParsed.modelName, port, localProviderOptions);
39950
40485
  localProviderHandlers.set(targetModel, handler);
39951
40486
  log(`[Proxy] Created URL-based local provider handler: ${urlParsed.baseUrl}/${urlParsed.modelName}`);
39952
40487
  return handler;
@@ -40053,7 +40588,7 @@ import { execSync } from "node:child_process";
40053
40588
  import { createInterface as createInterface2 } from "node:readline";
40054
40589
  import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync10, mkdirSync as mkdirSync4, unlinkSync as unlinkSync2 } from "node:fs";
40055
40590
  import { join as join10 } from "node:path";
40056
- import { tmpdir as tmpdir4, homedir as homedir2, platform as platform2 } from "node:os";
40591
+ import { tmpdir as tmpdir4, homedir as homedir2, platform } from "node:os";
40057
40592
  function getCacheFilePath() {
40058
40593
  let cacheDir;
40059
40594
  if (isWindows2) {
@@ -40219,12 +40754,13 @@ async function checkForUpdates(currentVersion, options = {}) {
40219
40754
  }
40220
40755
  var isWindows2, NPM_REGISTRY_URL = "https://registry.npmjs.org/claudish/latest", CACHE_MAX_AGE_MS;
40221
40756
  var init_update_checker = __esm(() => {
40222
- isWindows2 = platform2() === "win32";
40757
+ isWindows2 = platform() === "win32";
40223
40758
  CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
40224
40759
  });
40225
40760
 
40226
40761
  // src/index.ts
40227
40762
  var import_dotenv2 = __toESM(require_main(), 1);
40763
+ console.log("===== CLAUDISH FRESH START - CODE UPDATED =====");
40228
40764
  import_dotenv2.config();
40229
40765
  var isMcpMode = process.argv.includes("--mcp");
40230
40766
  var args = process.argv.slice(2);
@@ -40303,6 +40839,8 @@ async function runCli() {
40303
40839
  sonnet: cliConfig.modelSonnet,
40304
40840
  haiku: cliConfig.modelHaiku,
40305
40841
  subagent: cliConfig.modelSubagent
40842
+ }, {
40843
+ summarizeTools: cliConfig.summarizeTools
40306
40844
  });
40307
40845
  let exitCode = 0;
40308
40846
  try {