release v1.0.24 to support custom router

This commit is contained in:
musi
2025-07-21 15:13:58 +08:00
parent 7165953b50
commit 5e14b9b0e1
5 changed files with 136 additions and 64 deletions

3
custom-router.example.js Normal file
View File

@@ -0,0 +1,3 @@
module.exports = async function router(req, config) {
return "deepseek,deepseek-chat";
};

34
package-lock.json generated
View File

@@ -1,15 +1,15 @@
{ {
"name": "@musistudio/claude-code-router", "name": "@musistudio/claude-code-router",
"version": "1.0.15", "version": "1.0.23",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@musistudio/claude-code-router", "name": "@musistudio/claude-code-router",
"version": "1.0.15", "version": "1.0.23",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@musistudio/llms": "^1.0.4", "@musistudio/llms": "^1.0.10",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"tiktoken": "^1.0.21", "tiktoken": "^1.0.21",
"uuid": "^11.1.0" "uuid": "^11.1.0"
@@ -18,7 +18,9 @@
"ccr": "dist/cli.js" "ccr": "dist/cli.js"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^24.0.15",
"esbuild": "^0.25.1", "esbuild": "^0.25.1",
"fastify": "^5.4.0",
"shx": "^0.4.0", "shx": "^0.4.0",
"typescript": "^5.8.2" "typescript": "^5.8.2"
} }
@@ -182,9 +184,9 @@
} }
}, },
"node_modules/@musistudio/llms": { "node_modules/@musistudio/llms": {
"version": "1.0.4", "version": "1.0.10",
"resolved": "https://registry.npmjs.org/@musistudio/llms/-/llms-1.0.4.tgz", "resolved": "https://registry.npmjs.org/@musistudio/llms/-/llms-1.0.10.tgz",
"integrity": "sha512-z+Ge5NOaafIvgnGiZqySSz8b2sYIvRQRCVZHZH/IjotS2uQWXespcdIUu0h72toTRkLu7hVIxLuY5Poh+6PeTQ==", "integrity": "sha512-s3FUykkR/IykIHb5a/5GXfwB3MSf3DjGbJlmK9injoKhSVhA9SgbP8nG2cj3AlC1Ve5bFyLS5OR4R7wxWB4oqQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@anthropic-ai/sdk": "^0.54.0", "@anthropic-ai/sdk": "^0.54.0",
@@ -193,7 +195,8 @@
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"fastify": "^5.4.0", "fastify": "^5.4.0",
"openai": "^5.6.0", "openai": "^5.6.0",
"undici": "^7.10.0" "undici": "^7.10.0",
"uuid": "^11.1.0"
} }
}, },
"node_modules/@nodelib/fs.scandir": { "node_modules/@nodelib/fs.scandir": {
@@ -234,6 +237,16 @@
"node": ">= 8" "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": { "node_modules/abstract-logging": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz",
@@ -1609,6 +1622,13 @@
"node": ">=20.18.1" "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": { "node_modules/uuid": {
"version": "11.1.0", "version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@musistudio/claude-code-router", "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", "description": "Use Claude Code without an Anthropics account and route it to another LLM provider",
"bin": { "bin": {
"ccr": "./dist/cli.js" "ccr": "./dist/cli.js"
@@ -18,12 +18,13 @@
"author": "musistudio", "author": "musistudio",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@musistudio/llms": "^1.0.10", "@musistudio/llms": "^1.0.11",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"tiktoken": "^1.0.21", "tiktoken": "^1.0.21",
"uuid": "^11.1.0" "uuid": "^11.1.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^24.0.15",
"esbuild": "^0.25.1", "esbuild": "^0.25.1",
"fastify": "^5.4.0", "fastify": "^5.4.0",
"shx": "^0.4.0", "shx": "^0.4.0",

25
pnpm-lock.yaml generated
View File

@@ -9,8 +9,8 @@ importers:
.: .:
dependencies: dependencies:
'@musistudio/llms': '@musistudio/llms':
specifier: ^1.0.10 specifier: ^1.0.11
version: 1.0.10(ws@8.18.3)(zod@3.25.67) version: 1.0.11(ws@8.18.3)(zod@3.25.67)
dotenv: dotenv:
specifier: ^16.4.7 specifier: ^16.4.7
version: 16.6.1 version: 16.6.1
@@ -21,6 +21,9 @@ importers:
specifier: ^11.1.0 specifier: ^11.1.0
version: 11.1.0 version: 11.1.0
devDependencies: devDependencies:
'@types/node':
specifier: ^24.0.15
version: 24.0.15
esbuild: esbuild:
specifier: ^0.25.1 specifier: ^0.25.1
version: 0.25.5 version: 0.25.5
@@ -220,8 +223,8 @@ packages:
'@modelcontextprotocol/sdk': '@modelcontextprotocol/sdk':
optional: true optional: true
'@musistudio/llms@1.0.10': '@musistudio/llms@1.0.11':
resolution: {integrity: sha512-s3FUykkR/IykIHb5a/5GXfwB3MSf3DjGbJlmK9injoKhSVhA9SgbP8nG2cj3AlC1Ve5bFyLS5OR4R7wxWB4oqQ==} resolution: {integrity: sha512-qydLNzZDeURK8fsYJFspM04x/4mlqmKAN2Ie7MLvWuYjYT+fOtDm5BaEzQKhNLOqA5pcB2bCU0L0VFRnoeOpBg==}
'@nodelib/fs.scandir@2.1.5': '@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
@@ -235,6 +238,9 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
'@types/node@24.0.15':
resolution: {integrity: sha512-oaeTSbCef7U/z7rDeJA138xpG3NuKc64/rZ2qmUFkFJmnMsAPaluIifqyWd8hSSMxyP9oie3dLAqYPblag9KgA==}
abstract-logging@2.0.1: abstract-logging@2.0.1:
resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==}
@@ -651,6 +657,9 @@ packages:
engines: {node: '>=14.17'} engines: {node: '>=14.17'}
hasBin: true hasBin: true
undici-types@7.8.0:
resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==}
undici@7.11.0: undici@7.11.0:
resolution: {integrity: sha512-heTSIac3iLhsmZhUCjyS3JQEkZELateufzZuBaVM5RHXdSBMb1LPMQf5x+FH7qjsZYDP0ttAc3nnVpUB+wYbOg==} resolution: {integrity: sha512-heTSIac3iLhsmZhUCjyS3JQEkZELateufzZuBaVM5RHXdSBMb1LPMQf5x+FH7qjsZYDP0ttAc3nnVpUB+wYbOg==}
engines: {node: '>=20.18.1'} engines: {node: '>=20.18.1'}
@@ -815,7 +824,7 @@ snapshots:
- supports-color - supports-color
- utf-8-validate - 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: dependencies:
'@anthropic-ai/sdk': 0.54.0 '@anthropic-ai/sdk': 0.54.0
'@fastify/cors': 11.0.1 '@fastify/cors': 11.0.1
@@ -846,6 +855,10 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5 '@nodelib/fs.scandir': 2.1.5
fastq: 1.19.1 fastq: 1.19.1
'@types/node@24.0.15':
dependencies:
undici-types: 7.8.0
abstract-logging@2.0.1: {} abstract-logging@2.0.1: {}
agent-base@7.1.3: {} agent-base@7.1.3: {}
@@ -1278,6 +1291,8 @@ snapshots:
typescript@5.8.3: {} typescript@5.8.3: {}
undici-types@7.8.0: {}
undici@7.11.0: {} undici@7.11.0: {}
uuid@11.1.0: {} uuid@11.1.0: {}

View File

@@ -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 { get_encoding } from "tiktoken";
import { log } from "./log"; import { log } from "./log";
const enc = get_encoding("cl100k_base"); 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) => { const getUseModel = (req: any, tokenCount: number, config: any) => {
if (req.body.model.includes(",")) { if (req.body.model.includes(",")) {
return req.body.model; return req.body.model;
@@ -26,64 +86,37 @@ const getUseModel = (req: any, tokenCount: number, config: any) => {
log("Using think model for ", req.body.thinking); log("Using think model for ", req.body.thinking);
return config.Router.think; 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.webSearch;
} }
return config.Router!.default; 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; const { messages, system = [], tools }: MessageCreateParamsBase = req.body;
try { try {
let tokenCount = 0; const tokenCount = calculateTokenCount(
if (Array.isArray(messages)) { messages as MessageParam[],
messages.forEach((message) => { system,
if (typeof message.content === "string") { tools as Tool[]
tokenCount += enc.encode(message.content).length; );
} else if (Array.isArray(message.content)) {
message.content.forEach((contentPart) => { let model;
if (contentPart.type === "text") { if (config.CUSTOM_ROUTER_PATH) {
tokenCount += enc.encode(contentPart.text).length; try {
} else if (contentPart.type === "tool_use") { const customRouter = require(config.CUSTOM_ROUTER_PATH);
tokenCount += enc.encode( model = await customRouter(req, config);
JSON.stringify(contentPart.input) } catch (e: any) {
).length; log("failed to load custom router", e.message);
} else if (contentPart.type === "tool_result") {
tokenCount += enc.encode(
typeof contentPart.content === "string"
? contentPart.content
: JSON.stringify(contentPart.content)
).length;
} }
});
} }
}); if (!model) {
model = getUseModel(req, tokenCount, config);
} }
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 (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; req.body.model = model;
} catch (error: any) { } catch (error: any) {
log("Error in router middleware:", error.message); log("Error in router middleware:", error.message);