From 5e14b9b0e1f4dd27ede0979d76adb446d105d9a3 Mon Sep 17 00:00:00 2001 From: musi Date: Mon, 21 Jul 2025 15:13:58 +0800 Subject: [PATCH] release v1.0.24 to support custom router --- custom-router.example.js | 3 + package-lock.json | 34 +++++++--- package.json | 5 +- pnpm-lock.yaml | 25 ++++++-- src/utils/router.ts | 133 ++++++++++++++++++++++++--------------- 5 files changed, 136 insertions(+), 64 deletions(-) create mode 100644 custom-router.example.js diff --git a/custom-router.example.js b/custom-router.example.js new file mode 100644 index 0000000..078855c --- /dev/null +++ b/custom-router.example.js @@ -0,0 +1,3 @@ +module.exports = async function router(req, config) { + return "deepseek,deepseek-chat"; +}; diff --git a/package-lock.json b/package-lock.json index 69bb56e..986b911 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "@musistudio/claude-code-router", - "version": "1.0.15", + "version": "1.0.23", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@musistudio/claude-code-router", - "version": "1.0.15", + "version": "1.0.23", "license": "MIT", "dependencies": { - "@musistudio/llms": "^1.0.4", + "@musistudio/llms": "^1.0.10", "dotenv": "^16.4.7", "tiktoken": "^1.0.21", "uuid": "^11.1.0" @@ -18,7 +18,9 @@ "ccr": "dist/cli.js" }, "devDependencies": { + "@types/node": "^24.0.15", "esbuild": "^0.25.1", + "fastify": "^5.4.0", "shx": "^0.4.0", "typescript": "^5.8.2" } @@ -182,9 +184,9 @@ } }, "node_modules/@musistudio/llms": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@musistudio/llms/-/llms-1.0.4.tgz", - "integrity": "sha512-z+Ge5NOaafIvgnGiZqySSz8b2sYIvRQRCVZHZH/IjotS2uQWXespcdIUu0h72toTRkLu7hVIxLuY5Poh+6PeTQ==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@musistudio/llms/-/llms-1.0.10.tgz", + "integrity": "sha512-s3FUykkR/IykIHb5a/5GXfwB3MSf3DjGbJlmK9injoKhSVhA9SgbP8nG2cj3AlC1Ve5bFyLS5OR4R7wxWB4oqQ==", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "^0.54.0", @@ -193,7 +195,8 @@ "dotenv": "^16.5.0", "fastify": "^5.4.0", "openai": "^5.6.0", - "undici": "^7.10.0" + "undici": "^7.10.0", + "uuid": "^11.1.0" } }, "node_modules/@nodelib/fs.scandir": { @@ -234,6 +237,16 @@ "node": ">= 8" } }, + "node_modules/@types/node": { + "version": "24.0.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.15.tgz", + "integrity": "sha512-oaeTSbCef7U/z7rDeJA138xpG3NuKc64/rZ2qmUFkFJmnMsAPaluIifqyWd8hSSMxyP9oie3dLAqYPblag9KgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.8.0" + } + }, "node_modules/abstract-logging": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", @@ -1609,6 +1622,13 @@ "node": ">=20.18.1" } }, + "node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "dev": true, + "license": "MIT" + }, "node_modules/uuid": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", diff --git a/package.json b/package.json index 6f9ece4..1249db4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@musistudio/claude-code-router", - "version": "1.0.23", + "version": "1.0.24", "description": "Use Claude Code without an Anthropics account and route it to another LLM provider", "bin": { "ccr": "./dist/cli.js" @@ -18,12 +18,13 @@ "author": "musistudio", "license": "MIT", "dependencies": { - "@musistudio/llms": "^1.0.10", + "@musistudio/llms": "^1.0.11", "dotenv": "^16.4.7", "tiktoken": "^1.0.21", "uuid": "^11.1.0" }, "devDependencies": { + "@types/node": "^24.0.15", "esbuild": "^0.25.1", "fastify": "^5.4.0", "shx": "^0.4.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bbfea80..7a06ab8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@musistudio/llms': - specifier: ^1.0.10 - version: 1.0.10(ws@8.18.3)(zod@3.25.67) + specifier: ^1.0.11 + version: 1.0.11(ws@8.18.3)(zod@3.25.67) dotenv: specifier: ^16.4.7 version: 16.6.1 @@ -21,6 +21,9 @@ importers: specifier: ^11.1.0 version: 11.1.0 devDependencies: + '@types/node': + specifier: ^24.0.15 + version: 24.0.15 esbuild: specifier: ^0.25.1 version: 0.25.5 @@ -220,8 +223,8 @@ packages: '@modelcontextprotocol/sdk': optional: true - '@musistudio/llms@1.0.10': - resolution: {integrity: sha512-s3FUykkR/IykIHb5a/5GXfwB3MSf3DjGbJlmK9injoKhSVhA9SgbP8nG2cj3AlC1Ve5bFyLS5OR4R7wxWB4oqQ==} + '@musistudio/llms@1.0.11': + resolution: {integrity: sha512-qydLNzZDeURK8fsYJFspM04x/4mlqmKAN2Ie7MLvWuYjYT+fOtDm5BaEzQKhNLOqA5pcB2bCU0L0VFRnoeOpBg==} '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} @@ -235,6 +238,9 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@types/node@24.0.15': + resolution: {integrity: sha512-oaeTSbCef7U/z7rDeJA138xpG3NuKc64/rZ2qmUFkFJmnMsAPaluIifqyWd8hSSMxyP9oie3dLAqYPblag9KgA==} + abstract-logging@2.0.1: resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} @@ -651,6 +657,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + undici-types@7.8.0: + resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==} + undici@7.11.0: resolution: {integrity: sha512-heTSIac3iLhsmZhUCjyS3JQEkZELateufzZuBaVM5RHXdSBMb1LPMQf5x+FH7qjsZYDP0ttAc3nnVpUB+wYbOg==} engines: {node: '>=20.18.1'} @@ -815,7 +824,7 @@ snapshots: - supports-color - utf-8-validate - '@musistudio/llms@1.0.10(ws@8.18.3)(zod@3.25.67)': + '@musistudio/llms@1.0.11(ws@8.18.3)(zod@3.25.67)': dependencies: '@anthropic-ai/sdk': 0.54.0 '@fastify/cors': 11.0.1 @@ -846,6 +855,10 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 + '@types/node@24.0.15': + dependencies: + undici-types: 7.8.0 + abstract-logging@2.0.1: {} agent-base@7.1.3: {} @@ -1278,6 +1291,8 @@ snapshots: typescript@5.8.3: {} + undici-types@7.8.0: {} + undici@7.11.0: {} uuid@11.1.0: {} diff --git a/src/utils/router.ts b/src/utils/router.ts index 8353927..251b4ae 100644 --- a/src/utils/router.ts +++ b/src/utils/router.ts @@ -1,9 +1,69 @@ -import { MessageCreateParamsBase } from "@anthropic-ai/sdk/resources/messages"; +import { + MessageCreateParamsBase, + MessageParam, + Tool, +} from "@anthropic-ai/sdk/resources/messages"; import { get_encoding } from "tiktoken"; import { log } from "./log"; const enc = get_encoding("cl100k_base"); +const calculateTokenCount = ( + messages: MessageParam[], + system: any, + tools: Tool[] +) => { + let tokenCount = 0; + if (Array.isArray(messages)) { + messages.forEach((message) => { + if (typeof message.content === "string") { + tokenCount += enc.encode(message.content).length; + } else if (Array.isArray(message.content)) { + message.content.forEach((contentPart: any) => { + if (contentPart.type === "text") { + tokenCount += enc.encode(contentPart.text).length; + } else if (contentPart.type === "tool_use") { + tokenCount += enc.encode( + JSON.stringify(contentPart.input) + ).length; + } else if (contentPart.type === "tool_result") { + tokenCount += enc.encode( + typeof contentPart.content === "string" + ? contentPart.content + : JSON.stringify(contentPart.content) + ).length; + } + }); + } + }); + } + if (typeof system === "string") { + tokenCount += enc.encode(system).length; + } else if (Array.isArray(system)) { + system.forEach((item: any) => { + if (item.type !== "text") return; + if (typeof item.text === "string") { + tokenCount += enc.encode(item.text).length; + } else if (Array.isArray(item.text)) { + item.text.forEach((textPart: any) => { + tokenCount += enc.encode(textPart || "").length; + }); + } + }); + } + if (tools) { + tools.forEach((tool: Tool) => { + if (tool.description) { + tokenCount += enc.encode(tool.name + tool.description).length; + } + if (tool.input_schema) { + tokenCount += enc.encode(JSON.stringify(tool.input_schema)).length; + } + }); + } + return tokenCount; +}; + const getUseModel = (req: any, tokenCount: number, config: any) => { if (req.body.model.includes(",")) { return req.body.model; @@ -26,64 +86,37 @@ const getUseModel = (req: any, tokenCount: number, config: any) => { log("Using think model for ", req.body.thinking); return config.Router.think; } - if (Array.isArray(req.body.tools) && req.body.tools.some(tool => tool.type?.startsWith('web_search')) && config.Router.webSearch) { + 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; }; -export const router = async (req: any, res: any, config: any) => { +export const router = async (req: any, _res: any, config: any) => { const { messages, system = [], tools }: MessageCreateParamsBase = req.body; try { - let tokenCount = 0; - if (Array.isArray(messages)) { - messages.forEach((message) => { - if (typeof message.content === "string") { - tokenCount += enc.encode(message.content).length; - } else if (Array.isArray(message.content)) { - message.content.forEach((contentPart) => { - if (contentPart.type === "text") { - tokenCount += enc.encode(contentPart.text).length; - } else if (contentPart.type === "tool_use") { - tokenCount += enc.encode( - JSON.stringify(contentPart.input) - ).length; - } else if (contentPart.type === "tool_result") { - tokenCount += enc.encode( - typeof contentPart.content === "string" - ? contentPart.content - : JSON.stringify(contentPart.content) - ).length; - } - }); - } - }); + const tokenCount = calculateTokenCount( + messages as MessageParam[], + system, + tools as Tool[] + ); + + let model; + if (config.CUSTOM_ROUTER_PATH) { + try { + const customRouter = require(config.CUSTOM_ROUTER_PATH); + model = await customRouter(req, config); + } catch (e: any) { + log("failed to load custom router", e.message); + } } - if (typeof system === "string") { - tokenCount += enc.encode(system).length; - } else if (Array.isArray(system)) { - system.forEach((item) => { - if (item.type !== "text") return; - if (typeof item.text === "string") { - tokenCount += enc.encode(item.text).length; - } else if (Array.isArray(item.text)) { - item.text.forEach((textPart) => { - tokenCount += enc.encode(textPart || "").length; - }); - } - }); + if (!model) { + model = getUseModel(req, tokenCount, config); } - if (tools) { - tools.forEach((tool) => { - if (tool.description) { - tokenCount += enc.encode(tool.name + tool.description).length; - } - if (tool.input_schema) { - tokenCount += enc.encode(JSON.stringify(tool.input_schema)).length; - } - }); - } - const model = getUseModel(req, tokenCount, config); req.body.model = model; } catch (error: any) { log("Error in router middleware:", error.message);