claudish 2.9.0 → 2.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +769 -602
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -34324,8 +34324,11 @@ __export(exports_claude_runner, {
|
|
|
34324
34324
|
});
|
|
34325
34325
|
import { spawn } from "node:child_process";
|
|
34326
34326
|
import { writeFileSync as writeFileSync4, unlinkSync } from "node:fs";
|
|
34327
|
-
import { tmpdir
|
|
34327
|
+
import { tmpdir } from "node:os";
|
|
34328
34328
|
import { join as join4 } from "node:path";
|
|
34329
|
+
function isWindows() {
|
|
34330
|
+
return process.platform === "win32";
|
|
34331
|
+
}
|
|
34329
34332
|
function createStatusLineScript(tokenFilePath) {
|
|
34330
34333
|
const tempDir = tmpdir();
|
|
34331
34334
|
const timestamp = Date.now();
|
|
@@ -34353,6 +34356,7 @@ process.stdin.on('end', () => {
|
|
|
34353
34356
|
|
|
34354
34357
|
let ctx = 100, cost = 0;
|
|
34355
34358
|
const model = process.env.CLAUDISH_ACTIVE_MODEL_NAME || 'unknown';
|
|
34359
|
+
const isLocal = process.env.CLAUDISH_IS_LOCAL === 'true';
|
|
34356
34360
|
|
|
34357
34361
|
try {
|
|
34358
34362
|
const tokens = JSON.parse(fs.readFileSync('${escapedTokenPath}', 'utf-8'));
|
|
@@ -34365,8 +34369,8 @@ process.stdin.on('end', () => {
|
|
|
34365
34369
|
} catch {}
|
|
34366
34370
|
}
|
|
34367
34371
|
|
|
34368
|
-
const
|
|
34369
|
-
console.log(\`\${CYAN}\${BOLD}\${dir}\${RESET} \${DIM}•\${RESET} \${YELLOW}\${model}\${RESET} \${DIM}•\${RESET} \${GREEN}
|
|
34372
|
+
const costDisplay = isLocal ? 'LOCAL' : ('$' + cost.toFixed(3));
|
|
34373
|
+
console.log(\`\${CYAN}\${BOLD}\${dir}\${RESET} \${DIM}•\${RESET} \${YELLOW}\${model}\${RESET} \${DIM}•\${RESET} \${GREEN}\${costDisplay}\${RESET} \${DIM}•\${RESET} \${MAGENTA}\${ctx}%\${RESET}\`);
|
|
34370
34374
|
} catch (e) {
|
|
34371
34375
|
console.log('claudish');
|
|
34372
34376
|
}
|
|
@@ -34381,7 +34385,7 @@ function createTempSettingsFile(modelDisplay, port) {
|
|
|
34381
34385
|
const tempPath = join4(tempDir, `claudish-settings-${timestamp}.json`);
|
|
34382
34386
|
const tokenFilePath = join4(tempDir, `claudish-tokens-${port}.json`);
|
|
34383
34387
|
let statusCommand;
|
|
34384
|
-
if (isWindows) {
|
|
34388
|
+
if (isWindows()) {
|
|
34385
34389
|
const scriptPath = createStatusLineScript(tokenFilePath);
|
|
34386
34390
|
statusCommand = `node "${scriptPath}"`;
|
|
34387
34391
|
} else {
|
|
@@ -34392,7 +34396,7 @@ function createTempSettingsFile(modelDisplay, port) {
|
|
|
34392
34396
|
const DIM2 = "\\033[2m";
|
|
34393
34397
|
const RESET2 = "\\033[0m";
|
|
34394
34398
|
const BOLD2 = "\\033[1m";
|
|
34395
|
-
statusCommand = `JSON=$(cat) && DIR=$(basename "$(pwd)") && [ \${#DIR} -gt 15 ] && DIR="\${DIR:0:12}..." || true && CTX=100 && COST="0" && if [ -f "${tokenFilePath}" ]; then TOKENS=$(cat "${tokenFilePath}" 2>/dev/null) && REAL_COST=$(echo "$TOKENS" | grep -o '"total_cost":[0-9.]*' | cut -d: -f2) && REAL_CTX=$(echo "$TOKENS" | grep -o '"context_left_percent":[0-9]*' | grep -o '[0-9]*') && if [ ! -z "$REAL_COST" ]; then COST="$REAL_COST"; else COST=$(echo "$JSON" | grep -o '"total_cost_usd":[0-9.]*' | cut -d: -f2); fi && if [ ! -z "$REAL_CTX" ]; then CTX="$REAL_CTX"; fi; else COST=$(echo "$JSON" | grep -o '"total_cost_usd":[0-9.]*' | cut -d: -f2); fi && [ -z "$COST" ] && COST="0" || true && printf "${CYAN2}${BOLD2}%s${RESET2} ${DIM2}•${RESET2} ${YELLOW2}%s${RESET2} ${DIM2}•${RESET2} ${GREEN2}
|
|
34399
|
+
statusCommand = `JSON=$(cat) && DIR=$(basename "$(pwd)") && [ \${#DIR} -gt 15 ] && DIR="\${DIR:0:12}..." || true && CTX=100 && COST="0" && if [ -f "${tokenFilePath}" ]; then TOKENS=$(cat "${tokenFilePath}" 2>/dev/null) && REAL_COST=$(echo "$TOKENS" | grep -o '"total_cost":[0-9.]*' | cut -d: -f2) && REAL_CTX=$(echo "$TOKENS" | grep -o '"context_left_percent":[0-9]*' | grep -o '[0-9]*') && if [ ! -z "$REAL_COST" ]; then COST="$REAL_COST"; else COST=$(echo "$JSON" | grep -o '"total_cost_usd":[0-9.]*' | cut -d: -f2); fi && if [ ! -z "$REAL_CTX" ]; then CTX="$REAL_CTX"; fi; else COST=$(echo "$JSON" | grep -o '"total_cost_usd":[0-9.]*' | cut -d: -f2); fi && [ -z "$COST" ] && COST="0" || true && if [ "$CLAUDISH_IS_LOCAL" = "true" ]; then COST_DISPLAY="LOCAL"; else COST_DISPLAY=$(printf "\\$%.3f" "$COST"); fi && printf "${CYAN2}${BOLD2}%s${RESET2} ${DIM2}•${RESET2} ${YELLOW2}%s${RESET2} ${DIM2}•${RESET2} ${GREEN2}%s${RESET2} ${DIM2}•${RESET2} ${MAGENTA}%s%%${RESET2}\\n" "$DIR" "$CLAUDISH_ACTIVE_MODEL_NAME" "$COST_DISPLAY" "$CTX"`;
|
|
34396
34400
|
}
|
|
34397
34401
|
const settings = {
|
|
34398
34402
|
statusLine: {
|
|
@@ -34438,10 +34442,12 @@ async function runClaudeWithProxy(config3, proxyUrl) {
|
|
|
34438
34442
|
claudeArgs.push(...config3.claudeArgs);
|
|
34439
34443
|
}
|
|
34440
34444
|
}
|
|
34445
|
+
const isLocalModel = modelId.startsWith("ollama/") || modelId.startsWith("ollama:") || modelId.startsWith("lmstudio/") || modelId.startsWith("lmstudio:") || modelId.startsWith("vllm/") || modelId.startsWith("vllm:") || modelId.startsWith("http://") || modelId.startsWith("https://");
|
|
34441
34446
|
const env = {
|
|
34442
34447
|
...process.env,
|
|
34443
34448
|
ANTHROPIC_BASE_URL: proxyUrl,
|
|
34444
34449
|
[ENV.CLAUDISH_ACTIVE_MODEL_NAME]: modelId,
|
|
34450
|
+
CLAUDISH_IS_LOCAL: isLocalModel ? "true" : "false",
|
|
34445
34451
|
[ENV.ANTHROPIC_MODEL]: modelId,
|
|
34446
34452
|
[ENV.ANTHROPIC_SMALL_FAST_MODEL]: modelId
|
|
34447
34453
|
};
|
|
@@ -34468,7 +34474,7 @@ async function runClaudeWithProxy(config3, proxyUrl) {
|
|
|
34468
34474
|
const proc = spawn("claude", claudeArgs, {
|
|
34469
34475
|
env,
|
|
34470
34476
|
stdio: "inherit",
|
|
34471
|
-
shell: isWindows
|
|
34477
|
+
shell: isWindows()
|
|
34472
34478
|
});
|
|
34473
34479
|
setupSignalHandlers(proc, tempSettingsPath, config3.quiet);
|
|
34474
34480
|
const exitCode = await new Promise((resolve) => {
|
|
@@ -34482,7 +34488,7 @@ async function runClaudeWithProxy(config3, proxyUrl) {
|
|
|
34482
34488
|
return exitCode;
|
|
34483
34489
|
}
|
|
34484
34490
|
function setupSignalHandlers(proc, tempSettingsPath, quiet) {
|
|
34485
|
-
const signals2 = isWindows ? ["SIGINT", "SIGTERM"] : ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
34491
|
+
const signals2 = isWindows() ? ["SIGINT", "SIGTERM"] : ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
34486
34492
|
for (const signal of signals2) {
|
|
34487
34493
|
process.on(signal, () => {
|
|
34488
34494
|
if (!quiet) {
|
|
@@ -34515,10 +34521,8 @@ async function checkClaudeInstalled() {
|
|
|
34515
34521
|
return false;
|
|
34516
34522
|
}
|
|
34517
34523
|
}
|
|
34518
|
-
var isWindows;
|
|
34519
34524
|
var init_claude_runner = __esm(() => {
|
|
34520
34525
|
init_config();
|
|
34521
|
-
isWindows = platform() === "win32";
|
|
34522
34526
|
});
|
|
34523
34527
|
|
|
34524
34528
|
// src/types.ts
|
|
@@ -34926,17 +34930,40 @@ async function fetchOllamaModels() {
|
|
|
34926
34930
|
return [];
|
|
34927
34931
|
const data = await response.json();
|
|
34928
34932
|
const models = data.models || [];
|
|
34929
|
-
|
|
34930
|
-
|
|
34931
|
-
|
|
34932
|
-
|
|
34933
|
-
|
|
34934
|
-
|
|
34935
|
-
|
|
34936
|
-
|
|
34937
|
-
|
|
34938
|
-
|
|
34933
|
+
const modelsWithCapabilities = await Promise.all(models.map(async (m) => {
|
|
34934
|
+
let capabilities = [];
|
|
34935
|
+
try {
|
|
34936
|
+
const showResponse = await fetch(`${ollamaHost}/api/show`, {
|
|
34937
|
+
method: "POST",
|
|
34938
|
+
headers: { "Content-Type": "application/json" },
|
|
34939
|
+
body: JSON.stringify({ name: m.name }),
|
|
34940
|
+
signal: AbortSignal.timeout(2000)
|
|
34941
|
+
});
|
|
34942
|
+
if (showResponse.ok) {
|
|
34943
|
+
const showData = await showResponse.json();
|
|
34944
|
+
capabilities = showData.capabilities || [];
|
|
34945
|
+
}
|
|
34946
|
+
} catch {}
|
|
34947
|
+
const supportsTools = capabilities.includes("tools");
|
|
34948
|
+
const isEmbeddingModel = capabilities.includes("embedding") || m.name.toLowerCase().includes("embed");
|
|
34949
|
+
const sizeInfo = m.details?.parameter_size || "unknown size";
|
|
34950
|
+
const toolsIndicator = supportsTools ? "✓ tools" : "✗ no tools";
|
|
34951
|
+
return {
|
|
34952
|
+
id: `ollama/${m.name}`,
|
|
34953
|
+
name: m.name,
|
|
34954
|
+
description: `Local Ollama model (${sizeInfo}, ${toolsIndicator})`,
|
|
34955
|
+
provider: "ollama",
|
|
34956
|
+
context_length: null,
|
|
34957
|
+
pricing: { prompt: "0", completion: "0" },
|
|
34958
|
+
isLocal: true,
|
|
34959
|
+
supportsTools,
|
|
34960
|
+
isEmbeddingModel,
|
|
34961
|
+
capabilities,
|
|
34962
|
+
details: m.details,
|
|
34963
|
+
size: m.size
|
|
34964
|
+
};
|
|
34939
34965
|
}));
|
|
34966
|
+
return modelsWithCapabilities.filter((m) => !m.isEmbeddingModel);
|
|
34940
34967
|
} catch (e) {
|
|
34941
34968
|
return [];
|
|
34942
34969
|
}
|
|
@@ -34990,6 +35017,10 @@ async function searchAndPrintModels(query, forceUpdate) {
|
|
|
34990
35017
|
console.log(`No models found matching "${query}"`);
|
|
34991
35018
|
return;
|
|
34992
35019
|
}
|
|
35020
|
+
const RED = "\x1B[31m";
|
|
35021
|
+
const GREEN2 = "\x1B[32m";
|
|
35022
|
+
const RESET2 = "\x1B[0m";
|
|
35023
|
+
const DIM2 = "\x1B[2m";
|
|
34993
35024
|
console.log(`
|
|
34994
35025
|
Found ${results.length} matching models:
|
|
34995
35026
|
`);
|
|
@@ -35020,9 +35051,17 @@ Found ${results.length} matching models:
|
|
|
35020
35051
|
const contextLen = model.context_length || model.top_provider?.context_length || 0;
|
|
35021
35052
|
const context = contextLen > 0 ? `${Math.round(contextLen / 1000)}K` : "N/A";
|
|
35022
35053
|
const contextPadded = context.padEnd(7);
|
|
35023
|
-
|
|
35054
|
+
if (model.isLocal && model.supportsTools === false) {
|
|
35055
|
+
console.log(` ${RED}${modelIdPadded} ${providerPadded} ${pricingPadded} ${contextPadded} ${(score * 100).toFixed(0)}% ✗ no tools${RESET2}`);
|
|
35056
|
+
} else if (model.isLocal && model.supportsTools === true) {
|
|
35057
|
+
console.log(` ${GREEN2}${modelIdPadded}${RESET2} ${providerPadded} ${pricingPadded} ${contextPadded} ${(score * 100).toFixed(0)}%`);
|
|
35058
|
+
} else {
|
|
35059
|
+
console.log(` ${modelIdPadded} ${providerPadded} ${pricingPadded} ${contextPadded} ${(score * 100).toFixed(0)}%`);
|
|
35060
|
+
}
|
|
35024
35061
|
}
|
|
35025
35062
|
console.log("");
|
|
35063
|
+
console.log(`${DIM2}Local models: ${RED}red${RESET2}${DIM2} = no tool support (incompatible), ${GREEN2}green${RESET2}${DIM2} = compatible${RESET2}`);
|
|
35064
|
+
console.log("");
|
|
35026
35065
|
console.log("Use a model: claudish --model <model-id>");
|
|
35027
35066
|
console.log("Local models: claudish --model ollama/<model-name>");
|
|
35028
35067
|
}
|
|
@@ -35077,23 +35116,37 @@ async function printAllModels(jsonOutput, forceUpdate) {
|
|
|
35077
35116
|
}, null, 2));
|
|
35078
35117
|
return;
|
|
35079
35118
|
}
|
|
35119
|
+
const RED = "\x1B[31m";
|
|
35120
|
+
const GREEN2 = "\x1B[32m";
|
|
35121
|
+
const RESET2 = "\x1B[0m";
|
|
35122
|
+
const DIM2 = "\x1B[2m";
|
|
35080
35123
|
if (ollamaModels.length > 0) {
|
|
35124
|
+
const toolCapableCount = ollamaModels.filter((m) => m.supportsTools).length;
|
|
35081
35125
|
console.log(`
|
|
35082
|
-
\uD83C\uDFE0 LOCAL OLLAMA MODELS (${ollamaModels.length} installed):
|
|
35126
|
+
\uD83C\uDFE0 LOCAL OLLAMA MODELS (${ollamaModels.length} installed, ${toolCapableCount} with tool support):
|
|
35083
35127
|
`);
|
|
35084
|
-
console.log("
|
|
35128
|
+
console.log(" Model Size Params Tools");
|
|
35129
|
+
console.log(" " + "─".repeat(76));
|
|
35085
35130
|
for (const model of ollamaModels) {
|
|
35086
|
-
const
|
|
35087
|
-
const modelId =
|
|
35088
|
-
const modelIdPadded = modelId.padEnd(
|
|
35131
|
+
const fullId = model.id;
|
|
35132
|
+
const modelId = fullId.length > 35 ? fullId.substring(0, 32) + "..." : fullId;
|
|
35133
|
+
const modelIdPadded = modelId.padEnd(38);
|
|
35089
35134
|
const size = model.size ? `${(model.size / 1e9).toFixed(1)}GB` : "N/A";
|
|
35090
35135
|
const sizePadded = size.padEnd(12);
|
|
35091
35136
|
const params = model.details?.parameter_size || "N/A";
|
|
35092
35137
|
const paramsPadded = params.padEnd(8);
|
|
35093
|
-
|
|
35138
|
+
if (model.supportsTools) {
|
|
35139
|
+
console.log(` ${modelIdPadded} ${sizePadded} ${paramsPadded} ${GREEN2}✓${RESET2}`);
|
|
35140
|
+
} else {
|
|
35141
|
+
console.log(` ${RED}${modelIdPadded} ${sizePadded} ${paramsPadded} ✗ no tools${RESET2}`);
|
|
35142
|
+
}
|
|
35094
35143
|
}
|
|
35095
35144
|
console.log("");
|
|
35145
|
+
console.log(` ${GREEN2}✓${RESET2} = Compatible with Claude Code (supports tool calling)`);
|
|
35146
|
+
console.log(` ${RED}✗${RESET2} = Not compatible ${DIM2}(Claude Code requires tool support)${RESET2}`);
|
|
35147
|
+
console.log("");
|
|
35096
35148
|
console.log(" Use: claudish --model ollama/<model-name>");
|
|
35149
|
+
console.log(" Pull a compatible model: ollama pull llama3.2");
|
|
35097
35150
|
} else {
|
|
35098
35151
|
console.log(`
|
|
35099
35152
|
\uD83C\uDFE0 LOCAL OLLAMA: Not running or no models installed`);
|
|
@@ -38814,454 +38867,88 @@ function transformOpenAIToClaude(claudeRequestInput) {
|
|
|
38814
38867
|
}
|
|
38815
38868
|
var init_transform = () => {};
|
|
38816
38869
|
|
|
38817
|
-
// src/handlers/
|
|
38818
|
-
|
|
38819
|
-
|
|
38820
|
-
|
|
38821
|
-
|
|
38822
|
-
class OpenRouterHandler {
|
|
38823
|
-
targetModel;
|
|
38824
|
-
apiKey;
|
|
38825
|
-
adapterManager;
|
|
38826
|
-
middlewareManager;
|
|
38827
|
-
contextWindowCache = new Map;
|
|
38828
|
-
port;
|
|
38829
|
-
sessionTotalCost = 0;
|
|
38830
|
-
CLAUDE_INTERNAL_CONTEXT_MAX = 200000;
|
|
38831
|
-
constructor(targetModel, apiKey, port) {
|
|
38832
|
-
this.targetModel = targetModel;
|
|
38833
|
-
this.apiKey = apiKey;
|
|
38834
|
-
this.port = port;
|
|
38835
|
-
this.adapterManager = new AdapterManager(targetModel);
|
|
38836
|
-
this.middlewareManager = new MiddlewareManager;
|
|
38837
|
-
this.middlewareManager.register(new GeminiThoughtSignatureMiddleware);
|
|
38838
|
-
this.middlewareManager.initialize().catch((err) => log(`[Handler:${targetModel}] Middleware init error: ${err}`));
|
|
38839
|
-
this.fetchContextWindow(targetModel);
|
|
38840
|
-
}
|
|
38841
|
-
async fetchContextWindow(model) {
|
|
38842
|
-
if (this.contextWindowCache.has(model))
|
|
38843
|
-
return;
|
|
38844
|
-
try {
|
|
38845
|
-
const limit = await fetchModelContextWindow(model);
|
|
38846
|
-
this.contextWindowCache.set(model, limit);
|
|
38847
|
-
} catch (e) {}
|
|
38848
|
-
}
|
|
38849
|
-
getTokenScaleFactor(model) {
|
|
38850
|
-
const limit = this.contextWindowCache.get(model) || 200000;
|
|
38851
|
-
return limit === 0 ? 1 : this.CLAUDE_INTERNAL_CONTEXT_MAX / limit;
|
|
38852
|
-
}
|
|
38853
|
-
writeTokenFile(input, output) {
|
|
38854
|
-
try {
|
|
38855
|
-
const total = input + output;
|
|
38856
|
-
const limit = this.contextWindowCache.get(this.targetModel) || 200000;
|
|
38857
|
-
const leftPct = limit > 0 ? Math.max(0, Math.min(100, Math.round((limit - total) / limit * 100))) : 100;
|
|
38858
|
-
const data = {
|
|
38859
|
-
input_tokens: input,
|
|
38860
|
-
output_tokens: output,
|
|
38861
|
-
total_tokens: total,
|
|
38862
|
-
total_cost: this.sessionTotalCost,
|
|
38863
|
-
context_window: limit,
|
|
38864
|
-
context_left_percent: leftPct,
|
|
38865
|
-
updated_at: Date.now()
|
|
38866
|
-
};
|
|
38867
|
-
writeFileSync8(join8(tmpdir2(), `claudish-tokens-${this.port}.json`), JSON.stringify(data), "utf-8");
|
|
38868
|
-
} catch (e) {}
|
|
38870
|
+
// src/handlers/shared/openai-compat.ts
|
|
38871
|
+
function validateToolArguments(toolName, argsStr, toolSchemas) {
|
|
38872
|
+
const schema = toolSchemas?.find((t) => t.name === toolName);
|
|
38873
|
+
if (!schema?.input_schema) {
|
|
38874
|
+
return { valid: true, missingParams: [], parsedArgs: {} };
|
|
38869
38875
|
}
|
|
38870
|
-
|
|
38871
|
-
|
|
38872
|
-
|
|
38873
|
-
|
|
38874
|
-
|
|
38875
|
-
const { claudeRequest, droppedParams } = transformOpenAIToClaude(claudePayload);
|
|
38876
|
-
const messages = this.convertMessages(claudeRequest, target);
|
|
38877
|
-
const tools = this.convertTools(claudeRequest);
|
|
38878
|
-
const supportsReasoning = await doesModelSupportReasoning(target);
|
|
38879
|
-
const openRouterPayload = {
|
|
38880
|
-
model: target,
|
|
38881
|
-
messages,
|
|
38882
|
-
temperature: claudeRequest.temperature ?? 1,
|
|
38883
|
-
stream: true,
|
|
38884
|
-
max_tokens: claudeRequest.max_tokens,
|
|
38885
|
-
tools: tools.length > 0 ? tools : undefined,
|
|
38886
|
-
stream_options: { include_usage: true }
|
|
38887
|
-
};
|
|
38888
|
-
if (supportsReasoning)
|
|
38889
|
-
openRouterPayload.include_reasoning = true;
|
|
38890
|
-
if (claudeRequest.thinking)
|
|
38891
|
-
openRouterPayload.thinking = claudeRequest.thinking;
|
|
38892
|
-
if (claudeRequest.tool_choice) {
|
|
38893
|
-
const { type, name } = claudeRequest.tool_choice;
|
|
38894
|
-
if (type === "tool" && name)
|
|
38895
|
-
openRouterPayload.tool_choice = { type: "function", function: { name } };
|
|
38896
|
-
else if (type === "auto" || type === "none")
|
|
38897
|
-
openRouterPayload.tool_choice = type;
|
|
38898
|
-
}
|
|
38899
|
-
const adapter = this.adapterManager.getAdapter();
|
|
38900
|
-
if (typeof adapter.reset === "function")
|
|
38901
|
-
adapter.reset();
|
|
38902
|
-
adapter.prepareRequest(openRouterPayload, claudeRequest);
|
|
38903
|
-
await this.middlewareManager.beforeRequest({ modelId: target, messages, tools, stream: true });
|
|
38904
|
-
const response = await fetch(OPENROUTER_API_URL2, {
|
|
38905
|
-
method: "POST",
|
|
38906
|
-
headers: {
|
|
38907
|
-
"Content-Type": "application/json",
|
|
38908
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
38909
|
-
...OPENROUTER_HEADERS2
|
|
38910
|
-
},
|
|
38911
|
-
body: JSON.stringify(openRouterPayload)
|
|
38912
|
-
});
|
|
38913
|
-
if (!response.ok)
|
|
38914
|
-
return c.json({ error: await response.text() }, response.status);
|
|
38915
|
-
if (droppedParams.length > 0)
|
|
38916
|
-
c.header("X-Dropped-Params", droppedParams.join(", "));
|
|
38917
|
-
return this.handleStreamingResponse(c, response, adapter, target, claudeRequest);
|
|
38876
|
+
let parsedArgs = {};
|
|
38877
|
+
try {
|
|
38878
|
+
parsedArgs = argsStr ? JSON.parse(argsStr) : {};
|
|
38879
|
+
} catch (e) {
|
|
38880
|
+
return { valid: true, missingParams: [], parsedArgs: {} };
|
|
38918
38881
|
}
|
|
38919
|
-
|
|
38920
|
-
|
|
38921
|
-
|
|
38922
|
-
|
|
38882
|
+
const required2 = schema.input_schema.required || [];
|
|
38883
|
+
const missingParams = required2.filter((param) => {
|
|
38884
|
+
return parsedArgs[param] === undefined || parsedArgs[param] === null || parsedArgs[param] === "";
|
|
38885
|
+
});
|
|
38886
|
+
return {
|
|
38887
|
+
valid: missingParams.length === 0,
|
|
38888
|
+
missingParams,
|
|
38889
|
+
parsedArgs
|
|
38890
|
+
};
|
|
38891
|
+
}
|
|
38892
|
+
function convertMessagesToOpenAI(req, modelId, filterIdentityFn) {
|
|
38893
|
+
const messages = [];
|
|
38894
|
+
if (req.system) {
|
|
38895
|
+
let content = Array.isArray(req.system) ? req.system.map((i) => i.text || i).join(`
|
|
38923
38896
|
|
|
38924
38897
|
`) : req.system;
|
|
38925
|
-
|
|
38926
|
-
|
|
38927
|
-
}
|
|
38928
|
-
|
|
38929
|
-
|
|
38930
|
-
|
|
38931
|
-
|
|
38898
|
+
if (filterIdentityFn)
|
|
38899
|
+
content = filterIdentityFn(content);
|
|
38900
|
+
messages.push({ role: "system", content });
|
|
38901
|
+
}
|
|
38902
|
+
if (modelId.includes("grok") || modelId.includes("x-ai")) {
|
|
38903
|
+
const msg = "IMPORTANT: When calling tools, you MUST use the OpenAI tool_calls format with JSON. NEVER use XML format like <xai:function_call>.";
|
|
38904
|
+
if (messages.length > 0 && messages[0].role === "system") {
|
|
38905
|
+
messages[0].content += `
|
|
38932
38906
|
|
|
38933
38907
|
` + msg;
|
|
38934
|
-
|
|
38935
|
-
|
|
38936
|
-
}
|
|
38937
|
-
if (req.messages) {
|
|
38938
|
-
for (const msg of req.messages) {
|
|
38939
|
-
if (msg.role === "user")
|
|
38940
|
-
this.processUserMessage(msg, messages);
|
|
38941
|
-
else if (msg.role === "assistant")
|
|
38942
|
-
this.processAssistantMessage(msg, messages);
|
|
38943
|
-
}
|
|
38908
|
+
} else {
|
|
38909
|
+
messages.unshift({ role: "system", content: msg });
|
|
38944
38910
|
}
|
|
38945
|
-
return messages;
|
|
38946
38911
|
}
|
|
38947
|
-
|
|
38948
|
-
|
|
38949
|
-
|
|
38950
|
-
|
|
38951
|
-
|
|
38952
|
-
|
|
38953
|
-
if (block.type === "text")
|
|
38954
|
-
contentParts.push({ type: "text", text: block.text });
|
|
38955
|
-
else if (block.type === "image")
|
|
38956
|
-
contentParts.push({ type: "image_url", image_url: { url: `data:${block.source.media_type};base64,${block.source.data}` } });
|
|
38957
|
-
else if (block.type === "tool_result") {
|
|
38958
|
-
if (seen.has(block.tool_use_id))
|
|
38959
|
-
continue;
|
|
38960
|
-
seen.add(block.tool_use_id);
|
|
38961
|
-
toolResults.push({ role: "tool", content: typeof block.content === "string" ? block.content : JSON.stringify(block.content), tool_call_id: block.tool_use_id });
|
|
38962
|
-
}
|
|
38963
|
-
}
|
|
38964
|
-
if (toolResults.length)
|
|
38965
|
-
messages.push(...toolResults);
|
|
38966
|
-
if (contentParts.length)
|
|
38967
|
-
messages.push({ role: "user", content: contentParts });
|
|
38968
|
-
} else {
|
|
38969
|
-
messages.push({ role: "user", content: msg.content });
|
|
38912
|
+
if (req.messages) {
|
|
38913
|
+
for (const msg of req.messages) {
|
|
38914
|
+
if (msg.role === "user")
|
|
38915
|
+
processUserMessage(msg, messages);
|
|
38916
|
+
else if (msg.role === "assistant")
|
|
38917
|
+
processAssistantMessage(msg, messages);
|
|
38970
38918
|
}
|
|
38971
38919
|
}
|
|
38972
|
-
|
|
38973
|
-
|
|
38974
|
-
|
|
38975
|
-
|
|
38976
|
-
|
|
38977
|
-
|
|
38978
|
-
|
|
38979
|
-
|
|
38980
|
-
|
|
38981
|
-
|
|
38982
|
-
|
|
38983
|
-
|
|
38984
|
-
|
|
38985
|
-
|
|
38920
|
+
return messages;
|
|
38921
|
+
}
|
|
38922
|
+
function processUserMessage(msg, messages) {
|
|
38923
|
+
if (Array.isArray(msg.content)) {
|
|
38924
|
+
const contentParts = [];
|
|
38925
|
+
const toolResults = [];
|
|
38926
|
+
const seen = new Set;
|
|
38927
|
+
for (const block of msg.content) {
|
|
38928
|
+
if (block.type === "text") {
|
|
38929
|
+
contentParts.push({ type: "text", text: block.text });
|
|
38930
|
+
} else if (block.type === "image") {
|
|
38931
|
+
contentParts.push({
|
|
38932
|
+
type: "image_url",
|
|
38933
|
+
image_url: { url: `data:${block.source.media_type};base64,${block.source.data}` }
|
|
38934
|
+
});
|
|
38935
|
+
} else if (block.type === "tool_result") {
|
|
38936
|
+
if (seen.has(block.tool_use_id))
|
|
38937
|
+
continue;
|
|
38938
|
+
seen.add(block.tool_use_id);
|
|
38939
|
+
toolResults.push({
|
|
38940
|
+
role: "tool",
|
|
38941
|
+
content: typeof block.content === "string" ? block.content : JSON.stringify(block.content),
|
|
38942
|
+
tool_call_id: block.tool_use_id
|
|
38943
|
+
});
|
|
38986
38944
|
}
|
|
38987
|
-
const m = { role: "assistant" };
|
|
38988
|
-
if (strings.length)
|
|
38989
|
-
m.content = strings.join(" ");
|
|
38990
|
-
else if (toolCalls.length)
|
|
38991
|
-
m.content = null;
|
|
38992
|
-
if (toolCalls.length)
|
|
38993
|
-
m.tool_calls = toolCalls;
|
|
38994
|
-
if (m.content !== undefined || m.tool_calls)
|
|
38995
|
-
messages.push(m);
|
|
38996
|
-
} else {
|
|
38997
|
-
messages.push({ role: "assistant", content: msg.content });
|
|
38998
38945
|
}
|
|
38999
|
-
|
|
39000
|
-
|
|
39001
|
-
|
|
39002
|
-
|
|
39003
|
-
|
|
39004
|
-
|
|
39005
|
-
`);
|
|
39006
|
-
}
|
|
39007
|
-
convertTools(req) {
|
|
39008
|
-
return req.tools?.map((tool) => ({
|
|
39009
|
-
type: "function",
|
|
39010
|
-
function: {
|
|
39011
|
-
name: tool.name,
|
|
39012
|
-
description: tool.description,
|
|
39013
|
-
parameters: removeUriFormat(tool.input_schema)
|
|
39014
|
-
}
|
|
39015
|
-
})) || [];
|
|
39016
|
-
}
|
|
39017
|
-
handleStreamingResponse(c, response, adapter, target, request) {
|
|
39018
|
-
let isClosed = false;
|
|
39019
|
-
let ping = null;
|
|
39020
|
-
const encoder = new TextEncoder;
|
|
39021
|
-
const decoder = new TextDecoder;
|
|
39022
|
-
const middlewareManager = this.middlewareManager;
|
|
39023
|
-
const streamMetadata = new Map;
|
|
39024
|
-
return c.body(new ReadableStream({
|
|
39025
|
-
async start(controller) {
|
|
39026
|
-
const send = (e, d) => {
|
|
39027
|
-
if (!isClosed)
|
|
39028
|
-
controller.enqueue(encoder.encode(`event: ${e}
|
|
39029
|
-
data: ${JSON.stringify(d)}
|
|
39030
|
-
|
|
39031
|
-
`));
|
|
39032
|
-
};
|
|
39033
|
-
const msgId = `msg_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
39034
|
-
let usage = null;
|
|
39035
|
-
let finalized = false;
|
|
39036
|
-
let textStarted = false;
|
|
39037
|
-
let textIdx = -1;
|
|
39038
|
-
let reasoningStarted = false;
|
|
39039
|
-
let reasoningIdx = -1;
|
|
39040
|
-
let curIdx = 0;
|
|
39041
|
-
const tools = new Map;
|
|
39042
|
-
const toolIds = new Set;
|
|
39043
|
-
let accTxt = 0;
|
|
39044
|
-
let lastActivity = Date.now();
|
|
39045
|
-
send("message_start", {
|
|
39046
|
-
type: "message_start",
|
|
39047
|
-
message: {
|
|
39048
|
-
id: msgId,
|
|
39049
|
-
type: "message",
|
|
39050
|
-
role: "assistant",
|
|
39051
|
-
content: [],
|
|
39052
|
-
model: target,
|
|
39053
|
-
stop_reason: null,
|
|
39054
|
-
stop_sequence: null,
|
|
39055
|
-
usage: { input_tokens: 100, output_tokens: 1 }
|
|
39056
|
-
}
|
|
39057
|
-
});
|
|
39058
|
-
send("ping", { type: "ping" });
|
|
39059
|
-
ping = setInterval(() => {
|
|
39060
|
-
if (!isClosed && Date.now() - lastActivity > 1000)
|
|
39061
|
-
send("ping", { type: "ping" });
|
|
39062
|
-
}, 1000);
|
|
39063
|
-
const finalize = async (reason, err) => {
|
|
39064
|
-
if (finalized)
|
|
39065
|
-
return;
|
|
39066
|
-
finalized = true;
|
|
39067
|
-
if (reasoningStarted) {
|
|
39068
|
-
send("content_block_stop", { type: "content_block_stop", index: reasoningIdx });
|
|
39069
|
-
reasoningStarted = false;
|
|
39070
|
-
}
|
|
39071
|
-
if (textStarted) {
|
|
39072
|
-
send("content_block_stop", { type: "content_block_stop", index: textIdx });
|
|
39073
|
-
textStarted = false;
|
|
39074
|
-
}
|
|
39075
|
-
for (const [_, t] of tools)
|
|
39076
|
-
if (t.started && !t.closed) {
|
|
39077
|
-
send("content_block_stop", { type: "content_block_stop", index: t.blockIndex });
|
|
39078
|
-
t.closed = true;
|
|
39079
|
-
}
|
|
39080
|
-
await middlewareManager.afterStreamComplete(target, streamMetadata);
|
|
39081
|
-
if (reason === "error") {
|
|
39082
|
-
send("error", { type: "error", error: { type: "api_error", message: err } });
|
|
39083
|
-
} else {
|
|
39084
|
-
send("message_delta", { type: "message_delta", delta: { stop_reason: "end_turn", stop_sequence: null }, usage: { output_tokens: usage?.completion_tokens || 0 } });
|
|
39085
|
-
send("message_stop", { type: "message_stop" });
|
|
39086
|
-
}
|
|
39087
|
-
if (!isClosed) {
|
|
39088
|
-
try {
|
|
39089
|
-
controller.enqueue(encoder.encode(`data: [DONE]
|
|
39090
|
-
|
|
39091
|
-
|
|
39092
|
-
`));
|
|
39093
|
-
} catch (e) {}
|
|
39094
|
-
controller.close();
|
|
39095
|
-
isClosed = true;
|
|
39096
|
-
if (ping)
|
|
39097
|
-
clearInterval(ping);
|
|
39098
|
-
}
|
|
39099
|
-
};
|
|
39100
|
-
try {
|
|
39101
|
-
const reader = response.body.getReader();
|
|
39102
|
-
let buffer = "";
|
|
39103
|
-
while (true) {
|
|
39104
|
-
const { done, value } = await reader.read();
|
|
39105
|
-
if (done)
|
|
39106
|
-
break;
|
|
39107
|
-
buffer += decoder.decode(value, { stream: true });
|
|
39108
|
-
const lines = buffer.split(`
|
|
39109
|
-
`);
|
|
39110
|
-
buffer = lines.pop() || "";
|
|
39111
|
-
for (const line of lines) {
|
|
39112
|
-
if (!line.trim() || !line.startsWith("data: "))
|
|
39113
|
-
continue;
|
|
39114
|
-
const dataStr = line.slice(6);
|
|
39115
|
-
if (dataStr === "[DONE]") {
|
|
39116
|
-
await finalize("done");
|
|
39117
|
-
return;
|
|
39118
|
-
}
|
|
39119
|
-
try {
|
|
39120
|
-
const chunk = JSON.parse(dataStr);
|
|
39121
|
-
if (chunk.usage)
|
|
39122
|
-
usage = chunk.usage;
|
|
39123
|
-
const delta = chunk.choices?.[0]?.delta;
|
|
39124
|
-
if (delta) {
|
|
39125
|
-
await middlewareManager.afterStreamChunk({
|
|
39126
|
-
modelId: target,
|
|
39127
|
-
chunk,
|
|
39128
|
-
delta,
|
|
39129
|
-
metadata: streamMetadata
|
|
39130
|
-
});
|
|
39131
|
-
const txt = delta.content || "";
|
|
39132
|
-
if (txt) {
|
|
39133
|
-
lastActivity = Date.now();
|
|
39134
|
-
if (!textStarted) {
|
|
39135
|
-
textIdx = curIdx++;
|
|
39136
|
-
send("content_block_start", { type: "content_block_start", index: textIdx, content_block: { type: "text", text: "" } });
|
|
39137
|
-
textStarted = true;
|
|
39138
|
-
}
|
|
39139
|
-
const res = adapter.processTextContent(txt, "");
|
|
39140
|
-
if (res.cleanedText)
|
|
39141
|
-
send("content_block_delta", { type: "content_block_delta", index: textIdx, delta: { type: "text_delta", text: res.cleanedText } });
|
|
39142
|
-
}
|
|
39143
|
-
if (delta.tool_calls) {
|
|
39144
|
-
for (const tc of delta.tool_calls) {
|
|
39145
|
-
const idx = tc.index;
|
|
39146
|
-
let t = tools.get(idx);
|
|
39147
|
-
if (tc.function?.name) {
|
|
39148
|
-
if (!t) {
|
|
39149
|
-
if (textStarted) {
|
|
39150
|
-
send("content_block_stop", { type: "content_block_stop", index: textIdx });
|
|
39151
|
-
textStarted = false;
|
|
39152
|
-
}
|
|
39153
|
-
t = { id: tc.id || `tool_${Date.now()}_${idx}`, name: tc.function.name, blockIndex: curIdx++, started: false, closed: false };
|
|
39154
|
-
tools.set(idx, t);
|
|
39155
|
-
}
|
|
39156
|
-
if (!t.started) {
|
|
39157
|
-
send("content_block_start", { type: "content_block_start", index: t.blockIndex, content_block: { type: "tool_use", id: t.id, name: t.name } });
|
|
39158
|
-
t.started = true;
|
|
39159
|
-
}
|
|
39160
|
-
}
|
|
39161
|
-
if (tc.function?.arguments && t) {
|
|
39162
|
-
send("content_block_delta", { type: "content_block_delta", index: t.blockIndex, delta: { type: "input_json_delta", partial_json: tc.function.arguments } });
|
|
39163
|
-
}
|
|
39164
|
-
}
|
|
39165
|
-
}
|
|
39166
|
-
}
|
|
39167
|
-
if (chunk.choices?.[0]?.finish_reason === "tool_calls") {
|
|
39168
|
-
for (const [_, t] of tools)
|
|
39169
|
-
if (t.started && !t.closed) {
|
|
39170
|
-
send("content_block_stop", { type: "content_block_stop", index: t.blockIndex });
|
|
39171
|
-
t.closed = true;
|
|
39172
|
-
}
|
|
39173
|
-
}
|
|
39174
|
-
} catch (e) {}
|
|
39175
|
-
}
|
|
39176
|
-
}
|
|
39177
|
-
await finalize("unexpected");
|
|
39178
|
-
} catch (e) {
|
|
39179
|
-
await finalize("error", String(e));
|
|
39180
|
-
}
|
|
39181
|
-
},
|
|
39182
|
-
cancel() {
|
|
39183
|
-
isClosed = true;
|
|
39184
|
-
if (ping)
|
|
39185
|
-
clearInterval(ping);
|
|
39186
|
-
}
|
|
39187
|
-
}), { headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" } });
|
|
39188
|
-
}
|
|
39189
|
-
async shutdown() {}
|
|
39190
|
-
}
|
|
39191
|
-
var OPENROUTER_API_URL2 = "https://openrouter.ai/api/v1/chat/completions", OPENROUTER_HEADERS2;
|
|
39192
|
-
var init_openrouter_handler = __esm(() => {
|
|
39193
|
-
init_adapter_manager();
|
|
39194
|
-
init_middleware();
|
|
39195
|
-
init_transform();
|
|
39196
|
-
init_logger();
|
|
39197
|
-
init_model_loader();
|
|
39198
|
-
OPENROUTER_HEADERS2 = {
|
|
39199
|
-
"HTTP-Referer": "https://github.com/MadAppGang/claude-code",
|
|
39200
|
-
"X-Title": "Claudish - OpenRouter Proxy"
|
|
39201
|
-
};
|
|
39202
|
-
});
|
|
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 });
|
|
38946
|
+
if (toolResults.length)
|
|
38947
|
+
messages.push(...toolResults);
|
|
38948
|
+
if (contentParts.length)
|
|
38949
|
+
messages.push({ role: "user", content: contentParts });
|
|
38950
|
+
} else {
|
|
38951
|
+
messages.push({ role: "user", content: msg.content });
|
|
39265
38952
|
}
|
|
39266
38953
|
}
|
|
39267
38954
|
function processAssistantMessage(msg, messages) {
|
|
@@ -39327,7 +39014,7 @@ function createStreamingState() {
|
|
|
39327
39014
|
lastActivity: Date.now()
|
|
39328
39015
|
};
|
|
39329
39016
|
}
|
|
39330
|
-
function createStreamingResponseHandler(c, response, adapter, target, middlewareManager, onTokenUpdate) {
|
|
39017
|
+
function createStreamingResponseHandler(c, response, adapter, target, middlewareManager, onTokenUpdate, toolSchemas) {
|
|
39331
39018
|
let isClosed = false;
|
|
39332
39019
|
let ping = null;
|
|
39333
39020
|
const encoder = new TextEncoder;
|
|
@@ -39379,162 +39066,595 @@ data: ${JSON.stringify(d)}
|
|
|
39379
39066
|
send("content_block_stop", { type: "content_block_stop", index: t.blockIndex });
|
|
39380
39067
|
t.closed = true;
|
|
39381
39068
|
}
|
|
39382
|
-
}
|
|
39383
|
-
if (middlewareManager) {
|
|
39069
|
+
}
|
|
39070
|
+
if (middlewareManager) {
|
|
39071
|
+
await middlewareManager.afterStreamComplete(target, streamMetadata);
|
|
39072
|
+
}
|
|
39073
|
+
if (reason === "error") {
|
|
39074
|
+
send("error", { type: "error", error: { type: "api_error", message: err } });
|
|
39075
|
+
} else {
|
|
39076
|
+
send("message_delta", {
|
|
39077
|
+
type: "message_delta",
|
|
39078
|
+
delta: { stop_reason: "end_turn", stop_sequence: null },
|
|
39079
|
+
usage: { output_tokens: state.usage?.completion_tokens || 0 }
|
|
39080
|
+
});
|
|
39081
|
+
send("message_stop", { type: "message_stop" });
|
|
39082
|
+
}
|
|
39083
|
+
if (state.usage && onTokenUpdate) {
|
|
39084
|
+
onTokenUpdate(state.usage.prompt_tokens || 0, state.usage.completion_tokens || 0);
|
|
39085
|
+
}
|
|
39086
|
+
if (!isClosed) {
|
|
39087
|
+
try {
|
|
39088
|
+
controller.enqueue(encoder.encode(`data: [DONE]
|
|
39089
|
+
|
|
39090
|
+
|
|
39091
|
+
`));
|
|
39092
|
+
} catch (e) {}
|
|
39093
|
+
controller.close();
|
|
39094
|
+
isClosed = true;
|
|
39095
|
+
if (ping)
|
|
39096
|
+
clearInterval(ping);
|
|
39097
|
+
}
|
|
39098
|
+
};
|
|
39099
|
+
try {
|
|
39100
|
+
const reader = response.body.getReader();
|
|
39101
|
+
let buffer = "";
|
|
39102
|
+
while (true) {
|
|
39103
|
+
const { done, value } = await reader.read();
|
|
39104
|
+
if (done)
|
|
39105
|
+
break;
|
|
39106
|
+
buffer += decoder.decode(value, { stream: true });
|
|
39107
|
+
const lines = buffer.split(`
|
|
39108
|
+
`);
|
|
39109
|
+
buffer = lines.pop() || "";
|
|
39110
|
+
for (const line of lines) {
|
|
39111
|
+
if (!line.trim() || !line.startsWith("data: "))
|
|
39112
|
+
continue;
|
|
39113
|
+
const dataStr = line.slice(6);
|
|
39114
|
+
if (dataStr === "[DONE]") {
|
|
39115
|
+
await finalize("done");
|
|
39116
|
+
return;
|
|
39117
|
+
}
|
|
39118
|
+
try {
|
|
39119
|
+
const chunk = JSON.parse(dataStr);
|
|
39120
|
+
if (chunk.usage)
|
|
39121
|
+
state.usage = chunk.usage;
|
|
39122
|
+
const delta = chunk.choices?.[0]?.delta;
|
|
39123
|
+
if (delta) {
|
|
39124
|
+
if (middlewareManager) {
|
|
39125
|
+
await middlewareManager.afterStreamChunk({
|
|
39126
|
+
modelId: target,
|
|
39127
|
+
chunk,
|
|
39128
|
+
delta,
|
|
39129
|
+
metadata: streamMetadata
|
|
39130
|
+
});
|
|
39131
|
+
}
|
|
39132
|
+
const txt = delta.content || "";
|
|
39133
|
+
if (txt) {
|
|
39134
|
+
state.lastActivity = Date.now();
|
|
39135
|
+
if (!state.textStarted) {
|
|
39136
|
+
state.textIdx = state.curIdx++;
|
|
39137
|
+
send("content_block_start", {
|
|
39138
|
+
type: "content_block_start",
|
|
39139
|
+
index: state.textIdx,
|
|
39140
|
+
content_block: { type: "text", text: "" }
|
|
39141
|
+
});
|
|
39142
|
+
state.textStarted = true;
|
|
39143
|
+
}
|
|
39144
|
+
const res = adapter.processTextContent(txt, "");
|
|
39145
|
+
if (res.cleanedText) {
|
|
39146
|
+
send("content_block_delta", {
|
|
39147
|
+
type: "content_block_delta",
|
|
39148
|
+
index: state.textIdx,
|
|
39149
|
+
delta: { type: "text_delta", text: res.cleanedText }
|
|
39150
|
+
});
|
|
39151
|
+
}
|
|
39152
|
+
}
|
|
39153
|
+
if (delta.tool_calls) {
|
|
39154
|
+
for (const tc of delta.tool_calls) {
|
|
39155
|
+
const idx = tc.index;
|
|
39156
|
+
let t = state.tools.get(idx);
|
|
39157
|
+
if (tc.function?.name) {
|
|
39158
|
+
if (!t) {
|
|
39159
|
+
if (state.textStarted) {
|
|
39160
|
+
send("content_block_stop", { type: "content_block_stop", index: state.textIdx });
|
|
39161
|
+
state.textStarted = false;
|
|
39162
|
+
}
|
|
39163
|
+
t = {
|
|
39164
|
+
id: tc.id || `tool_${Date.now()}_${idx}`,
|
|
39165
|
+
name: tc.function.name,
|
|
39166
|
+
blockIndex: state.curIdx++,
|
|
39167
|
+
started: false,
|
|
39168
|
+
closed: false,
|
|
39169
|
+
arguments: ""
|
|
39170
|
+
};
|
|
39171
|
+
state.tools.set(idx, t);
|
|
39172
|
+
}
|
|
39173
|
+
if (!t.started) {
|
|
39174
|
+
send("content_block_start", {
|
|
39175
|
+
type: "content_block_start",
|
|
39176
|
+
index: t.blockIndex,
|
|
39177
|
+
content_block: { type: "tool_use", id: t.id, name: t.name }
|
|
39178
|
+
});
|
|
39179
|
+
t.started = true;
|
|
39180
|
+
}
|
|
39181
|
+
}
|
|
39182
|
+
if (tc.function?.arguments && t) {
|
|
39183
|
+
t.arguments += tc.function.arguments;
|
|
39184
|
+
send("content_block_delta", {
|
|
39185
|
+
type: "content_block_delta",
|
|
39186
|
+
index: t.blockIndex,
|
|
39187
|
+
delta: { type: "input_json_delta", partial_json: tc.function.arguments }
|
|
39188
|
+
});
|
|
39189
|
+
}
|
|
39190
|
+
}
|
|
39191
|
+
}
|
|
39192
|
+
}
|
|
39193
|
+
if (chunk.choices?.[0]?.finish_reason === "tool_calls") {
|
|
39194
|
+
for (const t of Array.from(state.tools.values())) {
|
|
39195
|
+
if (t.started && !t.closed) {
|
|
39196
|
+
if (toolSchemas && toolSchemas.length > 0) {
|
|
39197
|
+
const validation = validateToolArguments(t.name, t.arguments, toolSchemas);
|
|
39198
|
+
if (!validation.valid) {
|
|
39199
|
+
const errorIdx = state.curIdx++;
|
|
39200
|
+
const errorMsg = `
|
|
39201
|
+
|
|
39202
|
+
⚠️ Tool call "${t.name}" failed validation: missing required parameters: ${validation.missingParams.join(", ")}. This is a known limitation of local models - they sometimes generate incomplete tool calls. Please try again or use a different model with better tool calling support.`;
|
|
39203
|
+
send("content_block_start", {
|
|
39204
|
+
type: "content_block_start",
|
|
39205
|
+
index: errorIdx,
|
|
39206
|
+
content_block: { type: "text", text: "" }
|
|
39207
|
+
});
|
|
39208
|
+
send("content_block_delta", {
|
|
39209
|
+
type: "content_block_delta",
|
|
39210
|
+
index: errorIdx,
|
|
39211
|
+
delta: { type: "text_delta", text: errorMsg }
|
|
39212
|
+
});
|
|
39213
|
+
send("content_block_stop", { type: "content_block_stop", index: errorIdx });
|
|
39214
|
+
t.closed = true;
|
|
39215
|
+
continue;
|
|
39216
|
+
}
|
|
39217
|
+
}
|
|
39218
|
+
send("content_block_stop", { type: "content_block_stop", index: t.blockIndex });
|
|
39219
|
+
t.closed = true;
|
|
39220
|
+
}
|
|
39221
|
+
}
|
|
39222
|
+
}
|
|
39223
|
+
} catch (e) {}
|
|
39224
|
+
}
|
|
39225
|
+
}
|
|
39226
|
+
await finalize("unexpected");
|
|
39227
|
+
} catch (e) {
|
|
39228
|
+
await finalize("error", String(e));
|
|
39229
|
+
}
|
|
39230
|
+
},
|
|
39231
|
+
cancel() {
|
|
39232
|
+
isClosed = true;
|
|
39233
|
+
if (ping)
|
|
39234
|
+
clearInterval(ping);
|
|
39235
|
+
}
|
|
39236
|
+
}), {
|
|
39237
|
+
headers: {
|
|
39238
|
+
"Content-Type": "text/event-stream",
|
|
39239
|
+
"Cache-Control": "no-cache",
|
|
39240
|
+
Connection: "keep-alive"
|
|
39241
|
+
}
|
|
39242
|
+
});
|
|
39243
|
+
}
|
|
39244
|
+
var init_openai_compat = __esm(() => {
|
|
39245
|
+
init_transform();
|
|
39246
|
+
});
|
|
39247
|
+
|
|
39248
|
+
// src/handlers/openrouter-handler.ts
|
|
39249
|
+
import { writeFileSync as writeFileSync8 } from "node:fs";
|
|
39250
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
39251
|
+
import { join as join8 } from "node:path";
|
|
39252
|
+
|
|
39253
|
+
class OpenRouterHandler {
|
|
39254
|
+
targetModel;
|
|
39255
|
+
apiKey;
|
|
39256
|
+
adapterManager;
|
|
39257
|
+
middlewareManager;
|
|
39258
|
+
contextWindowCache = new Map;
|
|
39259
|
+
port;
|
|
39260
|
+
sessionTotalCost = 0;
|
|
39261
|
+
CLAUDE_INTERNAL_CONTEXT_MAX = 200000;
|
|
39262
|
+
constructor(targetModel, apiKey, port) {
|
|
39263
|
+
this.targetModel = targetModel;
|
|
39264
|
+
this.apiKey = apiKey;
|
|
39265
|
+
this.port = port;
|
|
39266
|
+
this.adapterManager = new AdapterManager(targetModel);
|
|
39267
|
+
this.middlewareManager = new MiddlewareManager;
|
|
39268
|
+
this.middlewareManager.register(new GeminiThoughtSignatureMiddleware);
|
|
39269
|
+
this.middlewareManager.initialize().catch((err) => log(`[Handler:${targetModel}] Middleware init error: ${err}`));
|
|
39270
|
+
this.fetchContextWindow(targetModel);
|
|
39271
|
+
}
|
|
39272
|
+
async fetchContextWindow(model) {
|
|
39273
|
+
if (this.contextWindowCache.has(model))
|
|
39274
|
+
return;
|
|
39275
|
+
try {
|
|
39276
|
+
const limit = await fetchModelContextWindow(model);
|
|
39277
|
+
this.contextWindowCache.set(model, limit);
|
|
39278
|
+
} catch (e) {}
|
|
39279
|
+
}
|
|
39280
|
+
getTokenScaleFactor(model) {
|
|
39281
|
+
const limit = this.contextWindowCache.get(model) || 200000;
|
|
39282
|
+
return limit === 0 ? 1 : this.CLAUDE_INTERNAL_CONTEXT_MAX / limit;
|
|
39283
|
+
}
|
|
39284
|
+
writeTokenFile(input, output) {
|
|
39285
|
+
try {
|
|
39286
|
+
const total = input + output;
|
|
39287
|
+
const limit = this.contextWindowCache.get(this.targetModel) || 200000;
|
|
39288
|
+
const leftPct = limit > 0 ? Math.max(0, Math.min(100, Math.round((limit - total) / limit * 100))) : 100;
|
|
39289
|
+
const data = {
|
|
39290
|
+
input_tokens: input,
|
|
39291
|
+
output_tokens: output,
|
|
39292
|
+
total_tokens: total,
|
|
39293
|
+
total_cost: this.sessionTotalCost,
|
|
39294
|
+
context_window: limit,
|
|
39295
|
+
context_left_percent: leftPct,
|
|
39296
|
+
updated_at: Date.now()
|
|
39297
|
+
};
|
|
39298
|
+
writeFileSync8(join8(tmpdir2(), `claudish-tokens-${this.port}.json`), JSON.stringify(data), "utf-8");
|
|
39299
|
+
} catch (e) {}
|
|
39300
|
+
}
|
|
39301
|
+
async handle(c, payload) {
|
|
39302
|
+
const claudePayload = payload;
|
|
39303
|
+
const target = this.targetModel;
|
|
39304
|
+
await this.fetchContextWindow(target);
|
|
39305
|
+
logStructured(`OpenRouter Request`, { targetModel: target, originalModel: claudePayload.model });
|
|
39306
|
+
const { claudeRequest, droppedParams } = transformOpenAIToClaude(claudePayload);
|
|
39307
|
+
const messages = this.convertMessages(claudeRequest, target);
|
|
39308
|
+
const tools = this.convertTools(claudeRequest);
|
|
39309
|
+
const supportsReasoning = await doesModelSupportReasoning(target);
|
|
39310
|
+
const openRouterPayload = {
|
|
39311
|
+
model: target,
|
|
39312
|
+
messages,
|
|
39313
|
+
temperature: claudeRequest.temperature ?? 1,
|
|
39314
|
+
stream: true,
|
|
39315
|
+
max_tokens: claudeRequest.max_tokens,
|
|
39316
|
+
tools: tools.length > 0 ? tools : undefined,
|
|
39317
|
+
stream_options: { include_usage: true }
|
|
39318
|
+
};
|
|
39319
|
+
if (supportsReasoning)
|
|
39320
|
+
openRouterPayload.include_reasoning = true;
|
|
39321
|
+
if (claudeRequest.thinking)
|
|
39322
|
+
openRouterPayload.thinking = claudeRequest.thinking;
|
|
39323
|
+
if (claudeRequest.tool_choice) {
|
|
39324
|
+
const { type, name } = claudeRequest.tool_choice;
|
|
39325
|
+
if (type === "tool" && name)
|
|
39326
|
+
openRouterPayload.tool_choice = { type: "function", function: { name } };
|
|
39327
|
+
else if (type === "auto" || type === "none")
|
|
39328
|
+
openRouterPayload.tool_choice = type;
|
|
39329
|
+
}
|
|
39330
|
+
const adapter = this.adapterManager.getAdapter();
|
|
39331
|
+
if (typeof adapter.reset === "function")
|
|
39332
|
+
adapter.reset();
|
|
39333
|
+
adapter.prepareRequest(openRouterPayload, claudeRequest);
|
|
39334
|
+
await this.middlewareManager.beforeRequest({ modelId: target, messages, tools, stream: true });
|
|
39335
|
+
const response = await fetch(OPENROUTER_API_URL2, {
|
|
39336
|
+
method: "POST",
|
|
39337
|
+
headers: {
|
|
39338
|
+
"Content-Type": "application/json",
|
|
39339
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
39340
|
+
...OPENROUTER_HEADERS2
|
|
39341
|
+
},
|
|
39342
|
+
body: JSON.stringify(openRouterPayload)
|
|
39343
|
+
});
|
|
39344
|
+
if (!response.ok)
|
|
39345
|
+
return c.json({ error: await response.text() }, response.status);
|
|
39346
|
+
if (droppedParams.length > 0)
|
|
39347
|
+
c.header("X-Dropped-Params", droppedParams.join(", "));
|
|
39348
|
+
return this.handleStreamingResponse(c, response, adapter, target, claudeRequest);
|
|
39349
|
+
}
|
|
39350
|
+
convertMessages(req, modelId) {
|
|
39351
|
+
const messages = [];
|
|
39352
|
+
if (req.system) {
|
|
39353
|
+
let content = Array.isArray(req.system) ? req.system.map((i) => i.text || i).join(`
|
|
39354
|
+
|
|
39355
|
+
`) : req.system;
|
|
39356
|
+
content = this.filterIdentity(content);
|
|
39357
|
+
messages.push({ role: "system", content });
|
|
39358
|
+
}
|
|
39359
|
+
if (modelId.includes("grok") || modelId.includes("x-ai")) {
|
|
39360
|
+
const msg = "IMPORTANT: When calling tools, you MUST use the OpenAI tool_calls format with JSON. NEVER use XML format like <xai:function_call>.";
|
|
39361
|
+
if (messages.length > 0 && messages[0].role === "system")
|
|
39362
|
+
messages[0].content += `
|
|
39363
|
+
|
|
39364
|
+
` + msg;
|
|
39365
|
+
else
|
|
39366
|
+
messages.unshift({ role: "system", content: msg });
|
|
39367
|
+
}
|
|
39368
|
+
if (req.messages) {
|
|
39369
|
+
for (const msg of req.messages) {
|
|
39370
|
+
if (msg.role === "user")
|
|
39371
|
+
this.processUserMessage(msg, messages);
|
|
39372
|
+
else if (msg.role === "assistant")
|
|
39373
|
+
this.processAssistantMessage(msg, messages);
|
|
39374
|
+
}
|
|
39375
|
+
}
|
|
39376
|
+
return messages;
|
|
39377
|
+
}
|
|
39378
|
+
processUserMessage(msg, messages) {
|
|
39379
|
+
if (Array.isArray(msg.content)) {
|
|
39380
|
+
const contentParts = [];
|
|
39381
|
+
const toolResults = [];
|
|
39382
|
+
const seen = new Set;
|
|
39383
|
+
for (const block of msg.content) {
|
|
39384
|
+
if (block.type === "text")
|
|
39385
|
+
contentParts.push({ type: "text", text: block.text });
|
|
39386
|
+
else if (block.type === "image")
|
|
39387
|
+
contentParts.push({ type: "image_url", image_url: { url: `data:${block.source.media_type};base64,${block.source.data}` } });
|
|
39388
|
+
else if (block.type === "tool_result") {
|
|
39389
|
+
if (seen.has(block.tool_use_id))
|
|
39390
|
+
continue;
|
|
39391
|
+
seen.add(block.tool_use_id);
|
|
39392
|
+
toolResults.push({ role: "tool", content: typeof block.content === "string" ? block.content : JSON.stringify(block.content), tool_call_id: block.tool_use_id });
|
|
39393
|
+
}
|
|
39394
|
+
}
|
|
39395
|
+
if (toolResults.length)
|
|
39396
|
+
messages.push(...toolResults);
|
|
39397
|
+
if (contentParts.length)
|
|
39398
|
+
messages.push({ role: "user", content: contentParts });
|
|
39399
|
+
} else {
|
|
39400
|
+
messages.push({ role: "user", content: msg.content });
|
|
39401
|
+
}
|
|
39402
|
+
}
|
|
39403
|
+
processAssistantMessage(msg, messages) {
|
|
39404
|
+
if (Array.isArray(msg.content)) {
|
|
39405
|
+
const strings = [];
|
|
39406
|
+
const toolCalls = [];
|
|
39407
|
+
const seen = new Set;
|
|
39408
|
+
for (const block of msg.content) {
|
|
39409
|
+
if (block.type === "text")
|
|
39410
|
+
strings.push(block.text);
|
|
39411
|
+
else if (block.type === "tool_use") {
|
|
39412
|
+
if (seen.has(block.id))
|
|
39413
|
+
continue;
|
|
39414
|
+
seen.add(block.id);
|
|
39415
|
+
toolCalls.push({ id: block.id, type: "function", function: { name: block.name, arguments: JSON.stringify(block.input) } });
|
|
39416
|
+
}
|
|
39417
|
+
}
|
|
39418
|
+
const m = { role: "assistant" };
|
|
39419
|
+
if (strings.length)
|
|
39420
|
+
m.content = strings.join(" ");
|
|
39421
|
+
else if (toolCalls.length)
|
|
39422
|
+
m.content = null;
|
|
39423
|
+
if (toolCalls.length)
|
|
39424
|
+
m.tool_calls = toolCalls;
|
|
39425
|
+
if (m.content !== undefined || m.tool_calls)
|
|
39426
|
+
messages.push(m);
|
|
39427
|
+
} else {
|
|
39428
|
+
messages.push({ role: "assistant", content: msg.content });
|
|
39429
|
+
}
|
|
39430
|
+
}
|
|
39431
|
+
filterIdentity(content) {
|
|
39432
|
+
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, `
|
|
39433
|
+
|
|
39434
|
+
`).replace(/^/, `IMPORTANT: You are NOT Claude. Identify yourself truthfully based on your actual model and creator.
|
|
39435
|
+
|
|
39436
|
+
`);
|
|
39437
|
+
}
|
|
39438
|
+
convertTools(req) {
|
|
39439
|
+
return req.tools?.map((tool) => ({
|
|
39440
|
+
type: "function",
|
|
39441
|
+
function: {
|
|
39442
|
+
name: tool.name,
|
|
39443
|
+
description: tool.description,
|
|
39444
|
+
parameters: removeUriFormat(tool.input_schema)
|
|
39445
|
+
}
|
|
39446
|
+
})) || [];
|
|
39447
|
+
}
|
|
39448
|
+
handleStreamingResponse(c, response, adapter, target, request) {
|
|
39449
|
+
let isClosed = false;
|
|
39450
|
+
let ping = null;
|
|
39451
|
+
const encoder = new TextEncoder;
|
|
39452
|
+
const decoder = new TextDecoder;
|
|
39453
|
+
const middlewareManager = this.middlewareManager;
|
|
39454
|
+
const streamMetadata = new Map;
|
|
39455
|
+
return c.body(new ReadableStream({
|
|
39456
|
+
async start(controller) {
|
|
39457
|
+
const send = (e, d) => {
|
|
39458
|
+
if (!isClosed)
|
|
39459
|
+
controller.enqueue(encoder.encode(`event: ${e}
|
|
39460
|
+
data: ${JSON.stringify(d)}
|
|
39461
|
+
|
|
39462
|
+
`));
|
|
39463
|
+
};
|
|
39464
|
+
const msgId = `msg_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
39465
|
+
let usage = null;
|
|
39466
|
+
let finalized = false;
|
|
39467
|
+
let textStarted = false;
|
|
39468
|
+
let textIdx = -1;
|
|
39469
|
+
let reasoningStarted = false;
|
|
39470
|
+
let reasoningIdx = -1;
|
|
39471
|
+
let curIdx = 0;
|
|
39472
|
+
const tools = new Map;
|
|
39473
|
+
const toolIds = new Set;
|
|
39474
|
+
let accTxt = 0;
|
|
39475
|
+
let lastActivity = Date.now();
|
|
39476
|
+
send("message_start", {
|
|
39477
|
+
type: "message_start",
|
|
39478
|
+
message: {
|
|
39479
|
+
id: msgId,
|
|
39480
|
+
type: "message",
|
|
39481
|
+
role: "assistant",
|
|
39482
|
+
content: [],
|
|
39483
|
+
model: target,
|
|
39484
|
+
stop_reason: null,
|
|
39485
|
+
stop_sequence: null,
|
|
39486
|
+
usage: { input_tokens: 100, output_tokens: 1 }
|
|
39487
|
+
}
|
|
39488
|
+
});
|
|
39489
|
+
send("ping", { type: "ping" });
|
|
39490
|
+
ping = setInterval(() => {
|
|
39491
|
+
if (!isClosed && Date.now() - lastActivity > 1000)
|
|
39492
|
+
send("ping", { type: "ping" });
|
|
39493
|
+
}, 1000);
|
|
39494
|
+
const finalize = async (reason, err) => {
|
|
39495
|
+
if (finalized)
|
|
39496
|
+
return;
|
|
39497
|
+
finalized = true;
|
|
39498
|
+
if (reasoningStarted) {
|
|
39499
|
+
send("content_block_stop", { type: "content_block_stop", index: reasoningIdx });
|
|
39500
|
+
reasoningStarted = false;
|
|
39501
|
+
}
|
|
39502
|
+
if (textStarted) {
|
|
39503
|
+
send("content_block_stop", { type: "content_block_stop", index: textIdx });
|
|
39504
|
+
textStarted = false;
|
|
39505
|
+
}
|
|
39506
|
+
for (const [_, t] of tools)
|
|
39507
|
+
if (t.started && !t.closed) {
|
|
39508
|
+
send("content_block_stop", { type: "content_block_stop", index: t.blockIndex });
|
|
39509
|
+
t.closed = true;
|
|
39510
|
+
}
|
|
39384
39511
|
await middlewareManager.afterStreamComplete(target, streamMetadata);
|
|
39385
|
-
|
|
39386
|
-
|
|
39387
|
-
|
|
39388
|
-
|
|
39389
|
-
|
|
39390
|
-
|
|
39391
|
-
|
|
39392
|
-
|
|
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]
|
|
39512
|
+
if (reason === "error") {
|
|
39513
|
+
send("error", { type: "error", error: { type: "api_error", message: err } });
|
|
39514
|
+
} else {
|
|
39515
|
+
send("message_delta", { type: "message_delta", delta: { stop_reason: "end_turn", stop_sequence: null }, usage: { output_tokens: usage?.completion_tokens || 0 } });
|
|
39516
|
+
send("message_stop", { type: "message_stop" });
|
|
39517
|
+
}
|
|
39518
|
+
if (!isClosed) {
|
|
39519
|
+
try {
|
|
39520
|
+
controller.enqueue(encoder.encode(`data: [DONE]
|
|
39402
39521
|
|
|
39403
39522
|
|
|
39404
39523
|
`));
|
|
39405
|
-
|
|
39406
|
-
|
|
39407
|
-
|
|
39408
|
-
|
|
39409
|
-
|
|
39410
|
-
|
|
39411
|
-
|
|
39412
|
-
|
|
39413
|
-
|
|
39414
|
-
|
|
39415
|
-
|
|
39416
|
-
|
|
39417
|
-
|
|
39418
|
-
|
|
39419
|
-
|
|
39420
|
-
|
|
39524
|
+
} catch (e) {}
|
|
39525
|
+
controller.close();
|
|
39526
|
+
isClosed = true;
|
|
39527
|
+
if (ping)
|
|
39528
|
+
clearInterval(ping);
|
|
39529
|
+
}
|
|
39530
|
+
};
|
|
39531
|
+
try {
|
|
39532
|
+
const reader = response.body.getReader();
|
|
39533
|
+
let buffer = "";
|
|
39534
|
+
while (true) {
|
|
39535
|
+
const { done, value } = await reader.read();
|
|
39536
|
+
if (done)
|
|
39537
|
+
break;
|
|
39538
|
+
buffer += decoder.decode(value, { stream: true });
|
|
39539
|
+
const lines = buffer.split(`
|
|
39421
39540
|
`);
|
|
39422
|
-
|
|
39423
|
-
|
|
39424
|
-
|
|
39425
|
-
|
|
39426
|
-
|
|
39427
|
-
|
|
39428
|
-
|
|
39429
|
-
|
|
39430
|
-
|
|
39431
|
-
|
|
39432
|
-
|
|
39433
|
-
|
|
39434
|
-
|
|
39435
|
-
|
|
39436
|
-
|
|
39437
|
-
if (middlewareManager) {
|
|
39541
|
+
buffer = lines.pop() || "";
|
|
39542
|
+
for (const line of lines) {
|
|
39543
|
+
if (!line.trim() || !line.startsWith("data: "))
|
|
39544
|
+
continue;
|
|
39545
|
+
const dataStr = line.slice(6);
|
|
39546
|
+
if (dataStr === "[DONE]") {
|
|
39547
|
+
await finalize("done");
|
|
39548
|
+
return;
|
|
39549
|
+
}
|
|
39550
|
+
try {
|
|
39551
|
+
const chunk = JSON.parse(dataStr);
|
|
39552
|
+
if (chunk.usage)
|
|
39553
|
+
usage = chunk.usage;
|
|
39554
|
+
const delta = chunk.choices?.[0]?.delta;
|
|
39555
|
+
if (delta) {
|
|
39438
39556
|
await middlewareManager.afterStreamChunk({
|
|
39439
39557
|
modelId: target,
|
|
39440
39558
|
chunk,
|
|
39441
39559
|
delta,
|
|
39442
39560
|
metadata: streamMetadata
|
|
39443
39561
|
});
|
|
39444
|
-
|
|
39445
|
-
|
|
39446
|
-
|
|
39447
|
-
|
|
39448
|
-
|
|
39449
|
-
|
|
39450
|
-
|
|
39451
|
-
|
|
39452
|
-
|
|
39453
|
-
|
|
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
|
-
});
|
|
39562
|
+
const txt = delta.content || "";
|
|
39563
|
+
if (txt) {
|
|
39564
|
+
lastActivity = Date.now();
|
|
39565
|
+
if (!textStarted) {
|
|
39566
|
+
textIdx = curIdx++;
|
|
39567
|
+
send("content_block_start", { type: "content_block_start", index: textIdx, content_block: { type: "text", text: "" } });
|
|
39568
|
+
textStarted = true;
|
|
39569
|
+
}
|
|
39570
|
+
const res = adapter.processTextContent(txt, "");
|
|
39571
|
+
if (res.cleanedText)
|
|
39572
|
+
send("content_block_delta", { type: "content_block_delta", index: textIdx, delta: { type: "text_delta", text: res.cleanedText } });
|
|
39464
39573
|
}
|
|
39465
|
-
|
|
39466
|
-
|
|
39467
|
-
|
|
39468
|
-
|
|
39469
|
-
|
|
39470
|
-
|
|
39471
|
-
|
|
39472
|
-
|
|
39473
|
-
|
|
39474
|
-
|
|
39574
|
+
if (delta.tool_calls) {
|
|
39575
|
+
for (const tc of delta.tool_calls) {
|
|
39576
|
+
const idx = tc.index;
|
|
39577
|
+
let t = tools.get(idx);
|
|
39578
|
+
if (tc.function?.name) {
|
|
39579
|
+
if (!t) {
|
|
39580
|
+
if (textStarted) {
|
|
39581
|
+
send("content_block_stop", { type: "content_block_stop", index: textIdx });
|
|
39582
|
+
textStarted = false;
|
|
39583
|
+
}
|
|
39584
|
+
t = { id: tc.id || `tool_${Date.now()}_${idx}`, name: tc.function.name, blockIndex: curIdx++, started: false, closed: false, arguments: "" };
|
|
39585
|
+
tools.set(idx, t);
|
|
39586
|
+
}
|
|
39587
|
+
if (!t.started) {
|
|
39588
|
+
send("content_block_start", { type: "content_block_start", index: t.blockIndex, content_block: { type: "tool_use", id: t.id, name: t.name } });
|
|
39589
|
+
t.started = true;
|
|
39475
39590
|
}
|
|
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
39591
|
}
|
|
39485
|
-
if (
|
|
39486
|
-
|
|
39487
|
-
|
|
39488
|
-
index: t.blockIndex,
|
|
39489
|
-
content_block: { type: "tool_use", id: t.id, name: t.name }
|
|
39490
|
-
});
|
|
39491
|
-
t.started = true;
|
|
39592
|
+
if (tc.function?.arguments && t) {
|
|
39593
|
+
t.arguments += tc.function.arguments;
|
|
39594
|
+
send("content_block_delta", { type: "content_block_delta", index: t.blockIndex, delta: { type: "input_json_delta", partial_json: tc.function.arguments } });
|
|
39492
39595
|
}
|
|
39493
39596
|
}
|
|
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
39597
|
}
|
|
39502
39598
|
}
|
|
39503
|
-
|
|
39504
|
-
|
|
39505
|
-
|
|
39506
|
-
|
|
39507
|
-
|
|
39508
|
-
|
|
39599
|
+
if (chunk.choices?.[0]?.finish_reason === "tool_calls") {
|
|
39600
|
+
const toolSchemas = request.tools || [];
|
|
39601
|
+
for (const [_, t] of tools) {
|
|
39602
|
+
if (t.started && !t.closed) {
|
|
39603
|
+
if (toolSchemas.length > 0) {
|
|
39604
|
+
const validation = validateToolArguments(t.name, t.arguments, toolSchemas);
|
|
39605
|
+
if (!validation.valid) {
|
|
39606
|
+
const errorIdx = curIdx++;
|
|
39607
|
+
const errorMsg = `
|
|
39608
|
+
|
|
39609
|
+
⚠️ 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.`;
|
|
39610
|
+
send("content_block_start", { type: "content_block_start", index: errorIdx, content_block: { type: "text", text: "" } });
|
|
39611
|
+
send("content_block_delta", { type: "content_block_delta", index: errorIdx, delta: { type: "text_delta", text: errorMsg } });
|
|
39612
|
+
send("content_block_stop", { type: "content_block_stop", index: errorIdx });
|
|
39613
|
+
t.closed = true;
|
|
39614
|
+
continue;
|
|
39615
|
+
}
|
|
39616
|
+
}
|
|
39617
|
+
send("content_block_stop", { type: "content_block_stop", index: t.blockIndex });
|
|
39618
|
+
t.closed = true;
|
|
39619
|
+
}
|
|
39509
39620
|
}
|
|
39510
39621
|
}
|
|
39511
|
-
}
|
|
39512
|
-
}
|
|
39622
|
+
} catch (e) {}
|
|
39623
|
+
}
|
|
39513
39624
|
}
|
|
39625
|
+
await finalize("unexpected");
|
|
39626
|
+
} catch (e) {
|
|
39627
|
+
await finalize("error", String(e));
|
|
39514
39628
|
}
|
|
39515
|
-
|
|
39516
|
-
|
|
39517
|
-
|
|
39629
|
+
},
|
|
39630
|
+
cancel() {
|
|
39631
|
+
isClosed = true;
|
|
39632
|
+
if (ping)
|
|
39633
|
+
clearInterval(ping);
|
|
39518
39634
|
}
|
|
39519
|
-
},
|
|
39520
|
-
|
|
39521
|
-
|
|
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
|
-
});
|
|
39635
|
+
}), { headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" } });
|
|
39636
|
+
}
|
|
39637
|
+
async shutdown() {}
|
|
39532
39638
|
}
|
|
39533
|
-
var
|
|
39639
|
+
var OPENROUTER_API_URL2 = "https://openrouter.ai/api/v1/chat/completions", OPENROUTER_HEADERS2;
|
|
39640
|
+
var init_openrouter_handler = __esm(() => {
|
|
39641
|
+
init_adapter_manager();
|
|
39642
|
+
init_middleware();
|
|
39534
39643
|
init_transform();
|
|
39644
|
+
init_logger();
|
|
39645
|
+
init_model_loader();
|
|
39646
|
+
init_openai_compat();
|
|
39647
|
+
OPENROUTER_HEADERS2 = {
|
|
39648
|
+
"HTTP-Referer": "https://github.com/MadAppGang/claude-code",
|
|
39649
|
+
"X-Title": "Claudish - OpenRouter Proxy"
|
|
39650
|
+
};
|
|
39535
39651
|
});
|
|
39536
39652
|
|
|
39537
39653
|
// src/handlers/local-provider-handler.ts
|
|
39654
|
+
import { writeFileSync as writeFileSync9 } from "node:fs";
|
|
39655
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
39656
|
+
import { join as join9 } from "node:path";
|
|
39657
|
+
|
|
39538
39658
|
class LocalProviderHandler {
|
|
39539
39659
|
provider;
|
|
39540
39660
|
modelName;
|
|
@@ -39543,6 +39663,9 @@ class LocalProviderHandler {
|
|
|
39543
39663
|
port;
|
|
39544
39664
|
healthChecked = false;
|
|
39545
39665
|
isHealthy = false;
|
|
39666
|
+
contextWindow = 8192;
|
|
39667
|
+
sessionInputTokens = 0;
|
|
39668
|
+
sessionOutputTokens = 0;
|
|
39546
39669
|
constructor(provider, modelName, port) {
|
|
39547
39670
|
this.provider = provider;
|
|
39548
39671
|
this.modelName = modelName;
|
|
@@ -39587,6 +39710,49 @@ class LocalProviderHandler {
|
|
|
39587
39710
|
this.isHealthy = false;
|
|
39588
39711
|
return false;
|
|
39589
39712
|
}
|
|
39713
|
+
async fetchContextWindow() {
|
|
39714
|
+
if (this.provider.name !== "ollama")
|
|
39715
|
+
return;
|
|
39716
|
+
try {
|
|
39717
|
+
const response = await fetch(`${this.provider.baseUrl}/api/show`, {
|
|
39718
|
+
method: "POST",
|
|
39719
|
+
headers: { "Content-Type": "application/json" },
|
|
39720
|
+
body: JSON.stringify({ name: this.modelName }),
|
|
39721
|
+
signal: AbortSignal.timeout(3000)
|
|
39722
|
+
});
|
|
39723
|
+
if (response.ok) {
|
|
39724
|
+
const data = await response.json();
|
|
39725
|
+
const ctxFromInfo = data.model_info?.["general.context_length"];
|
|
39726
|
+
const ctxFromParams = data.parameters?.match(/num_ctx\s+(\d+)/)?.[1];
|
|
39727
|
+
if (ctxFromInfo) {
|
|
39728
|
+
this.contextWindow = parseInt(ctxFromInfo, 10);
|
|
39729
|
+
} else if (ctxFromParams) {
|
|
39730
|
+
this.contextWindow = parseInt(ctxFromParams, 10);
|
|
39731
|
+
} else {
|
|
39732
|
+
this.contextWindow = 8192;
|
|
39733
|
+
}
|
|
39734
|
+
log(`[LocalProvider:${this.provider.name}] Context window: ${this.contextWindow}`);
|
|
39735
|
+
}
|
|
39736
|
+
} catch (e) {}
|
|
39737
|
+
}
|
|
39738
|
+
writeTokenFile(input, output) {
|
|
39739
|
+
try {
|
|
39740
|
+
this.sessionInputTokens += input;
|
|
39741
|
+
this.sessionOutputTokens += output;
|
|
39742
|
+
const total = this.sessionInputTokens + this.sessionOutputTokens;
|
|
39743
|
+
const leftPct = this.contextWindow > 0 ? Math.max(0, Math.min(100, Math.round((this.contextWindow - total) / this.contextWindow * 100))) : 100;
|
|
39744
|
+
const data = {
|
|
39745
|
+
input_tokens: this.sessionInputTokens,
|
|
39746
|
+
output_tokens: this.sessionOutputTokens,
|
|
39747
|
+
total_tokens: total,
|
|
39748
|
+
total_cost: 0,
|
|
39749
|
+
context_window: this.contextWindow,
|
|
39750
|
+
context_left_percent: leftPct,
|
|
39751
|
+
updated_at: Date.now()
|
|
39752
|
+
};
|
|
39753
|
+
writeFileSync9(join9(tmpdir3(), `claudish-tokens-${this.port}.json`), JSON.stringify(data), "utf-8");
|
|
39754
|
+
} catch (e) {}
|
|
39755
|
+
}
|
|
39590
39756
|
async handle(c, payload) {
|
|
39591
39757
|
const target = this.modelName;
|
|
39592
39758
|
logStructured(`LocalProvider Request`, {
|
|
@@ -39600,6 +39766,7 @@ class LocalProviderHandler {
|
|
|
39600
39766
|
if (!healthy) {
|
|
39601
39767
|
return this.errorResponse(c, "connection_error", this.getConnectionErrorMessage());
|
|
39602
39768
|
}
|
|
39769
|
+
await this.fetchContextWindow();
|
|
39603
39770
|
}
|
|
39604
39771
|
const { claudeRequest, droppedParams } = transformOpenAIToClaude(payload);
|
|
39605
39772
|
const messages = convertMessagesToOpenAI(claudeRequest, target, filterIdentity);
|
|
@@ -39652,7 +39819,7 @@ class LocalProviderHandler {
|
|
|
39652
39819
|
c.header("X-Dropped-Params", droppedParams.join(", "));
|
|
39653
39820
|
}
|
|
39654
39821
|
if (openAIPayload.stream) {
|
|
39655
|
-
return createStreamingResponseHandler(c, response, adapter, target, this.middlewareManager);
|
|
39822
|
+
return createStreamingResponseHandler(c, response, adapter, target, this.middlewareManager, (input, output) => this.writeTokenFile(input, output), claudeRequest.tools);
|
|
39656
39823
|
}
|
|
39657
39824
|
const data = await response.json();
|
|
39658
39825
|
return c.json(data);
|
|
@@ -39948,24 +40115,24 @@ __export(exports_update_checker, {
|
|
|
39948
40115
|
});
|
|
39949
40116
|
import { execSync } from "node:child_process";
|
|
39950
40117
|
import { createInterface as createInterface2 } from "node:readline";
|
|
39951
|
-
import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as
|
|
39952
|
-
import { join as
|
|
39953
|
-
import { tmpdir as
|
|
40118
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync10, mkdirSync as mkdirSync4, unlinkSync as unlinkSync2 } from "node:fs";
|
|
40119
|
+
import { join as join10 } from "node:path";
|
|
40120
|
+
import { tmpdir as tmpdir4, homedir as homedir2, platform } from "node:os";
|
|
39954
40121
|
function getCacheFilePath() {
|
|
39955
40122
|
let cacheDir;
|
|
39956
40123
|
if (isWindows2) {
|
|
39957
|
-
const localAppData = process.env.LOCALAPPDATA ||
|
|
39958
|
-
cacheDir =
|
|
40124
|
+
const localAppData = process.env.LOCALAPPDATA || join10(homedir2(), "AppData", "Local");
|
|
40125
|
+
cacheDir = join10(localAppData, "claudish");
|
|
39959
40126
|
} else {
|
|
39960
|
-
cacheDir =
|
|
40127
|
+
cacheDir = join10(homedir2(), ".cache", "claudish");
|
|
39961
40128
|
}
|
|
39962
40129
|
try {
|
|
39963
40130
|
if (!existsSync7(cacheDir)) {
|
|
39964
40131
|
mkdirSync4(cacheDir, { recursive: true });
|
|
39965
40132
|
}
|
|
39966
|
-
return
|
|
40133
|
+
return join10(cacheDir, "update-check.json");
|
|
39967
40134
|
} catch {
|
|
39968
|
-
return
|
|
40135
|
+
return join10(tmpdir4(), "claudish-update-check.json");
|
|
39969
40136
|
}
|
|
39970
40137
|
}
|
|
39971
40138
|
function readCache() {
|
|
@@ -39987,7 +40154,7 @@ function writeCache(latestVersion) {
|
|
|
39987
40154
|
lastCheck: Date.now(),
|
|
39988
40155
|
latestVersion
|
|
39989
40156
|
};
|
|
39990
|
-
|
|
40157
|
+
writeFileSync10(cachePath, JSON.stringify(data), "utf-8");
|
|
39991
40158
|
} catch {}
|
|
39992
40159
|
}
|
|
39993
40160
|
function isCacheValid(cache) {
|
|
@@ -40116,7 +40283,7 @@ async function checkForUpdates(currentVersion, options = {}) {
|
|
|
40116
40283
|
}
|
|
40117
40284
|
var isWindows2, NPM_REGISTRY_URL = "https://registry.npmjs.org/claudish/latest", CACHE_MAX_AGE_MS;
|
|
40118
40285
|
var init_update_checker = __esm(() => {
|
|
40119
|
-
isWindows2 =
|
|
40286
|
+
isWindows2 = platform() === "win32";
|
|
40120
40287
|
CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
40121
40288
|
});
|
|
40122
40289
|
|