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",
"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",

View File

@@ -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",

25
pnpm-lock.yaml generated
View File

@@ -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: {}

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 { 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 (!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;
} catch (error: any) {
log("Error in router middleware:", error.message);