diff --git a/.gitignore b/.gitignore index 57438d50..10ea7e20 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,7 @@ node_modules .env log.txt .idea -dist \ No newline at end of file +dist +CLAUDE.md +.cursor/** +.claude-code-router \ No newline at end of file diff --git a/README.md b/README.md index 71bb8569..4b871269 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Claude Code Router -[中文版](README_zh.md) +English | [中文](README_zh.md) + > A powerful tool to route Claude Code requests to different models and customize any request. @@ -312,6 +313,9 @@ module.exports = async function router(req, config) { }; ``` +## 🛠️ Working with this repo +👉 [Contributing Guide](CONTRIBUTING.md) + ## 🤖 GitHub Actions Integrate Claude Code Router into your CI/CD pipeline. After setting up [Claude Code Actions](https://docs.anthropic.com/en/docs/claude-code/github-actions), modify your `.github/workflows/claude.yaml` to use the router: diff --git a/README_zh.md b/README_zh.md index 17113146..b07af8aa 100644 --- a/README_zh.md +++ b/README_zh.md @@ -1,5 +1,8 @@ # Claude Code Router + +[English](README.md) | 中文 + > 一款强大的工具,可将 Claude Code 请求路由到不同的模型,并自定义任何请求。 ![](blog/images/claude-code.png) @@ -306,6 +309,8 @@ module.exports = async function router(req, config) { return null; }; ``` +## 🛠️ Working with this repo +👉 [Contributing Guide](CONTRIBUTING.md) ## 🤖 GitHub Actions diff --git a/package.json b/package.json index c499ebdf..8f3be13d 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,9 @@ }, "scripts": { "build": "esbuild src/cli.ts --bundle --platform=node --outfile=dist/cli.js && shx cp node_modules/tiktoken/tiktoken_bg.wasm dist/tiktoken_bg.wasm", - "release": "npm run build && npm publish" + "release": "npm run build && npm publish", + "dev:server": "nodemon --watch 'src/**' --ext ts --exec 'npm run build && NODE_ENV=development node dist/cli.js start'", + "code": "NODE_ENV=development node dist/cli.js code" }, "keywords": [ "claude", @@ -30,7 +32,8 @@ "esbuild": "^0.25.1", "fastify": "^5.4.0", "shx": "^0.4.0", - "typescript": "^5.8.2" + "typescript": "^5.8.2", + "nodemon": "^3.1.7" }, "publishConfig": { "ignore": [ diff --git a/src/cli.ts b/src/cli.ts index 0fe4b941..14a3917a 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,11 +1,11 @@ #!/usr/bin/env node import { run } from "./index"; -import { showStatus } from "./utils/status"; -import { executeCodeCommand } from "./utils/codeCommand"; -import { cleanupPidFile, isServiceRunning } from "./utils/processCheck"; +import { showStatus } from "@/utils/status"; +import { executeCodeCommand } from "@/utils/codeCommand"; +import { cleanupPidFile, isServiceRunning } from "@/utils/processCheck"; import { version } from "../package.json"; import { spawn } from "child_process"; -import { PID_FILE, REFERENCE_COUNT_FILE } from "./constants"; +import {getPidFile, getReferenceCountFile, isDevMode, PID_FILE, REFERENCE_COUNT_FILE} from "@/constants"; import fs, { existsSync, readFileSync } from "fs"; import {join} from "path"; @@ -28,16 +28,18 @@ Example: ccr code "Write a Hello World" `; +// Ensure service is fully initialized before proceeding with commands async function waitForService( timeout = 10000, initialDelay = 1000 ): Promise { + // Wait for an initial period to let the service initialize await new Promise((resolve) => setTimeout(resolve, initialDelay)); const startTime = Date.now(); while (Date.now() - startTime < timeout) { - if (isServiceRunning()) { + if (isServiceRunning(getPidFile())) { // Wait for an additional short period to ensure service is fully ready await new Promise((resolve) => setTimeout(resolve, 500)); return true; @@ -48,18 +50,23 @@ async function waitForService( } async function main() { +// Handle CLI commands with appropriate service management actions switch (command) { + // Start the router service in background mode case "start": - run(); + await run(); break; + // Stop the service and clean up PID/reference files case "stop": try { - const pid = parseInt(readFileSync(PID_FILE, "utf-8")); + const pidFile = getPidFile() + const referenceCountFile = getReferenceCountFile() + const pid = parseInt(readFileSync(pidFile, "utf-8")); process.kill(pid); - cleanupPidFile(); - if (existsSync(REFERENCE_COUNT_FILE)) { + cleanupPidFile(pidFile); + if (existsSync(referenceCountFile)) { try { - fs.unlinkSync(REFERENCE_COUNT_FILE); + fs.unlinkSync(referenceCountFile); } catch (e) { // Ignore cleanup errors } @@ -71,62 +78,68 @@ async function main() { console.log( "Failed to stop the service. It may have already been stopped." ); - cleanupPidFile(); + const pidFile = getPidFile() + cleanupPidFile(pidFile); } break; case "status": await showStatus(); break; + // Execute Claude Code command with auto-start capability if service isn't running case "code": - if (!isServiceRunning()) { - console.log("Service not running, starting service..."); - const cliPath = join(__dirname, "cli.js"); - const startProcess = spawn("node", [cliPath, "start"], { - detached: true, - stdio: "ignore", - }); - - // let errorMessage = ""; - // startProcess.stderr?.on("data", (data) => { - // errorMessage += data.toString(); - // }); - - startProcess.on("error", (error) => { - console.error("Failed to start service:", error.message); - process.exit(1); - }); - - // startProcess.on("close", (code) => { - // if (code !== 0 && errorMessage) { - // console.error("Failed to start service:", errorMessage.trim()); - // process.exit(1); - // } - // }); - - startProcess.unref(); - - if (await waitForService()) { - executeCodeCommand(process.argv.slice(3)); + { + const pidFile = getPidFile() + // Auto-start service if not running before executing code command + if (!isServiceRunning(pidFile)) { + console.log("Service not running, starting service..."); + const cliPath = join(__dirname, "cli.js"); + const startProcessArgs = isDevMode() ? [cliPath, "start"] : [cliPath, "start"]; + const startProcess = spawn("node",startProcessArgs, { + detached: true, + stdio: "ignore", + env: { + ...process.env, + SERVICE_PORT: process.env.SERVICE_PORT || isDevMode() ? "3457" : "3456", + NODE_ENV: process.env.NODE_ENV || isDevMode() ? "Development" : "production", + } + }); + + + startProcess.on("error", (error) => { + console.error("Failed to start service:", error.message); + process.exit(1); + }); + + // 处理子进程输出 + startProcess.stdout?.on('data', (data) => { + console.log(`输出: ${data}`); + }); + startProcess.unref(); + if (await waitForService()) { + await executeCodeCommand(process.argv.slice(3)); + } else { + console.error( + "Service startup timeout, please manually run `ccr start` to start the service" + ); + process.exit(1); + } } else { - console.error( - "Service startup timeout, please manually run `ccr start` to start the service" - ); - process.exit(1); + await executeCodeCommand(process.argv.slice(3)); } - } else { - executeCodeCommand(process.argv.slice(3)); } break; case "-v": case "version": console.log(`claude-code-router version: ${version}`); break; + // Gracefully stop and restart the service with cleanup case "restart": // Stop the service if it's running + const pidFile = getPidFile(); try { - const pid = parseInt(readFileSync(PID_FILE, "utf-8")); + const pid = parseInt(readFileSync(pidFile, "utf-8")); process.kill(pid); - cleanupPidFile(); + cleanupPidFile(pidFile); if (existsSync(REFERENCE_COUNT_FILE)) { try { fs.unlinkSync(REFERENCE_COUNT_FILE); @@ -136,16 +149,21 @@ async function main() { } console.log("claude code router service has been stopped."); } catch (e) { - console.log("Service was not running or failed to stop."); - cleanupPidFile(); + // If the service was not running or failed to stop, log a message + console.log(`${isDevMode() ? "Development" : ""}Service was not running or failed to stop. cleaning up PID file ${pidFile}.`); + + cleanupPidFile(pidFile); } // Start the service again in the background - console.log("Starting claude code router service..."); + console.log(` ${isDevMode() ? "Development" : ""}. Starting claude code router service... please wait.`); const cliPath = join(__dirname, "cli.js"); const startProcess = spawn("node", [cliPath, "start"], { detached: true, stdio: "ignore", + env: { + ...process.env, + } }); startProcess.on("error", (error) => { @@ -154,7 +172,8 @@ async function main() { }); startProcess.unref(); - console.log("✅ Service started successfully in the background."); + console.log(`${isDevMode() ? "Development" : ""}✅ Service started successfully in the background.`); + break; case "-h": case "help": diff --git a/src/constants.ts b/src/constants.ts index 5b7f3bf0..ef87766c 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -18,3 +18,32 @@ export const DEFAULT_CONFIG = { OPENAI_BASE_URL: "", OPENAI_MODEL: "", }; + + +// 项目的相对路径 +export const DEV_HOME_DIR = path.join(__dirname, "..", ".claude-code-router"); +export const DEV_PID_FILE = path.join(DEV_HOME_DIR, ".claude-code-router.pid") +export const DEV_CONFIG_FILE = path.join(DEV_HOME_DIR, "config.json"); +export const DEV_PLUGINS_DIR = path.join(DEV_HOME_DIR, "plugins"); +export const DEV_REFERENCE_COUNT_FILE = path.join(DEV_HOME_DIR, "claude-code-reference-count.txt"); + + +export function getConfigFile(): string { + return process.env.NODE_ENV === 'development' ? DEV_CONFIG_FILE : CONFIG_FILE; +} +export function getPluginsDir(): string { + return process.env.NODE_ENV === 'development' ? DEV_PLUGINS_DIR : PLUGINS_DIR; +} +export function getPidFile(): string { + return process.env.NODE_ENV === 'development' ? DEV_PID_FILE : PID_FILE; +} +export function getReferenceCountFile(): string { + return process.env.NODE_ENV === 'development' ? DEV_REFERENCE_COUNT_FILE : REFERENCE_COUNT_FILE; +} +export function getHomeDir(): string { + return process.env.NODE_ENV === 'development' ? DEV_HOME_DIR : HOME_DIR; +} + +export function isDevMode(): boolean { + return process.env.NODE_ENV === 'development'; +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index c52ac1a2..23b0f126 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,20 +2,22 @@ import { existsSync } from "fs"; import { writeFile } from "fs/promises"; import { homedir } from "os"; import { join } from "path"; -import { initConfig, initDir } from "./utils"; -import { createServer } from "./server"; -import { router } from "./utils/router"; -import { apiKeyAuth } from "./middleware/auth"; +import { initConfig, initDir } from "@/utils"; +import { createServer } from "@/server"; +import { router } from "@/utils/router"; +import { apiKeyAuth } from "@/middleware/auth"; import { cleanupPidFile, isServiceRunning, savePid, -} from "./utils/processCheck"; -import { CONFIG_FILE } from "./constants"; +} from "@/utils/processCheck"; +import { getHomeDir, getConfigFile, getPidFile} from "@/constants"; + +const isDev = process.env.NODE_ENV === "development"; + async function initializeClaudeConfig() { - const homeDir = homedir(); - const configPath = join(homeDir, ".claude.json"); + const configPath = join(getHomeDir(), ".claude.json"); if (!existsSync(configPath)) { const userID = Array.from( { length: 64 }, @@ -39,15 +41,19 @@ interface RunOptions { async function run(options: RunOptions = {}) { // Check if service is already running - if (isServiceRunning()) { - console.log("✅ Service is already running in the background."); + + const pidFile = getPidFile() + if (isServiceRunning(pidFile)) { + console.log ( + `${isDev ? "development" : ""}. ✅ Service is already running in the background, PID file found at ${pidFile}, .` + ); return; } await initializeClaudeConfig(); await initDir(); const config = await initConfig(); - let HOST = config.HOST; + let HOST = config.HOST || "127.0.0.1"; if (config.HOST && !config.APIKEY) { HOST = "127.0.0.1"; @@ -56,31 +62,32 @@ async function run(options: RunOptions = {}) { ); } - const port = config.PORT || 3456; + const port = config.PORT || isDev ? 3457: 3456; // Save the PID of the background process - savePid(process.pid); + savePid(process.pid, pidFile); // Handle SIGINT (Ctrl+C) to clean up PID file process.on("SIGINT", () => { - console.log("Received SIGINT, cleaning up..."); - cleanupPidFile(); + const pidFile = getPidFile() + console.log(` Received SIGINT, cleaning up pidFile ${pidFile}..`); + cleanupPidFile(getPidFile()); process.exit(0); }); // Handle SIGTERM to clean up PID file process.on("SIGTERM", () => { - cleanupPidFile(); + cleanupPidFile(getPidFile()); process.exit(0); }); - console.log(HOST) + console.log(`=========> Starting service on ${HOST}:${port}`); // Use port from environment variable if set (for background process) const servicePort = process.env.SERVICE_PORT ? parseInt(process.env.SERVICE_PORT) : port; const server = createServer({ - jsonPath: CONFIG_FILE, + jsonPath: getConfigFile(), initialConfig: { // ...config, providers: config.Providers || config.providers, @@ -94,7 +101,7 @@ async function run(options: RunOptions = {}) { }, }); server.addHook("preHandler", apiKeyAuth(config)); - server.addHook("preHandler", async (req, reply) => + server.addHook("preHandler", async (req: any, reply: any) => router(req, reply, config) ); server.start(); diff --git a/src/middleware/auth.ts b/src/middleware/auth.ts index fc4cd424..ed5a53ae 100644 --- a/src/middleware/auth.ts +++ b/src/middleware/auth.ts @@ -8,17 +8,19 @@ export const apiKeyAuth = } const apiKey = config.APIKEY; + // Allow bypass when no API key is configured (development mode) if (!apiKey) { return done(); } - const authKey: string = - req.headers.authorization || req.headers["x-api-key"]; + // Support both standard Authorization and x-api-key header formats + const authKey: string = req.headers.authorization || req.headers["x-api-key"] as string; if (!authKey) { reply.status(401).send("APIKEY is missing"); return; } let token = ""; + // Handle both 'Bearer ' and direct token formats if (authKey.startsWith("Bearer")) { token = authKey.split(" ")[1]; } else { diff --git a/src/utils/close.ts b/src/utils/close.ts index 6c1f9735..6050be4f 100644 --- a/src/utils/close.ts +++ b/src/utils/close.ts @@ -1,12 +1,12 @@ import { isServiceRunning, cleanupPidFile, getReferenceCount } from './processCheck'; import { readFileSync } from 'fs'; -import { HOME_DIR } from '../constants'; -import { join } from 'path'; +import { getPidFile} from '@/constants'; export async function closeService() { - const PID_FILE = join(HOME_DIR, '.claude-code-router.pid'); + + const pidFile = getPidFile() - if (!isServiceRunning()) { + if (!isServiceRunning(pidFile)) { console.log("No service is currently running."); return; } @@ -16,12 +16,12 @@ export async function closeService() { } try { - const pid = parseInt(readFileSync(PID_FILE, 'utf-8')); + const pid = parseInt(readFileSync(pidFile, 'utf-8')); process.kill(pid); - cleanupPidFile(); + cleanupPidFile(pidFile); console.log("claude code router service has been successfully stopped."); } catch (e) { console.log("Failed to stop the service. It may have already been stopped."); - cleanupPidFile(); + cleanupPidFile(pidFile); } } diff --git a/src/utils/codeCommand.ts b/src/utils/codeCommand.ts index feba445a..4bf1be41 100644 --- a/src/utils/codeCommand.ts +++ b/src/utils/codeCommand.ts @@ -5,45 +5,64 @@ import { } from "./processCheck"; import { closeService } from "./close"; import { readConfigFile } from "."; +import {isDevMode} from "@/constants"; export async function executeCodeCommand(args: string[] = []) { // Set environment variables const config = await readConfigFile(); - const env = { + const port = process.env.NODE_ENV === 'development' ? 3457 : (config.PORT || 3456); + const host = config.HOST || "127.0.0.1" + const env : { + [key: string]: string | undefined; + } = { ...process.env, - ANTHROPIC_AUTH_TOKEN: "test", - ANTHROPIC_BASE_URL: `http://127.0.0.1:${config.PORT || 3456}`, + ANTHROPIC_BASE_URL: `http://${host}:${port}`, API_TIMEOUT_MS: String(config.API_TIMEOUT_MS ?? 600000), // Default to 10 minutes if not set }; + // Configure authentication: use API key if provided, otherwise use test token if (config?.APIKEY) { env.ANTHROPIC_API_KEY = config.APIKEY; - delete env.ANTHROPIC_AUTH_TOKEN; + } else { + env.ANTHROPIC_AUTH_TOKEN = "test_router_token_123"; } - // Increment reference count when command starts + // Increment reference count to track active processes and prevent premature shutdown incrementReferenceCount(); // Execute claude command const claudePath = process.env.CLAUDE_PATH || "claude"; + + // Spawn Claude process with inherited stdio for real-time output const claudeProcess = spawn(claudePath, args, { env, stdio: "inherit", - shell: true, + shell: false, }); + claudeProcess.on("error", (error) => { - console.error("Failed to start claude command:", error.message); + console.error(`${isDevMode() ? 'Development' :'' }. Failed to start claude command:${error.message}`); console.log( "Make sure Claude Code is installed: npm install -g @anthropic-ai/claude-code" ); decrementReferenceCount(); + closeService(); process.exit(1); }); claudeProcess.on("close", (code) => { + console.log(`${isDevMode() ? 'Development' :'' }. Claude command exited with code ${code}`); decrementReferenceCount(); closeService(); process.exit(code || 0); }); + + claudeProcess.stderr?.on("data", (data) => { + console.error(`Error: ${data}`); + }) + + claudeProcess.stdout?.on("data", (data) => { + console.log(`Output: ${data}`); + }) } diff --git a/src/utils/index.ts b/src/utils/index.ts index ba0c3f14..ed4623e2 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -2,11 +2,9 @@ import fs from "node:fs/promises"; import readline from "node:readline"; import JSON5 from "json5"; import { - CONFIG_FILE, DEFAULT_CONFIG, - HOME_DIR, - PLUGINS_DIR, -} from "../constants"; + getConfigFile, getHomeDir, getPluginsDir, +} from "@/constants"; const ensureDir = async (dir_path: string) => { try { @@ -17,8 +15,8 @@ const ensureDir = async (dir_path: string) => { }; export const initDir = async () => { - await ensureDir(HOME_DIR); - await ensureDir(PLUGINS_DIR); + await ensureDir(getHomeDir()); + await ensureDir(getPluginsDir()); }; const createReadline = () => { @@ -45,12 +43,12 @@ const confirm = async (query: string): Promise => { export const readConfigFile = async () => { try { - const config = await fs.readFile(CONFIG_FILE, "utf-8"); + const config = await fs.readFile(getConfigFile(), "utf-8"); try { // Try to parse with JSON5 first (which also supports standard JSON) return JSON5.parse(config); } catch (parseError) { - console.error(`Failed to parse config file at ${CONFIG_FILE}`); + console.error(`Failed to parse config file at ${getConfigFile()}`); console.error("Error details:", (parseError as Error).message); console.error("Please check your config file syntax."); process.exit(1); @@ -78,7 +76,7 @@ export const readConfigFile = async () => { await writeConfigFile(config); return config; } else { - console.error(`Failed to read config file at ${CONFIG_FILE}`); + console.error(`Failed to read config file at ${getConfigFile()}`); console.error("Error details:", readError.message); process.exit(1); } @@ -86,10 +84,10 @@ export const readConfigFile = async () => { }; export const writeConfigFile = async (config: any) => { - await ensureDir(HOME_DIR); + await ensureDir(getHomeDir()); // Add a comment to indicate JSON5 support const configWithComment = `// This config file supports JSON5 format (comments, trailing commas, etc.)\n${JSON5.stringify(config, null, 2)}`; - await fs.writeFile(CONFIG_FILE, configWithComment); + await fs.writeFile(getConfigFile(), configWithComment); }; export const initConfig = async () => { diff --git a/src/utils/log.ts b/src/utils/log.ts index 6999726e..197dddcd 100644 --- a/src/utils/log.ts +++ b/src/utils/log.ts @@ -1,6 +1,6 @@ import fs from "node:fs"; import path from "node:path"; -import { HOME_DIR } from "../constants"; +import { HOME_DIR } from "@/constants"; const LOG_FILE = path.join(HOME_DIR, "claude-code-router.log"); diff --git a/src/utils/processCheck.ts b/src/utils/processCheck.ts index b5b2113f..4e344d6d 100644 --- a/src/utils/processCheck.ts +++ b/src/utils/processCheck.ts @@ -1,79 +1,90 @@ import { existsSync, readFileSync, writeFileSync } from 'fs'; -import { PID_FILE, REFERENCE_COUNT_FILE } from '../constants'; +import {getPidFile, getReferenceCountFile, PID_FILE, REFERENCE_COUNT_FILE} from '@/constants'; import { readConfigFile } from '.'; +// Atomically increment service reference count to track active users export function incrementReferenceCount() { let count = 0; - if (existsSync(REFERENCE_COUNT_FILE)) { - count = parseInt(readFileSync(REFERENCE_COUNT_FILE, 'utf-8')) || 0; + const referenceCountFile = getReferenceCountFile(); + if (existsSync(referenceCountFile)) { + count = parseInt(readFileSync(referenceCountFile, 'utf-8')) || 0; } count++; - writeFileSync(REFERENCE_COUNT_FILE, count.toString()); + writeFileSync(referenceCountFile, count.toString()); } +// Safely decrement reference count with floor at zero to prevent negative values export function decrementReferenceCount() { let count = 0; - if (existsSync(REFERENCE_COUNT_FILE)) { - count = parseInt(readFileSync(REFERENCE_COUNT_FILE, 'utf-8')) || 0; + const referenceCountFile = getReferenceCountFile(); + if (existsSync(referenceCountFile)) { + count = parseInt(readFileSync(referenceCountFile, 'utf-8')) || 0; } count = Math.max(0, count - 1); - writeFileSync(REFERENCE_COUNT_FILE, count.toString()); + writeFileSync(referenceCountFile, count.toString()); } export function getReferenceCount(): number { - if (!existsSync(REFERENCE_COUNT_FILE)) { + const referenceCountFile = getReferenceCountFile(); + if (!existsSync(referenceCountFile)) { return 0; } - return parseInt(readFileSync(REFERENCE_COUNT_FILE, 'utf-8')) || 0; + return parseInt(readFileSync(referenceCountFile, 'utf-8')) || 0; } -export function isServiceRunning(): boolean { - if (!existsSync(PID_FILE)) { +// Check if service is actively running by verifying PID file and process existence +export function isServiceRunning(pid_file= PID_FILE): boolean { + if (!existsSync(pid_file)) { return false; } try { - const pid = parseInt(readFileSync(PID_FILE, 'utf-8')); + const pid = parseInt(readFileSync(pid_file, 'utf-8')); process.kill(pid, 0); return true; } catch (e) { // Process not running, clean up pid file - cleanupPidFile(); + cleanupPidFile(pid_file); return false; } } -export function savePid(pid: number) { - writeFileSync(PID_FILE, pid.toString()); +// Persist current process ID to file for lifecycle management and status checks +export function savePid(pid: number, pidFile = PID_FILE) { + writeFileSync(pidFile, pid.toString()); } -export function cleanupPidFile() { - if (existsSync(PID_FILE)) { +// Remove PID file when service terminates to prevent stale process detection +export function cleanupPidFile(pidFile = PID_FILE) { + if (existsSync(pidFile)) { try { const fs = require('fs'); - fs.unlinkSync(PID_FILE); + fs.unlinkSync(pidFile); } catch (e) { // Ignore cleanup errors } } } -export function getServicePid(): number | null { - if (!existsSync(PID_FILE)) { +export function getServicePid(pidFile = PID_FILE): number | null { + if (!existsSync(pidFile)) { return null; } try { - const pid = parseInt(readFileSync(PID_FILE, 'utf-8')); + const pid = parseInt(readFileSync(pidFile, 'utf-8')); return isNaN(pid) ? null : pid; } catch (e) { return null; } } +// Aggregate service status information for CLI status reporting and monitoring export async function getServiceInfo() { - const pid = getServicePid(); - const running = isServiceRunning(); + + const pidFile = getPidFile() + const pid = getServicePid(pidFile); + const running = isServiceRunning(pidFile); const config = await readConfigFile(); return { @@ -81,7 +92,7 @@ export async function getServiceInfo() { pid, port: config.PORT, endpoint: `http://127.0.0.1:${config.PORT}`, - pidFile: PID_FILE, + pidFile, referenceCount: getReferenceCount() }; } diff --git a/src/utils/router.ts b/src/utils/router.ts index 87cca285..1cacd5af 100644 --- a/src/utils/router.ts +++ b/src/utils/router.ts @@ -8,17 +8,24 @@ import { log } from "./log"; const enc = get_encoding("cl100k_base"); +/** + * Calculates token count for messages, system prompts, and tools using cl100k_base encoding + * Handles multiple content types (text, tool_use, tool_result) and complex structures + * Critical for routing decisions based on context length + */ const calculateTokenCount = ( messages: MessageParam[], system: any, tools: Tool[] ) => { let tokenCount = 0; - if (Array.isArray(messages)) { + // Process user messages which may contain mixed content types (text, tool_use, tool_result) +if (Array.isArray(messages)) { messages.forEach((message) => { if (typeof message.content === "string") { tokenCount += enc.encode(message.content).length; - } else if (Array.isArray(message.content)) { + // Handle structured message content arrays (e.g., multimodal inputs) +} else if (Array.isArray(message.content)) { message.content.forEach((contentPart: any) => { if (contentPart.type === "text") { tokenCount += enc.encode(contentPart.text).length; @@ -39,7 +46,8 @@ const calculateTokenCount = ( } if (typeof system === "string") { tokenCount += enc.encode(system).length; - } else if (Array.isArray(system)) { + // Handle structured system prompts with potential mixed content types +} else if (Array.isArray(system)) { system.forEach((item: any) => { if (item.type !== "text") return; if (typeof item.text === "string") { @@ -51,7 +59,8 @@ const calculateTokenCount = ( } }); } - if (tools) { + // Include tool definitions in token count (critical for tool-using models) +if (tools) { tools.forEach((tool: Tool) => { if (tool.description) { tokenCount += enc.encode(tool.name + tool.description).length; @@ -68,13 +77,13 @@ const getUseModel = async (req: any, tokenCount: number, config: any) => { if (req.body.model.includes(",")) { return req.body.model; } - // if tokenCount is greater than the configured threshold, use the long context model + // Use long context model when input exceeds token threshold (default: 60k) for handling large context requests const longContextThreshold = config.Router.longContextThreshold || 60000; if (tokenCount > longContextThreshold && config.Router.longContext) { log("Using long context model due to token count:", tokenCount, "threshold:", longContextThreshold); return config.Router.longContext; } - // If the model is claude-3-5-haiku, use the background model + // Prefer background model for haiku requests to optimize cost/performance tradeoff if ( req.body.model?.startsWith("claude-3-5-haiku") && config.Router.background @@ -82,19 +91,21 @@ const getUseModel = async (req: any, tokenCount: number, config: any) => { log("Using background model for ", req.body.model); return config.Router.background; } - // if exits thinking, use the think model + // Use specialized think model for requests requiring extended reasoning (e.g. --think flag) if (req.body.thinking && config.Router.think) { log("Using think model for ", req.body.thinking); return config.Router.think; } - if ( + // Route to webSearch model when any tool requires web search capabilities (e.g. /websearch command) +if ( Array.isArray(req.body.tools) && req.body.tools.some((tool: any) => tool.type?.startsWith("web_search")) && config.Router.webSearch ) { return config.Router.webSearch; } - return config.Router!.default; + // Fallback to default model when no specialized conditions are met +return config.Router!.default; }; export const router = async (req: any, _res: any, config: any) => { diff --git a/tsconfig.json b/tsconfig.json index 3a82250f..65ba5044 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,8 +13,13 @@ "noImplicitAny": true, "allowSyntheticDefaultImports": true, "sourceMap": true, - "declaration": true + "declaration": true, + "baseUrl": "./", + "paths": { + "@/*": [ "src/*" ] + } }, + "include": ["src/**/*.ts"], "exclude": ["node_modules", "dist"] }