From 93fcd77544f287148d0f7f0e23d599807cb9c07e Mon Sep 17 00:00:00 2001 From: "jinhui.li" Date: Tue, 25 Mar 2025 14:51:28 +0800 Subject: [PATCH 1/3] support deepseek-v3-20250324 --- .gitignore | 4 +- CLAUDE.md | 25 - README.md | 50 +- index.mjs | 330 ------- package.json | 32 +- pnpm-lock.yaml | 1142 +++++++++++++++++++++++++ src/constants.ts | 28 + src/deepseek.ts | 140 +++ src/index.ts | 35 + src/middlewares/rewriteToolsPrompt.ts | 34 + router.mjs => src/router copy.ts | 175 ++-- src/server.ts | 159 ++++ src/utils/index.ts | 57 ++ src/utils/log.ts | 27 + src/utils/stream.ts | 268 ++++++ tsconfig.json | 20 + utils.mjs | 9 - 17 files changed, 2050 insertions(+), 485 deletions(-) delete mode 100644 CLAUDE.md delete mode 100644 index.mjs create mode 100644 pnpm-lock.yaml create mode 100644 src/constants.ts create mode 100644 src/deepseek.ts create mode 100644 src/index.ts create mode 100644 src/middlewares/rewriteToolsPrompt.ts rename router.mjs => src/router copy.ts (50%) create mode 100644 src/server.ts create mode 100644 src/utils/index.ts create mode 100644 src/utils/log.ts create mode 100644 src/utils/stream.ts create mode 100644 tsconfig.json delete mode 100644 utils.mjs diff --git a/.gitignore b/.gitignore index 3b66faa..57438d5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ node_modules .env -log.txt \ No newline at end of file +log.txt +.idea +dist \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 084d88c..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,25 +0,0 @@ -# CLAUDE.md - -## Build/Lint/Test Commands -- Install dependencies: `npm i` -- Start server: `node index.mjs` (requires OPENAI_API_KEY, OPENAI_BASE_URL, OPENAI_MODEL env vars) -- Set environment variables: - ```shell - export DISABLE_PROMPT_CACHING=1 - export ANTHROPIC_AUTH_TOKEN="test" - export ANTHROPIC_BASE_URL="http://127.0.0.1:3456" - export API_TIMEOUT_MS=600000 - ``` - -## Code Style Guidelines -- Follow existing formatting in README.md and other files -- Use ES module syntax (`import`/`export`) -- Environment variables are uppercase with underscores -- API endpoints use `/v1/` prefix -- JSON payloads follow strict structure with model, max_tokens, messages, system, etc. -- Include type information in JSON payloads where possible -- Use descriptive variable names -- Keep code modular - separate files for router, index, etc. -- Include example usage/documentation in README -- Use markdown code blocks for code samples -- Document API endpoints and parameters \ No newline at end of file diff --git a/README.md b/README.md index da77e7f..bdce7fa 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,13 @@ > This is a repository for testing routing Claude Code requests to different models. -![demo.png](https://github.com/musistudio/claude-code-reverse/blob/main/screenshoots/demo.png) +![demo.png](https://github.com/musistudio/claude-code-router/blob/main/screenshoots/demo.png) ## Warning! This project is for testing purposes and may consume a lot of tokens! It may also fail to complete tasks! ## Implemented -- [x] Mormal Mode and Router Mode +- [x] Normal Mode and Router Mode - [x] Using the qwen2.5-coder-3b model as the routing dispatcher (since it’s currently free on Alibaba Cloud’s official website) @@ -30,56 +30,22 @@ Thanks to the free qwen2.5-coder-3b model from Alibaba and deepseek’s KV-Cache npm install -g @anthropic-ai/claude-code ``` -1. Clone this repo +1. Install claude-code-router ```shell -git clone https://github.com/musistudio/claude-code-reverse.git +npm install -g @musistudio/claude-code-router ``` -2. Install dependencies +2. Start claude-code-router server ```shell -npm i +claude-code-router ``` -3. Start server - -```shell -# Alternatively, you can create an .env file in the repo directory -# You can refer to the .env.example file to create the .env file - -## disable router -ENABLE_ROUTER=false -OPENAI_API_KEY="" -OPENAI_BASE_URL="" -OPENAI_MODEL="" - -## enable router -ENABLE_ROUTER=true -export TOOL_AGENT_API_KEY="" -export TOOL_AGENT_BASE_URL="" -export TOOL_AGENT_MODEL="qwen-max-2025-01-25" - -export CODER_AGENT_API_KEY="" -export CODER_AGENT_BASE_URL="https://api.deepseek.com" -export CODER_AGENT_MODEL="deepseek-chat" - -export THINK_AGENT_API_KEY="" -export THINK_AGENT_BASE_URL="https://api.deepseek.com" -export THINK_AGENT_MODEL="deepseek-reasoner" - -export ROUTER_AGENT_API_KEY="" -export ROUTER_AGENT_BASE_URL="" -export ROUTER_AGENT_MODEL="qwen2.5-coder-3b-instruct" - -node index.mjs -``` - -4. Set environment variable to start claude code +3. Set environment variable to start claude code ```shell export DISABLE_PROMPT_CACHING=1 -export ANTHROPIC_AUTH_TOKEN="test" export ANTHROPIC_BASE_URL="http://127.0.0.1:3456" export API_TIMEOUT_MS=600000 claude @@ -102,4 +68,4 @@ CODER_AGENT_MODEL and THINK_AGENT_MODEL can use the DeepSeek series of models. The purpose of router mode is to separate tool invocation from coding tasks, enabling the use of inference models like r1, which do not support function calling. -![router mode](https://github.com/musistudio/claude-code-reverse/blob/main/screenshoots/router.png) +![router mode](https://github.com/musistudio/claude-code-router/blob/main/screenshoots/router.png) diff --git a/index.mjs b/index.mjs deleted file mode 100644 index 67bfa57..0000000 --- a/index.mjs +++ /dev/null @@ -1,330 +0,0 @@ -import express from "express"; -import { OpenAI } from "openai"; -import dotenv from "dotenv"; -import { existsSync } from "fs"; -import { writeFile } from "fs/promises"; -import { Router } from "./router.mjs"; -import { getOpenAICommonOptions } from "./utils.mjs"; - -dotenv.config(); -const app = express(); -const port = 3456; -app.use(express.json({ limit: "500mb" })); - -let client; -if (process.env.ENABLE_ROUTER && process.env.ENABLE_ROUTER === "true") { - const router = new Router(); - client = { - call: (data) => { - return router.route(data); - }, - }; -} else { - const openai = new OpenAI({ - apiKey: process.env.OPENAI_API_KEY, - baseURL: process.env.OPENAI_BASE_URL, - ...getOpenAICommonOptions(), - }); - client = { - call: (data) => { - data.model = process.env.OPENAI_MODEL; - return openai.chat.completions.create(data); - }, - }; -} - -app.post("/v1/messages", async (req, res) => { - try { - let { - model, - max_tokens, - messages, - system = [], - temperature, - metadata, - tools, - } = req.body; - - messages = messages.map((item) => { - if (item.content instanceof Array) { - return { - role: item.role, - content: item.content.map((it) => { - const msg = { - ...it, - type: ["tool_result", "tool_use"].includes(it?.type) - ? "text" - : it?.type, - }; - if (msg.type === "text") { - msg.text = it?.content - ? JSON.stringify(it.content) - : it?.text || ""; - delete msg.content; - } - return msg; - }), - }; - } - return { - role: item.role, - content: item.content, - }; - }); - const data = { - model, - messages: [ - ...system.map((item) => ({ - role: "system", - content: item.text, - })), - ...messages, - ], - temperature, - stream: true, - }; - if (tools) { - data.tools = tools - .filter((tool) => !["StickerRequest"].includes(tool.name)) - .map((item) => ({ - type: "function", - function: { - name: item.name, - description: item.description, - parameters: item.input_schema, - }, - })); - } - const completion = await client.call(data); - - // Set SSE response headers - res.setHeader("Content-Type", "text/event-stream"); - res.setHeader("Cache-Control", "no-cache"); - res.setHeader("Connection", "keep-alive"); - - const messageId = "msg_" + Date.now(); - let contentBlockIndex = 0; - let currentContentBlocks = []; - - // Send message_start event - const messageStart = { - type: "message_start", - message: { - id: messageId, - type: "message", - role: "assistant", - content: [], - model, - stop_reason: null, - stop_sequence: null, - usage: { input_tokens: 1, output_tokens: 1 }, - }, - }; - res.write( - `event: message_start\ndata: ${JSON.stringify(messageStart)}\n\n` - ); - - let isToolUse = false; - let toolUseJson = ""; - let currentToolCall = null; - let hasStartedTextBlock = false; - - for await (const chunk of completion) { - const delta = chunk.choices[0].delta; - if (delta.tool_calls && delta.tool_calls.length > 0) { - const toolCall = delta.tool_calls[0]; - - if (!isToolUse) { - // Start new tool call block - isToolUse = true; - currentToolCall = toolCall; - - const toolBlockStart = { - type: "content_block_start", - index: contentBlockIndex, - content_block: { - type: "tool_use", - id: `toolu_${Date.now()}`, - name: toolCall.function.name, - input: {}, - }, - }; - - // Add to content blocks list - currentContentBlocks.push({ - type: "tool_use", - id: toolBlockStart.content_block.id, - name: toolCall.function.name, - input: {}, - }); - - res.write( - `event: content_block_start\ndata: ${JSON.stringify( - toolBlockStart - )}\n\n` - ); - toolUseJson = ""; - } - - // Stream tool call JSON - if (toolCall.function.arguments) { - const jsonDelta = { - type: "content_block_delta", - index: contentBlockIndex, - delta: { - type: "input_json_delta", - partial_json: toolCall.function.arguments, - }, - }; - - toolUseJson += toolCall.function.arguments; - - // Try to parse complete JSON and update content block - try { - const parsedJson = JSON.parse(toolUseJson); - currentContentBlocks[contentBlockIndex].input = parsedJson; - } catch (e) { - // JSON not yet complete, continue accumulating - } - - res.write( - `event: content_block_delta\ndata: ${JSON.stringify(jsonDelta)}\n\n` - ); - } - } else if (delta.content) { - // Handle regular text content - if (isToolUse) { - // End previous tool call block - const contentBlockStop = { - type: "content_block_stop", - index: contentBlockIndex, - }; - - res.write( - `event: content_block_stop\ndata: ${JSON.stringify( - contentBlockStop - )}\n\n` - ); - contentBlockIndex++; - isToolUse = false; - } - - if (!delta.content) continue; - - // If text block not yet started, send content_block_start - if (!hasStartedTextBlock) { - const textBlockStart = { - type: "content_block_start", - index: contentBlockIndex, - content_block: { - type: "text", - text: "", - }, - }; - - // Add to content blocks list - currentContentBlocks.push({ - type: "text", - text: "", - }); - - res.write( - `event: content_block_start\ndata: ${JSON.stringify( - textBlockStart - )}\n\n` - ); - hasStartedTextBlock = true; - } - - // Send regular text content - const contentDelta = { - type: "content_block_delta", - index: contentBlockIndex, - delta: { - type: "text_delta", - text: delta.content, - }, - }; - - // Update content block text - if (currentContentBlocks[contentBlockIndex]) { - currentContentBlocks[contentBlockIndex].text += delta.content; - } - - res.write( - `event: content_block_delta\ndata: ${JSON.stringify( - contentDelta - )}\n\n` - ); - } - } - - // Close last content block - const contentBlockStop = { - type: "content_block_stop", - index: contentBlockIndex, - }; - - res.write( - `event: content_block_stop\ndata: ${JSON.stringify(contentBlockStop)}\n\n` - ); - - // Send message_delta event with appropriate stop_reason - const messageDelta = { - type: "message_delta", - delta: { - stop_reason: isToolUse ? "tool_use" : "end_turn", - stop_sequence: null, - content: currentContentBlocks, - }, - usage: { input_tokens: 100, output_tokens: 150 }, - }; - - res.write( - `event: message_delta\ndata: ${JSON.stringify(messageDelta)}\n\n` - ); - - // Send message_stop event - const messageStop = { - type: "message_stop", - }; - - res.write(`event: message_stop\ndata: ${JSON.stringify(messageStop)}\n\n`); - res.end(); - } catch (error) { - console.error("Error in streaming response:", error); - res.status(400).json({ - status: "error", - message: error.message, - }); - } -}); - -async function initializeClaudeConfig() { - const homeDir = process.env.HOME; - const configPath = `${homeDir}/.claude.json`; - if (!existsSync(configPath)) { - const userID = Array.from( - { length: 64 }, - () => Math.random().toString(16)[2] - ).join(""); - const configContent = { - numStartups: 184, - autoUpdaterStatus: "enabled", - userID, - hasCompletedOnboarding: true, - lastOnboardingVersion: "0.2.9", - projects: {}, - }; - await writeFile(configPath, JSON.stringify(configContent, null, 2)); - } -} - -async function run() { - await initializeClaudeConfig(); - - app.listen(port, "127.0.0.1", () => { - console.log(`Example app listening on port ${port}`); - }); -} -run(); diff --git a/package.json b/package.json index 8e81af9..9b043b3 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,35 @@ { "name": "claude-code-router", "version": "1.0.0", - "description": "You can switch the API endpoint by modifying the ANTHROPIC_BASE_URL environment variable.", - "main": "index.mjs", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "start": "node index.mjs" + "description": "Use Claude Code without an Anthropics account and route it to another LLM provider", + "bin": { + "claude-code-router": "./dist/cli.js" }, - "keywords": [], - "author": "", - "license": "ISC", + "scripts": { + "start": "node dist/cli.js", + "build": "tsc && esbuild src/index.ts --bundle --platform=node --outfile=dist/cli.js" + }, + "keywords": ["claude", "code", "router", "llm", "anthropic"], + "author": "musistudio", + "license": "MIT", "dependencies": { + "@anthropic-ai/claude-code": "^0.2.53", + "@anthropic-ai/sdk": "^0.39.0", "dotenv": "^16.4.7", "express": "^4.21.2", "https-proxy-agent": "^7.0.6", "openai": "^4.85.4" + }, + "devDependencies": { + "@types/express": "^5.0.0", + "esbuild": "^0.25.1", + "typescript": "^5.8.2" + }, + "publishConfig": { + "ignore": [ + "!build/", + "src/", + "screenshots/" + ] } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..4099b1f --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,1142 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + '@anthropic-ai/claude-code': + specifier: ^0.2.53 + version: 0.2.53 + '@anthropic-ai/sdk': + specifier: ^0.39.0 + version: 0.39.0 + dotenv: + specifier: ^16.4.7 + version: 16.4.7 + express: + specifier: ^4.21.2 + version: 4.21.2 + https-proxy-agent: + specifier: ^7.0.6 + version: 7.0.6 + openai: + specifier: ^4.85.4 + version: 4.86.1 + +devDependencies: + '@types/express': + specifier: ^5.0.0 + version: 5.0.0 + esbuild: + specifier: ^0.25.1 + version: 0.25.1 + typescript: + specifier: ^5.8.2 + version: 5.8.2 + +packages: + + /@anthropic-ai/claude-code@0.2.53: + resolution: {integrity: sha512-DKXGjSsu2+rc1GaAdOjRqD7fMLvyQgwi/sqf6lLHWQAarwYxR/ahbSheu7h1Ub0wm0htnuIqgNnmNZUM43w/3Q==} + engines: {node: '>=18.0.0'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@img/sharp-darwin-arm64': 0.33.5 + '@img/sharp-linux-arm': 0.33.5 + '@img/sharp-linux-x64': 0.33.5 + '@img/sharp-win32-x64': 0.33.5 + dev: false + + /@anthropic-ai/sdk@0.39.0: + resolution: {integrity: sha512-eMyDIPRZbt1CCLErRCi3exlAvNkBtRe+kW5vvJyef93PmNr/clstYgHhtvmkxN82nlKgzyGPCyGxrm0JQ1ZIdg==} + dependencies: + '@types/node': 18.19.78 + '@types/node-fetch': 2.6.12 + abort-controller: 3.0.0 + agentkeepalive: 4.6.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + dev: false + + /@esbuild/aix-ppc64@0.25.1: + resolution: {integrity: sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm64@0.25.1: + resolution: {integrity: sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm@0.25.1: + resolution: {integrity: sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-x64@0.25.1: + resolution: {integrity: sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-arm64@0.25.1: + resolution: {integrity: sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-x64@0.25.1: + resolution: {integrity: sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-arm64@0.25.1: + resolution: {integrity: sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-x64@0.25.1: + resolution: {integrity: sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm64@0.25.1: + resolution: {integrity: sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm@0.25.1: + resolution: {integrity: sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ia32@0.25.1: + resolution: {integrity: sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-loong64@0.25.1: + resolution: {integrity: sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-mips64el@0.25.1: + resolution: {integrity: sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ppc64@0.25.1: + resolution: {integrity: sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-riscv64@0.25.1: + resolution: {integrity: sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-s390x@0.25.1: + resolution: {integrity: sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-x64@0.25.1: + resolution: {integrity: sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/netbsd-arm64@0.25.1: + resolution: {integrity: sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/netbsd-x64@0.25.1: + resolution: {integrity: sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openbsd-arm64@0.25.1: + resolution: {integrity: sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openbsd-x64@0.25.1: + resolution: {integrity: sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/sunos-x64@0.25.1: + resolution: {integrity: sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-arm64@0.25.1: + resolution: {integrity: sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-ia32@0.25.1: + resolution: {integrity: sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-x64@0.25.1: + resolution: {integrity: sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@img/sharp-darwin-arm64@0.33.5: + resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + requiresBuild: true + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.0.4 + dev: false + optional: true + + /@img/sharp-libvips-darwin-arm64@1.0.4: + resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@img/sharp-libvips-linux-arm@1.0.5: + resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@img/sharp-libvips-linux-x64@1.0.4: + resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@img/sharp-linux-arm@0.33.5: + resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + requiresBuild: true + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.0.5 + dev: false + optional: true + + /@img/sharp-linux-x64@0.33.5: + resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + requiresBuild: true + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.0.4 + dev: false + optional: true + + /@img/sharp-win32-x64@0.33.5: + resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@types/body-parser@1.19.5: + resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} + dependencies: + '@types/connect': 3.4.38 + '@types/node': 18.19.78 + dev: true + + /@types/connect@3.4.38: + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + dependencies: + '@types/node': 18.19.78 + dev: true + + /@types/express-serve-static-core@5.0.6: + resolution: {integrity: sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==} + dependencies: + '@types/node': 18.19.78 + '@types/qs': 6.9.18 + '@types/range-parser': 1.2.7 + '@types/send': 0.17.4 + dev: true + + /@types/express@5.0.0: + resolution: {integrity: sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==} + dependencies: + '@types/body-parser': 1.19.5 + '@types/express-serve-static-core': 5.0.6 + '@types/qs': 6.9.18 + '@types/serve-static': 1.15.7 + dev: true + + /@types/http-errors@2.0.4: + resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} + dev: true + + /@types/mime@1.3.5: + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + dev: true + + /@types/node-fetch@2.6.12: + resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==} + dependencies: + '@types/node': 18.19.78 + form-data: 4.0.2 + dev: false + + /@types/node@18.19.78: + resolution: {integrity: sha512-m1ilZCTwKLkk9rruBJXFeYN0Bc5SbjirwYX/Td3MqPfioYbgun3IvK/m8dQxMCnrPGZPg1kvXjp3SIekCN/ynw==} + dependencies: + undici-types: 5.26.5 + + /@types/qs@6.9.18: + resolution: {integrity: sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==} + dev: true + + /@types/range-parser@1.2.7: + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + dev: true + + /@types/send@0.17.4: + resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} + dependencies: + '@types/mime': 1.3.5 + '@types/node': 18.19.78 + dev: true + + /@types/serve-static@1.15.7: + resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} + dependencies: + '@types/http-errors': 2.0.4 + '@types/node': 18.19.78 + '@types/send': 0.17.4 + dev: true + + /abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + dependencies: + event-target-shim: 5.0.1 + dev: false + + /accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + dev: false + + /agent-base@7.1.3: + resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} + engines: {node: '>= 14'} + dev: false + + /agentkeepalive@4.6.0: + resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} + engines: {node: '>= 8.0.0'} + dependencies: + humanize-ms: 1.2.1 + dev: false + + /array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + dev: false + + /asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + dev: false + + /body-parser@1.20.3: + resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.13.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: false + + /bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + dev: false + + /call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + dev: false + + /call-bound@1.0.3: + resolution: {integrity: sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + dev: false + + /combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + dependencies: + delayed-stream: 1.0.0 + dev: false + + /content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + dev: false + + /cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + dev: false + + /cookie@0.7.1: + resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} + engines: {node: '>= 0.6'} + dev: false + + /debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.0.0 + dev: false + + /debug@4.4.0: + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.3 + dev: false + + /delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + dev: false + + /depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + dev: false + + /destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dev: false + + /dotenv@16.4.7: + resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} + engines: {node: '>=12'} + dev: false + + /dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + dev: false + + /ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + dev: false + + /encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + dev: false + + /encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + dev: false + + /es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + dev: false + + /es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + dev: false + + /es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + dev: false + + /es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + dev: false + + /esbuild@0.25.1: + resolution: {integrity: sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==} + engines: {node: '>=18'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.1 + '@esbuild/android-arm': 0.25.1 + '@esbuild/android-arm64': 0.25.1 + '@esbuild/android-x64': 0.25.1 + '@esbuild/darwin-arm64': 0.25.1 + '@esbuild/darwin-x64': 0.25.1 + '@esbuild/freebsd-arm64': 0.25.1 + '@esbuild/freebsd-x64': 0.25.1 + '@esbuild/linux-arm': 0.25.1 + '@esbuild/linux-arm64': 0.25.1 + '@esbuild/linux-ia32': 0.25.1 + '@esbuild/linux-loong64': 0.25.1 + '@esbuild/linux-mips64el': 0.25.1 + '@esbuild/linux-ppc64': 0.25.1 + '@esbuild/linux-riscv64': 0.25.1 + '@esbuild/linux-s390x': 0.25.1 + '@esbuild/linux-x64': 0.25.1 + '@esbuild/netbsd-arm64': 0.25.1 + '@esbuild/netbsd-x64': 0.25.1 + '@esbuild/openbsd-arm64': 0.25.1 + '@esbuild/openbsd-x64': 0.25.1 + '@esbuild/sunos-x64': 0.25.1 + '@esbuild/win32-arm64': 0.25.1 + '@esbuild/win32-ia32': 0.25.1 + '@esbuild/win32-x64': 0.25.1 + dev: true + + /escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + dev: false + + /etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + dev: false + + /event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + dev: false + + /express@4.21.2: + resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} + engines: {node: '>= 0.10.0'} + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.3 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.1 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.1 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.12 + proxy-addr: 2.0.7 + qs: 6.13.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.0 + serve-static: 1.16.2 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + dev: false + + /finalhandler@1.3.1: + resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} + engines: {node: '>= 0.8'} + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: false + + /form-data-encoder@1.7.2: + resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==} + dev: false + + /form-data@4.0.2: + resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + mime-types: 2.1.35 + dev: false + + /formdata-node@4.4.1: + resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} + engines: {node: '>= 12.20'} + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 4.0.0-beta.3 + dev: false + + /forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + dev: false + + /fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + dev: false + + /function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + dev: false + + /get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + dev: false + + /get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + dev: false + + /gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + dev: false + + /has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + dev: false + + /has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.1.0 + dev: false + + /hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + dependencies: + function-bind: 1.1.2 + dev: false + + /http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + dev: false + + /https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + dependencies: + agent-base: 7.1.3 + debug: 4.4.0 + transitivePeerDependencies: + - supports-color + dev: false + + /humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + dependencies: + ms: 2.1.3 + dev: false + + /iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: false + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: false + + /ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + dev: false + + /math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + dev: false + + /media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + dev: false + + /merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + dev: false + + /methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + dev: false + + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + dev: false + + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: false + + /mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + dev: false + + /ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + dev: false + + /ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + dev: false + + /negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + dev: false + + /node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + dev: false + + /node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + dependencies: + whatwg-url: 5.0.0 + dev: false + + /object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + dev: false + + /on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + dependencies: + ee-first: 1.1.1 + dev: false + + /openai@4.86.1: + resolution: {integrity: sha512-x3iCLyaC3yegFVZaxOmrYJjitKxZ9hpVbLi+ZlT5UHuHTMlEQEbKXkGOM78z9qm2T5GF+XRUZCP2/aV4UPFPJQ==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.23.8 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + dependencies: + '@types/node': 18.19.78 + '@types/node-fetch': 2.6.12 + abort-controller: 3.0.0 + agentkeepalive: 4.6.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + dev: false + + /parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + dev: false + + /path-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + dev: false + + /proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + dev: false + + /qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} + engines: {node: '>=0.6'} + dependencies: + side-channel: 1.1.0 + dev: false + + /range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + dev: false + + /raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + dev: false + + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: false + + /safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + dev: false + + /send@0.19.0: + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} + engines: {node: '>= 0.8.0'} + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + dev: false + + /serve-static@1.16.2: + resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} + engines: {node: '>= 0.8.0'} + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.0 + transitivePeerDependencies: + - supports-color + dev: false + + /setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + dev: false + + /side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + dev: false + + /side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + dev: false + + /side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + dev: false + + /side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + dev: false + + /statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + dev: false + + /toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + dev: false + + /tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + dev: false + + /type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + dev: false + + /typescript@5.8.2: + resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==} + engines: {node: '>=14.17'} + hasBin: true + dev: true + + /undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + /unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + dev: false + + /utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + dev: false + + /vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + dev: false + + /web-streams-polyfill@4.0.0-beta.3: + resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} + engines: {node: '>= 14'} + dev: false + + /webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + dev: false + + /whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + dev: false diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..9255079 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,28 @@ +import path from "node:path"; +import os from "node:os"; + +export const HOME_DIR = path.join(os.homedir(), ".claude-code-router"); + +export const CONFIG_FILE = `${HOME_DIR}/config.json`; + +export const PROMPTS_DIR = `${HOME_DIR}/prompts`; + +export const DEFAULT_CONFIG = { + log: false, + ENABLE_ROUTER: true, + OPENAI_API_KEY: "", + OPENAI_BASE_URL: "https://openrouter.ai/api/v1", + OPENAI_MODEL: "openai/o3-mini", + + CODER_AGENT_API_KEY: "", + CODER_AGENT_BASE_URL: "https://api.deepseek.com", + CODER_AGENT_MODEL: "deepseek-chat", + + THINK_AGENT_API_KEY: "", + THINK_AGENT_BASE_URL: "https://api.deepseek.com", + THINK_AGENT_MODEL: "deepseek-reasoner", + + ROUTER_AGENT_API_KEY: "", + ROUTER_AGENT_BASE_URL: "https://api.deepseek.com", + ROUTER_AGENT_MODEL: "deepseek-chat", +}; diff --git a/src/deepseek.ts b/src/deepseek.ts new file mode 100644 index 0000000..afc0c9d --- /dev/null +++ b/src/deepseek.ts @@ -0,0 +1,140 @@ +import { OpenAI } from "openai"; +import { createClient } from "./utils"; +import { log } from "./utils/log"; +export interface BaseRouter { + name: string; + description: string; + run: ( + args: OpenAI.Chat.Completions.ChatCompletionCreateParams + ) => Promise; +} + +const thinkRouter: BaseRouter = { + name: "think", + description: `This agent is used solely for complex reasoning and thinking tasks. It should not be called for information retrieval or repetitive, frequent requests. Only use this agent for tasks that require deep analysis or problem-solving. If there is an existing result from the Thinker agent, do not call this agent again.你只负责深度思考以拆分任务,不需要进行任何的编码和调用工具。最后讲拆分的步骤按照顺序返回。比如\n1. xxx\n2. xxx\n3. xxx`, + run(args) { + const client = createClient({ + apiKey: process.env.THINK_AGENT_API_KEY, + baseURL: process.env.THINK_AGENT_BASE_URL, + }); + const messages = JSON.parse(JSON.stringify(args.messages)); + messages.forEach((msg: any) => { + if (Array.isArray(msg.content)) { + msg.content = JSON.stringify(msg.content); + } + }); + + let startIdx = messages.findIndex((msg: any) => msg.role !== "system"); + if (startIdx === -1) startIdx = messages.length; + + for (let i = startIdx; i < messages.length; i++) { + const expectedRole = (i - startIdx) % 2 === 0 ? "user" : "assistant"; + messages[i].role = expectedRole; + } + + if ( + messages.length > 0 && + messages[messages.length - 1].role === "assistant" + ) { + messages.push({ + role: "user", + content: + "Please follow the instructions provided above to resolve the issue.", + }); + } + delete args.tools; + return client.chat.completions.create({ + ...args, + messages, + model: process.env.THINK_AGENT_MODEL as string, + }); + }, +}; + +export class Router { + routers: BaseRouter[]; + client: OpenAI; + constructor() { + this.routers = [thinkRouter]; + this.client = createClient({ + apiKey: process.env.ROUTER_AGENT_API_KEY, + baseURL: process.env.ROUTER_AGENT_BASE_URL, + }); + } + async route( + args: OpenAI.Chat.Completions.ChatCompletionCreateParams + ): Promise { + log(`Request Router: ${JSON.stringify(args, null, 2)}`); + const res: OpenAI.Chat.Completions.ChatCompletion = + await this.client.chat.completions.create({ + ...args, + messages: [ + ...args.messages, + { + role: "system", + content: `## **Guidelines:** +- **Trigger the "think" mode when the user's request involves deep thinking, complex reasoning, or multi-step analysis.** +- **Criteria:** + - Involves multi-layered logical reasoning or causal analysis + - Requires establishing connections or pattern recognition between different pieces of information + - Involves cross-domain knowledge integration or weighing multiple possibilities + - Requires creative thinking or non-direct inference +### **Format requirements:** +- When you need to trigger the "think" mode, return the following JSON format: +\`\`\`json +{ + "use": "think" +} +\`\`\` +`, + }, + ], + model: process.env.ROUTER_AGENT_MODEL as string, + stream: false, + }); + let result; + try { + const text = res.choices[0].message.content; + if (!text) { + throw new Error("No text"); + } + result = JSON.parse( + text.slice(text.indexOf("{"), text.lastIndexOf("}") + 1) + ); + } catch (e) { + (res.choices[0] as any).delta = res.choices[0].message; + log(`No Router: ${JSON.stringify(res.choices[0].message)}`); + return [res]; + } + const router = this.routers.find((item) => item.name === result.use); + if (!router) { + (res.choices[0] as any).delta = res.choices[0].message; + log(`No Router: ${JSON.stringify(res.choices[0].message)}`); + return [res]; + } + log(`Use Router: ${router.name}`); + if (router.name === "think") { + const agentResult = await router.run({ + ...args, + stream: false, + }); + try { + args.messages.push({ + role: "user", + content: + `${router.name} Agent Result: ` + + agentResult.choices[0].message.content, + }); + log( + `${router.name} Agent Result: ` + + agentResult.choices[0].message.content + ); + return await this.route(args); + } catch (error) { + console.log(agentResult); + throw error; + } + } + return router.run(args); + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..08c5ca8 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,35 @@ +import { existsSync } from "fs"; +import { writeFile } from "fs/promises"; +import { initConfig, initDir } from "./utils"; +import { createServer } from "./server"; +import { rewriteToolsPrompt } from "./middlewares/rewriteToolsPrompt"; + +async function initializeClaudeConfig() { + const homeDir = process.env.HOME; + const configPath = `${homeDir}/.claude.json`; + if (!existsSync(configPath)) { + const userID = Array.from( + { length: 64 }, + () => Math.random().toString(16)[2] + ).join(""); + const configContent = { + numStartups: 184, + autoUpdaterStatus: "enabled", + userID, + hasCompletedOnboarding: true, + lastOnboardingVersion: "0.2.9", + projects: {}, + }; + await writeFile(configPath, JSON.stringify(configContent, null, 2)); + } +} + +async function run() { + await initializeClaudeConfig(); + await initDir(); + await initConfig(); + const server = createServer(3456); + server.useMiddleware(rewriteToolsPrompt); + server.start(); +} +run(); diff --git a/src/middlewares/rewriteToolsPrompt.ts b/src/middlewares/rewriteToolsPrompt.ts new file mode 100644 index 0000000..699e52d --- /dev/null +++ b/src/middlewares/rewriteToolsPrompt.ts @@ -0,0 +1,34 @@ +import { Request, Response, NextFunction } from "express"; +import { readFile, access } from "node:fs/promises"; +import { join } from "node:path"; +import { PROMPTS_DIR } from "../constants"; + +const getPrompt = async (name: string) => { + try { + const promptPath = join(PROMPTS_DIR, `${name}.md`); + await access(promptPath); + const prompt = await readFile(promptPath, "utf-8"); + return prompt; + } catch { + return null; + } +}; + +export const rewriteToolsPrompt = async ( + req: Request, + res: Response, + next: NextFunction +) => { + const { tools } = req.body; + if (!Array.isArray(tools)) { + next(); + return; + } + for (const tool of tools) { + const prompt = await getPrompt(tool.name); + if (prompt) { + tool.description = prompt; + } + } + next(); +}; diff --git a/router.mjs b/src/router copy.ts similarity index 50% rename from router.mjs rename to src/router copy.ts index 27e6c05..9516ada 100644 --- a/router.mjs +++ b/src/router copy.ts @@ -1,38 +1,21 @@ import { OpenAI } from "openai"; -import { getOpenAICommonOptions } from "./utils.mjs"; +import { createClient } from "./utils"; +import { log } from "./utils/log"; +export interface BaseRouter { + name: string; + description: string; + run: ( + args: OpenAI.Chat.Completions.ChatCompletionCreateParams + ) => Promise; +} -const useToolRouter = { - name: "use-tool", - description: `This agent can call user-specified tools to perform tasks. The user provides a list of tools to be used, and the agent integrates these tools to complete the specified tasks efficiently. The agent follows user instructions and ensures proper tool utilization for each request`, - run(args) { - const client = new OpenAI({ - apiKey: process.env.TOOL_AGENT_API_KEY, - baseURL: process.env.TOOL_AGENT_BASE_URL, - ...getOpenAICommonOptions(), - }); - return client.chat.completions.create({ - ...args, - messages: [ - ...args.messages, - { - role: "system", - content: - "You need to select the appropriate tool for the task based on the user’s request. Review the requirements and choose the tool that fits the task best.", - }, - ], - model: process.env.TOOL_AGENT_MODEL, - }); - }, -}; - -const coderRouter = { +const coderRouter: BaseRouter = { name: "coder", - description: `This agent is solely responsible for helping users write code. This agent could not call tools. This agent is used for writing and modifying code when the user provides clear and specific coding requirements. For example, tasks like implementing a quicksort algorithm in JavaScript or creating an HTML layout. If the user's request is unclear or cannot be directly translated into code, please route the task to 'Thinker' first for clarification or further processing.`, + description: `This agent is solely responsible for helping users write code. This agent could not call tools. This agent is used for writing and modifying code when the user provides clear and specific coding requirements. For example, tasks like implementing a quicksort algorithm in JavaScript or creating an HTML layout. If the user's request is unclear or cannot be directly translated into code, please route the task to 'think' first for clarification or further processing.`, run(args) { - const client = new OpenAI({ + const client = createClient({ apiKey: process.env.CODER_AGENT_API_KEY, baseURL: process.env.CODER_AGENT_BASE_URL, - ...getOpenAICommonOptions(), }); delete args.tools; args.messages.forEach((item) => { @@ -50,28 +33,51 @@ const coderRouter = { "You are a code writer who helps users write code based on their specific requirements. You create algorithms, implement functionality, and build structures according to the clear instructions provided by the user. Your focus is solely on writing code, ensuring that the task is completed accurately and efficiently.", }, ], - model: process.env.CODER_AGENT_MODEL, + model: process.env.CODER_AGENT_MODEL as string, }); }, }; -const thinkRouter = { - name: "thinker", - description: `This agent is used solely for complex reasoning and thinking tasks. It should not be called for information retrieval or repetitive, frequent requests. Only use this agent for tasks that require deep analysis or problem-solving. If there is an existing result from the Thinker agent, do not call this agent again.`, + +const useToolRouter: BaseRouter = { + name: "use-tool", + description: `This agent can call user-specified tools to perform tasks. The user provides a list of tools to be used, and the agent integrates these tools to complete the specified tasks efficiently. The agent follows user instructions and ensures proper tool utilization for each request`, run(args) { - const client = new OpenAI({ + const client = createClient({ + apiKey: process.env.TOOL_AGENT_API_KEY, + baseURL: process.env.TOOL_AGENT_BASE_URL, + }); + return client.chat.completions.create({ + ...args, + messages: [ + ...args.messages, + { + role: "system", + content: + "You need to select the appropriate tool for the task based on the user’s request. Review the requirements and choose the tool that fits the task best.", + }, + ], + model: process.env.TOOL_AGENT_MODEL as string, + }); + }, +}; + +const thinkRouter: BaseRouter = { + name: "think", + description: `This agent is used solely for complex reasoning and thinking tasks. It should not be called for information retrieval or repetitive, frequent requests. Only use this agent for tasks that require deep analysis or problem-solving. If there is an existing result from the Thinker agent, do not call this agent again.你只负责深度思考以拆分任务,不需要进行任何的编码和调用工具。最后讲拆分的步骤按照顺序返回。比如\n1. xxx\n2. xxx\n3. xxx`, + run(args) { + const client = createClient({ apiKey: process.env.THINK_AGENT_API_KEY, baseURL: process.env.THINK_AGENT_BASE_URL, - ...getOpenAICommonOptions(), }); const messages = JSON.parse(JSON.stringify(args.messages)); - messages.forEach((msg) => { + messages.forEach((msg: any) => { if (Array.isArray(msg.content)) { msg.content = JSON.stringify(msg.content); } }); - let startIdx = messages.findIndex((msg) => msg.role !== "system"); + let startIdx = messages.findIndex((msg: any) => msg.role !== "system"); if (startIdx === -1) startIdx = messages.length; for (let i = startIdx; i < messages.length; i++) { @@ -93,73 +99,102 @@ const thinkRouter = { return client.chat.completions.create({ ...args, messages, - model: process.env.THINK_AGENT_MODEL, + model: process.env.THINK_AGENT_MODEL as string, }); }, }; export class Router { + routers: BaseRouter[]; + client: OpenAI; constructor() { - this.routers = [useToolRouter, coderRouter, thinkRouter]; - this.client = new OpenAI({ + this.routers = [coderRouter, useToolRouter, thinkRouter]; + this.client = createClient({ apiKey: process.env.ROUTER_AGENT_API_KEY, baseURL: process.env.ROUTER_AGENT_BASE_URL, - ...getOpenAICommonOptions(), }); } - async route(args) { - const res = await this.client.chat.completions.create({ - ...args, - messages: [ - ...args.messages, - { - role: "system", - content: `You are an AI task router that receives user requests and forwards them to the appropriate AI models for task handling. You do not process any requests directly but are responsible for understanding the user's request and choosing the correct router based on the task and necessary steps. The available routers are: ${JSON.stringify( - this.routers.map((router) => { - return { - name: router.name, - description: router.description, - }; - }) - )}. Each router is designated for specific types of tasks, and you ensure that the request is routed accordingly for efficient processing. Use the appropriate router based on the user’s request: + async route( + args: OpenAI.Chat.Completions.ChatCompletionCreateParams + ): Promise { + log(`Route: ${JSON.stringify(args, null, 2)}`); + const res: OpenAI.Chat.Completions.ChatCompletion = + await this.client.chat.completions.create({ + ...args, + messages: [ + ...args.messages, + { + role: "system", + content: `You are an AI task router and executor, responsible for understanding user requests and directing them to the appropriate processing mode or tool based on the task type and requirements. Your main responsibility is to determine the nature of the request, execute the task when possible, and respond appropriately. -If external tools are needed to gather more information, use the 'use-tool' router. -If the task involves writing code, use the 'coder' router. -If deep reasoning or analysis is required to break down steps, use the 'thinker' router. -Instead, format your response as a JSON object with one field: 'use' (string)`, - }, - ], - model: process.env.ROUTER_AGENT_MODEL, - stream: false, - }); +### **Guidelines:** +- **If an external tool is required to complete the task (such as searching for information, generating images, or modifying code), route the task to \`use-tool\` rather than handling it directly.** +- If the task requires generating an image, route to \`use-tool\` and specify the image generation tool. +- If the task requires searching for information, route to \`use-tool\` and specify the search tool. +- If the task requires modifying or executing code, route to \`use-tool\` and specify the code handling tool. +- **Do NOT execute the tool action directly; always trigger it through \`use-tool\`.** + +- **If the user is chatting casually or having a general conversation, respond naturally and conversationally. Improving the user experience through friendly interactions is one of your main responsibilities.** + +- **If the user's request involves deep thinking, complex reasoning, or multi-step analysis, use the "think" mode to break down and solve the problem.** + +- **If the user's request involves coding or technical implementation, use the "coder" mode to generate or modify code.** + - **After generating the code, if the task requires applying or integrating the code, route to \`use-tool\` and specify the code execution tool.** + - **Do NOT re-trigger "coder" to apply code — route to \`use-tool\` instead.** + +### **Format requirements:** +- When you need to trigger a specific mode (such as "think", "coder", or "use-tool"), return the following JSON format: + +### IMPORTANT: +- 你不能也不会调用BatchTool,如果你需要使用工具请路由到\`use-tool\`,由\`use-tool\`来调用BatchTool。 + +\`\`\`json +{ + "use": "", +} +\`\`\` +`, + }, + ], + model: process.env.ROUTER_AGENT_MODEL as string, + stream: false, + }); let result; try { const text = res.choices[0].message.content; + if (!text) { + throw new Error("No text"); + } result = JSON.parse( text.slice(text.indexOf("{"), text.lastIndexOf("}") + 1) ); } catch (e) { - console.log(e); - res.choices[0].delta = res.choices[0].message; + (res.choices[0] as any).delta = res.choices[0].message; return [res]; } const router = this.routers.find((item) => item.name === result.use); if (!router) { - res.choices[0].delta = res.choices[0].message; + (res.choices[0] as any).delta = res.choices[0].message; + log(`No Router: ${JSON.stringify(res.choices[0].message)}`); return [res]; } - if (router.name === "thinker" || router.name === "coder") { + log(`Use Router: ${router.name}`); + if (router.name === "think" || router.name === "coder") { const agentResult = await router.run({ ...args, stream: false, }); try { args.messages.push({ - role: "assistant", + role: "user", content: `${router.name} Agent Result: ` + agentResult.choices[0].message.content, }); + log( + `${router.name} Agent Result: ` + + agentResult.choices[0].message.content + ); return await this.route(args); } catch (error) { console.log(agentResult); diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..328ebf8 --- /dev/null +++ b/src/server.ts @@ -0,0 +1,159 @@ +import express, { RequestHandler } from "express"; +import { + ContentBlockParam, + MessageCreateParamsBase, +} from "@anthropic-ai/sdk/resources/messages"; +import { OpenAI } from "openai"; +import { Router } from "./deepseek"; +import { getOpenAICommonOptions } from "./utils"; +import { streamOpenAIResponse } from "./utils/stream"; + +interface Client { + call: ( + data: OpenAI.Chat.Completions.ChatCompletionCreateParams + ) => Promise; +} + +interface Server { + app: express.Application; + useMiddleware: (middleware: RequestHandler) => void; + start: () => void; +} + +export const createServer = (port: number): Server => { + const app = express(); + app.use(express.json({ limit: "500mb" })); + + let client: Client; + if (process.env.ENABLE_ROUTER && process.env.ENABLE_ROUTER === "true") { + const router = new Router(); + client = { + call: (data) => { + return router.route(data); + }, + }; + } else { + const openai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, + baseURL: process.env.OPENAI_BASE_URL, + ...getOpenAICommonOptions(), + }); + client = { + call: (data) => { + if (process.env.OPENAI_MODEL) { + data.model = process.env.OPENAI_MODEL; + } + return openai.chat.completions.create(data); + }, + }; + } + + app.post("/v1/messages", async (req, res) => { + try { + let { + model, + max_tokens, + messages, + system = [], + temperature, + metadata, + tools, + }: MessageCreateParamsBase = req.body; + + const openAIMessages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = + messages.map((item) => { + if (item.content instanceof Array) { + return { + role: item.role, + content: item.content + .map((it: ContentBlockParam) => { + if (it.type === "text") { + return typeof it.text === "string" + ? it.text + : JSON.stringify(it); + } + return JSON.stringify(it); + }) + .join(""), + } as OpenAI.Chat.Completions.ChatCompletionMessageParam; + } + return { + role: item.role, + content: + typeof item.content === "string" + ? item.content + : JSON.stringify(item.content), + }; + }); + const systemMessages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = + Array.isArray(system) + ? system.map((item) => ({ + role: "system", + content: item.text, + })) + : [{ role: "system", content: system }]; + const data: OpenAI.Chat.Completions.ChatCompletionCreateParams = { + model, + messages: [...systemMessages, ...openAIMessages], + temperature, + stream: true, + }; + if (tools) { + data.tools = tools + .filter((tool) => !["StickerRequest"].includes(tool.name)) + .map((item: any) => ({ + type: "function", + function: { + name: item.name, + description: item.description, + parameters: item.input_schema, + }, + })); + } + res.setHeader("Content-Type", "text/event-stream"); + res.setHeader("Cache-Control", "no-cache"); + res.setHeader("Connection", "keep-alive"); + try { + const completion = await client.call(data); + await streamOpenAIResponse(res, completion, model); + } catch (e) { + console.error("Error in OpenAI API call:", e); + } + } catch (error) { + console.error("Error in request processing:", error); + const errorCompletion: AsyncIterable = + { + async *[Symbol.asyncIterator]() { + yield { + id: `error_${Date.now()}`, + created: Math.floor(Date.now() / 1000), + model: "gpt-3.5-turbo", + object: "chat.completion.chunk", + choices: [ + { + index: 0, + delta: { + content: `Error: ${(error as Error).message}`, + }, + finish_reason: "stop", + }, + ], + }; + }, + }; + await streamOpenAIResponse(res, errorCompletion, "gpt-3.5-turbo"); + } + }); + + return { + app, + useMiddleware: (middleware: RequestHandler) => { + app.use("/v1/messages", middleware); + }, + start: () => { + app.listen(port, () => { + console.log(`Server is running on port ${port}`); + }); + }, + }; +}; diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..57478a4 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,57 @@ +import { HttpsProxyAgent } from "https-proxy-agent"; +import OpenAI, { ClientOptions } from "openai"; +import fs from "node:fs/promises"; +import { + CONFIG_FILE, + DEFAULT_CONFIG, + HOME_DIR, + PROMPTS_DIR, +} from "../constants"; + +export function getOpenAICommonOptions(): ClientOptions { + const options: ClientOptions = {}; + if (process.env.PROXY_URL) { + options.httpAgent = new HttpsProxyAgent(process.env.PROXY_URL); + } + return options; +} + +const ensureDir = async (dir_path: string) => { + try { + await fs.access(dir_path); + } catch { + await fs.mkdir(dir_path, { recursive: true }); + } +}; + +export const initDir = async () => { + await ensureDir(HOME_DIR); + await ensureDir(PROMPTS_DIR); +}; + +export const readConfigFile = async () => { + try { + const config = await fs.readFile(CONFIG_FILE, "utf-8"); + return JSON.parse(config); + } catch { + await writeConfigFile(DEFAULT_CONFIG); + return DEFAULT_CONFIG; + } +}; + +export const writeConfigFile = async (config: any) => { + await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2)); +}; + +export const initConfig = async () => { + const config = await readConfigFile(); + Object.assign(process.env, config); +}; + +export const createClient = (options: ClientOptions) => { + const client = new OpenAI({ + ...options, + ...getOpenAICommonOptions(), + }); + return client; +}; diff --git a/src/utils/log.ts b/src/utils/log.ts new file mode 100644 index 0000000..8f9f271 --- /dev/null +++ b/src/utils/log.ts @@ -0,0 +1,27 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { HOME_DIR } from '../constants'; + +const LOG_FILE = path.join(HOME_DIR, 'claude-code-router.log'); + +// Ensure log directory exists +if (!fs.existsSync(HOME_DIR)) { + fs.mkdirSync(HOME_DIR, { recursive: true }); +} + +export function log(...args: any[]) { + // Check if logging is enabled via environment variable + const isLogEnabled = process.env.LOG === 'true'; + + if (!isLogEnabled) { + return; + } + + const timestamp = new Date().toISOString(); + const logMessage = `[${timestamp}] ${args.map(arg => + typeof arg === 'object' ? JSON.stringify(arg) : String(arg) + ).join(' ')}\n`; + + // Append to log file + fs.appendFileSync(LOG_FILE, logMessage, 'utf8'); +} diff --git a/src/utils/stream.ts b/src/utils/stream.ts new file mode 100644 index 0000000..b71ec50 --- /dev/null +++ b/src/utils/stream.ts @@ -0,0 +1,268 @@ +import { Response } from "express"; +import { OpenAI } from "openai"; + +interface ContentBlock { + type: string; + id?: string; + name?: string; + input?: any; + text?: string; +} + +interface MessageEvent { + type: string; + message?: { + id: string; + type: string; + role: string; + content: any[]; + model: string; + stop_reason: string | null; + stop_sequence: string | null; + usage: { + input_tokens: number; + output_tokens: number; + }; + }; + delta?: { + stop_reason?: string; + stop_sequence?: string | null; + content?: ContentBlock[]; + type?: string; + text?: string; + partial_json?: string; + }; + index?: number; + content_block?: ContentBlock; + usage?: { + input_tokens: number; + output_tokens: number; + }; +} + +export async function streamOpenAIResponse( + res: Response, + completion: AsyncIterable, + model: string +) { + const messageId = "msg_" + Date.now(); + let contentBlockIndex = 0; + let currentContentBlocks: ContentBlock[] = []; + + // Send message_start event + const messageStart: MessageEvent = { + type: "message_start", + message: { + id: messageId, + type: "message", + role: "assistant", + content: [], + model, + stop_reason: null, + stop_sequence: null, + usage: { input_tokens: 1, output_tokens: 1 }, + }, + }; + res.write(`event: message_start\ndata: ${JSON.stringify(messageStart)}\n\n`); + + let isToolUse = false; + let toolUseJson = ""; + let hasStartedTextBlock = false; + + try { + for await (const chunk of completion) { + const delta = chunk.choices[0].delta; + + if (delta.tool_calls && delta.tool_calls.length > 0) { + const toolCall = delta.tool_calls[0]; + + if (!isToolUse) { + // Start new tool call block + isToolUse = true; + const toolBlock: ContentBlock = { + type: "tool_use", + id: `toolu_${Date.now()}`, + name: toolCall.function?.name, + input: {}, + }; + + const toolBlockStart: MessageEvent = { + type: "content_block_start", + index: contentBlockIndex, + content_block: toolBlock, + }; + + currentContentBlocks.push(toolBlock); + + res.write( + `event: content_block_start\ndata: ${JSON.stringify( + toolBlockStart + )}\n\n` + ); + toolUseJson = ""; + } + + // Stream tool call JSON + if (toolCall.function?.arguments) { + const jsonDelta: MessageEvent = { + type: "content_block_delta", + index: contentBlockIndex, + delta: { + type: "input_json_delta", + partial_json: toolCall.function?.arguments, + }, + }; + + toolUseJson += toolCall.function.arguments; + + try { + const parsedJson = JSON.parse(toolUseJson); + currentContentBlocks[contentBlockIndex].input = parsedJson; + } catch (e) { + // JSON not yet complete, continue accumulating + } + + res.write( + `event: content_block_delta\ndata: ${JSON.stringify(jsonDelta)}\n\n` + ); + } + } else if (delta.content) { + // Handle regular text content + if (isToolUse) { + // End previous tool call block + const contentBlockStop: MessageEvent = { + type: "content_block_stop", + index: contentBlockIndex, + }; + + res.write( + `event: content_block_stop\ndata: ${JSON.stringify( + contentBlockStop + )}\n\n` + ); + contentBlockIndex++; + isToolUse = false; + } + + if (!delta.content) continue; + + // If text block not yet started, send content_block_start + if (!hasStartedTextBlock) { + const textBlock: ContentBlock = { + type: "text", + text: "", + }; + + const textBlockStart: MessageEvent = { + type: "content_block_start", + index: contentBlockIndex, + content_block: textBlock, + }; + + currentContentBlocks.push(textBlock); + + res.write( + `event: content_block_start\ndata: ${JSON.stringify( + textBlockStart + )}\n\n` + ); + hasStartedTextBlock = true; + } + + // Send regular text content + const contentDelta: MessageEvent = { + type: "content_block_delta", + index: contentBlockIndex, + delta: { + type: "text_delta", + text: delta.content, + }, + }; + + // Update content block text + if (currentContentBlocks[contentBlockIndex]) { + currentContentBlocks[contentBlockIndex].text += delta.content; + } + + res.write( + `event: content_block_delta\ndata: ${JSON.stringify( + contentDelta + )}\n\n` + ); + } + } + } catch (e: any) { + // If text block not yet started, send content_block_start + if (!hasStartedTextBlock) { + const textBlock: ContentBlock = { + type: "text", + text: "", + }; + + const textBlockStart: MessageEvent = { + type: "content_block_start", + index: contentBlockIndex, + content_block: textBlock, + }; + + currentContentBlocks.push(textBlock); + + res.write( + `event: content_block_start\ndata: ${JSON.stringify( + textBlockStart + )}\n\n` + ); + hasStartedTextBlock = true; + } + + // Send regular text content + const contentDelta: MessageEvent = { + type: "content_block_delta", + index: contentBlockIndex, + delta: { + type: "text_delta", + text: JSON.stringify(e), + }, + }; + + // Update content block text + if (currentContentBlocks[contentBlockIndex]) { + currentContentBlocks[contentBlockIndex].text += JSON.stringify(e); + } + + res.write( + `event: content_block_delta\ndata: ${JSON.stringify(contentDelta)}\n\n` + ); + } + + // Close last content block + const contentBlockStop: MessageEvent = { + type: "content_block_stop", + index: contentBlockIndex, + }; + + res.write( + `event: content_block_stop\ndata: ${JSON.stringify(contentBlockStop)}\n\n` + ); + + // Send message_delta event with appropriate stop_reason + const messageDelta: MessageEvent = { + type: "message_delta", + delta: { + stop_reason: isToolUse ? "tool_use" : "end_turn", + stop_sequence: null, + content: currentContentBlocks, + }, + usage: { input_tokens: 100, output_tokens: 150 }, + }; + + res.write(`event: message_delta\ndata: ${JSON.stringify(messageDelta)}\n\n`); + + // Send message_stop event + const messageStop: MessageEvent = { + type: "message_stop", + }; + + res.write(`event: message_stop\ndata: ${JSON.stringify(messageStop)}\n\n`); + res.end(); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..3a82250 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "CommonJS", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "moduleResolution": "node", + "noImplicitAny": true, + "allowSyntheticDefaultImports": true, + "sourceMap": true, + "declaration": true + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/utils.mjs b/utils.mjs deleted file mode 100644 index d94175e..0000000 --- a/utils.mjs +++ /dev/null @@ -1,9 +0,0 @@ -import { HttpsProxyAgent } from "https-proxy-agent"; - -export function getOpenAICommonOptions() { - const options = {}; - if (process.env.PROXY_URL) { - options.httpAgent = new HttpsProxyAgent(process.env.PROXY_URL); - } - return options; -} From 089654871cf7b7d877f6bc0ccc48da45d4231035 Mon Sep 17 00:00:00 2001 From: "jinhui.li" Date: Mon, 31 Mar 2025 22:28:02 +0800 Subject: [PATCH 2/3] support plugins --- README.md | 53 +++---- src/deepseek.ts => plugins/deepseek.js | 97 ++++++------ plugins/gemini.js | 23 +++ src/constants.ts | 15 +- src/index.ts | 27 +++- src/middlewares/formatRequest.ts | 101 ++++++++++++ src/middlewares/rewriteBody.ts | 43 ++++++ src/middlewares/rewriteToolsPrompt.ts | 34 ---- src/router copy.ts | 206 ------------------------- src/server.ts | 136 ---------------- src/utils/index.ts | 47 +++++- 11 files changed, 304 insertions(+), 478 deletions(-) rename src/deepseek.ts => plugins/deepseek.js (62%) create mode 100644 plugins/gemini.js create mode 100644 src/middlewares/formatRequest.ts create mode 100644 src/middlewares/rewriteBody.ts delete mode 100644 src/middlewares/rewriteToolsPrompt.ts delete mode 100644 src/router copy.ts diff --git a/README.md b/README.md index bdce7fa..a17cf44 100644 --- a/README.md +++ b/README.md @@ -4,23 +4,11 @@ ![demo.png](https://github.com/musistudio/claude-code-router/blob/main/screenshoots/demo.png) -## Warning! This project is for testing purposes and may consume a lot of tokens! It may also fail to complete tasks! - ## Implemented -- [x] Normal Mode and Router Mode +- [x] Support writing custom plugins for rewriting prompts. -- [x] Using the qwen2.5-coder-3b model as the routing dispatcher (since it’s currently free on Alibaba Cloud’s official website) - -- [x] Using the qwen-max-0125 model as the tool invoker - -- [x] Using deepseek-v3 as the coder model - -- [x] Using deepseek-r1 as the reasoning model - -- [x] Support proxy - -Thanks to the free qwen2.5-coder-3b model from Alibaba and deepseek’s KV-Cache, we can significantly reduce the cost of using Claude Code. Make sure to set appropriate ignorePatterns for the project. See: https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview +- [x] Support writing custom plugins for implementing routers. ## Usage @@ -30,16 +18,18 @@ Thanks to the free qwen2.5-coder-3b model from Alibaba and deepseek’s KV-Cache npm install -g @anthropic-ai/claude-code ``` -1. Install claude-code-router +1. Clone this repo and install dependencies ```shell -npm install -g @musistudio/claude-code-router +git clone https://github.com/musistudio/claude-code-router +cd claude-code-router && pnpm i +npm run build ``` 2. Start claude-code-router server ```shell -claude-code-router +node dist/cli.js ``` 3. Set environment variable to start claude code @@ -51,21 +41,20 @@ export API_TIMEOUT_MS=600000 claude ``` -## Normal Mode +## Plugin -The initial version uses a single model to accomplish all tasks. This model needs to support function calling and must allow for a sufficiently large tool description length, ideally greater than 1754. If the model used in this mode does not support KV Cache, it will consume a significant number of tokens. +The plugin allows users to rewrite Claude Code prompt and custom router. The plugin path is in `$HOME/.claude-code-router/plugins`. Currently, there are two demos available: +1. [custom router](https://github.com/musistudio/claude-code-router/blob/dev/custom-prompt/plugins/deepseek.js) +2. [rewrite prompt](https://github.com/musistudio/claude-code-router/blob/dev/custom-prompt/plugins/gemini.js) -![normal mode](https://github.com/musistudio/claude-code-reverse/blob/main/screenshoots/normal.png) +You need to move them to the `$HOME/.claude-code-router/plugins` directory and configure 'usePlugin' in `$HOME/.claude-code-router/config.json`,like this: -## Router Mode - -Using multiple models to handle different tasks, this mode requires setting ENABLE_ROUTER to true and configuring four models: ROUTER_AGENT_MODEL, TOOL_AGENT_MODEL, CODER_AGENT_MODEL, and THINK_AGENT_MODEL. - -ROUTER_AGENT_MODEL does not require high intelligence and is only responsible for request routing. A small model is sufficient for this task (testing has shown that the qwen-coder-3b model performs well). -TOOL_AGENT_MODEL must support function calling and allow for a sufficiently large tool description length, ideally greater than 1754. If the model used in this mode does not support KV Cache, it will consume a significant number of tokens. - -CODER_AGENT_MODEL and THINK_AGENT_MODEL can use the DeepSeek series of models. - -The purpose of router mode is to separate tool invocation from coding tasks, enabling the use of inference models like r1, which do not support function calling. - -![router mode](https://github.com/musistudio/claude-code-router/blob/main/screenshoots/router.png) +```json +{ + "usePlugin": "gemini", + "LOG": true, + "OPENAI_API_KEY": "", + "OPENAI_BASE_URL": "", + "OPENAI_MODEL": "" +} +``` diff --git a/src/deepseek.ts b/plugins/deepseek.js similarity index 62% rename from src/deepseek.ts rename to plugins/deepseek.js index afc0c9d..7de7038 100644 --- a/src/deepseek.ts +++ b/plugins/deepseek.js @@ -1,15 +1,10 @@ -import { OpenAI } from "openai"; -import { createClient } from "./utils"; -import { log } from "./utils/log"; -export interface BaseRouter { - name: string; - description: string; - run: ( - args: OpenAI.Chat.Completions.ChatCompletionCreateParams - ) => Promise; -} +const { + log, + streamOpenAIResponse, + createClient, +} = require("claude-code-router"); -const thinkRouter: BaseRouter = { +const thinkRouter = { name: "think", description: `This agent is used solely for complex reasoning and thinking tasks. It should not be called for information retrieval or repetitive, frequent requests. Only use this agent for tasks that require deep analysis or problem-solving. If there is an existing result from the Thinker agent, do not call this agent again.你只负责深度思考以拆分任务,不需要进行任何的编码和调用工具。最后讲拆分的步骤按照顺序返回。比如\n1. xxx\n2. xxx\n3. xxx`, run(args) { @@ -18,13 +13,13 @@ const thinkRouter: BaseRouter = { baseURL: process.env.THINK_AGENT_BASE_URL, }); const messages = JSON.parse(JSON.stringify(args.messages)); - messages.forEach((msg: any) => { + messages.forEach((msg) => { if (Array.isArray(msg.content)) { msg.content = JSON.stringify(msg.content); } }); - let startIdx = messages.findIndex((msg: any) => msg.role !== "system"); + let startIdx = messages.findIndex((msg) => msg.role !== "system"); if (startIdx === -1) startIdx = messages.length; for (let i = startIdx; i < messages.length; i++) { @@ -46,14 +41,12 @@ const thinkRouter: BaseRouter = { return client.chat.completions.create({ ...args, messages, - model: process.env.THINK_AGENT_MODEL as string, + model: process.env.THINK_AGENT_MODEL, }); }, }; -export class Router { - routers: BaseRouter[]; - client: OpenAI; +class Router { constructor() { this.routers = [thinkRouter]; this.client = createClient({ @@ -61,37 +54,37 @@ export class Router { baseURL: process.env.ROUTER_AGENT_BASE_URL, }); } - async route( - args: OpenAI.Chat.Completions.ChatCompletionCreateParams - ): Promise { + async route(args) { log(`Request Router: ${JSON.stringify(args, null, 2)}`); - const res: OpenAI.Chat.Completions.ChatCompletion = - await this.client.chat.completions.create({ - ...args, - messages: [ - ...args.messages, - { - role: "system", - content: `## **Guidelines:** -- **Trigger the "think" mode when the user's request involves deep thinking, complex reasoning, or multi-step analysis.** -- **Criteria:** - - Involves multi-layered logical reasoning or causal analysis - - Requires establishing connections or pattern recognition between different pieces of information - - Involves cross-domain knowledge integration or weighing multiple possibilities - - Requires creative thinking or non-direct inference -### **Format requirements:** -- When you need to trigger the "think" mode, return the following JSON format: -\`\`\`json -{ - "use": "think" -} -\`\`\` -`, - }, - ], - model: process.env.ROUTER_AGENT_MODEL as string, - stream: false, - }); + const res = await this.client.chat.completions.create({ + ...args, + messages: [ + ...args.messages, + { + role: "system", + content: `## **Guidelines:** + - **Trigger the "think" mode when the user's request involves deep thinking, complex reasoning, or multi-step analysis.** + - **Criteria:** + - Involves multi-layered logical reasoning or causal analysis + - Requires establishing connections or pattern recognition between different pieces of information + - Involves cross-domain knowledge integration or weighing multiple possibilities + - Requires creative thinking or non-direct inference + ### **Special Case:** + - **When the user sends "test", respond with "success" only.** + + ### **Format requirements:** + - When you need to trigger the "think" mode, return the following JSON format: + \`\`\`json + { + "use": "think" + } + \`\`\` + `, + }, + ], + model: process.env.ROUTER_AGENT_MODEL, + stream: false, + }); let result; try { const text = res.choices[0].message.content; @@ -102,13 +95,13 @@ export class Router { text.slice(text.indexOf("{"), text.lastIndexOf("}") + 1) ); } catch (e) { - (res.choices[0] as any).delta = res.choices[0].message; + res.choices[0].delta = res.choices[0].message; log(`No Router: ${JSON.stringify(res.choices[0].message)}`); return [res]; } const router = this.routers.find((item) => item.name === result.use); if (!router) { - (res.choices[0] as any).delta = res.choices[0].message; + res.choices[0].delta = res.choices[0].message; log(`No Router: ${JSON.stringify(res.choices[0].message)}`); return [res]; } @@ -138,3 +131,9 @@ export class Router { return router.run(args); } } + +const router = new Router(); +module.exports = async function handle(req, res, next) { + const completions = await router.route(req.body); + streamOpenAIResponse(res, completions, req.body.model); +}; diff --git a/plugins/gemini.js b/plugins/gemini.js new file mode 100644 index 0000000..a5458a1 --- /dev/null +++ b/plugins/gemini.js @@ -0,0 +1,23 @@ +module.exports = async function handle(req, res, next) { + if (Array.isArray(req.body.tools)) { + // rewrite tools definition + req.body.tools.forEach((tool) => { + if (tool.function.name === "BatchTool") { + // HACK: Gemini does not support objects with empty properties + tool.function.parameters.properties.invocations.items.properties.input.type = + "number"; + return; + } + Object.keys(tool.function.parameters.properties).forEach((key) => { + const prop = tool.function.parameters.properties[key]; + if ( + prop.type === "string" && + !["enum", "date-time"].includes(prop.format) + ) { + delete prop.format; + } + }); + }); + } + next(); +}; diff --git a/src/constants.ts b/src/constants.ts index 9255079..644bf6f 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -5,24 +5,11 @@ export const HOME_DIR = path.join(os.homedir(), ".claude-code-router"); export const CONFIG_FILE = `${HOME_DIR}/config.json`; -export const PROMPTS_DIR = `${HOME_DIR}/prompts`; +export const PLUGINS_DIR = `${HOME_DIR}/plugins`; export const DEFAULT_CONFIG = { log: false, - ENABLE_ROUTER: true, OPENAI_API_KEY: "", OPENAI_BASE_URL: "https://openrouter.ai/api/v1", OPENAI_MODEL: "openai/o3-mini", - - CODER_AGENT_API_KEY: "", - CODER_AGENT_BASE_URL: "https://api.deepseek.com", - CODER_AGENT_MODEL: "deepseek-chat", - - THINK_AGENT_API_KEY: "", - THINK_AGENT_BASE_URL: "https://api.deepseek.com", - THINK_AGENT_MODEL: "deepseek-reasoner", - - ROUTER_AGENT_API_KEY: "", - ROUTER_AGENT_BASE_URL: "https://api.deepseek.com", - ROUTER_AGENT_MODEL: "deepseek-chat", }; diff --git a/src/index.ts b/src/index.ts index 08c5ca8..6fd70c9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,11 @@ import { existsSync } from "fs"; import { writeFile } from "fs/promises"; -import { initConfig, initDir } from "./utils"; +import { getOpenAICommonOptions, initConfig, initDir } from "./utils"; import { createServer } from "./server"; -import { rewriteToolsPrompt } from "./middlewares/rewriteToolsPrompt"; +import { formatRequest } from "./middlewares/formatRequest"; +import { rewriteBody } from "./middlewares/rewriteBody"; +import OpenAI from "openai"; +import { streamOpenAIResponse } from "./utils/stream"; async function initializeClaudeConfig() { const homeDir = process.env.HOME; @@ -29,7 +32,25 @@ async function run() { await initDir(); await initConfig(); const server = createServer(3456); - server.useMiddleware(rewriteToolsPrompt); + server.useMiddleware(formatRequest); + server.useMiddleware(rewriteBody); + + const openai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, + baseURL: process.env.OPENAI_BASE_URL, + ...getOpenAICommonOptions(), + }); + server.app.post("/v1/messages", async (req, res) => { + try { + if (process.env.OPENAI_MODEL) { + req.body.model = process.env.OPENAI_MODEL; + } + const completion: any = await openai.chat.completions.create(req.body); + await streamOpenAIResponse(res, completion, req.body.model); + } catch (e) { + console.error("Error in OpenAI API call:", e); + } + }); server.start(); } run(); diff --git a/src/middlewares/formatRequest.ts b/src/middlewares/formatRequest.ts new file mode 100644 index 0000000..e5ab184 --- /dev/null +++ b/src/middlewares/formatRequest.ts @@ -0,0 +1,101 @@ +import { Request, Response, NextFunction } from "express"; +import { ContentBlockParam } from "@anthropic-ai/sdk/resources"; +import { MessageCreateParamsBase } from "@anthropic-ai/sdk/resources/messages"; +import OpenAI from "openai"; +import { streamOpenAIResponse } from "../utils/stream"; + +export const formatRequest = async ( + req: Request, + res: Response, + next: NextFunction +) => { + let { + model, + max_tokens, + messages, + system = [], + temperature, + metadata, + tools, + }: MessageCreateParamsBase = req.body; + try { + const openAIMessages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = + messages.map((item) => { + if (item.content instanceof Array) { + return { + role: item.role, + content: item.content + .map((it: ContentBlockParam) => { + if (it.type === "text") { + return typeof it.text === "string" + ? it.text + : JSON.stringify(it); + } + return JSON.stringify(it); + }) + .join(""), + } as OpenAI.Chat.Completions.ChatCompletionMessageParam; + } + return { + role: item.role, + content: + typeof item.content === "string" + ? item.content + : JSON.stringify(item.content), + }; + }); + const systemMessages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = + Array.isArray(system) + ? system.map((item) => ({ + role: "system", + content: item.text, + })) + : [{ role: "system", content: system }]; + const data: OpenAI.Chat.Completions.ChatCompletionCreateParams = { + model, + messages: [...systemMessages, ...openAIMessages], + temperature, + stream: true, + }; + if (tools) { + data.tools = tools + .filter((tool) => !["StickerRequest"].includes(tool.name)) + .map((item: any) => ({ + type: "function", + function: { + name: item.name, + description: item.description, + parameters: item.input_schema, + }, + })); + } + res.setHeader("Content-Type", "text/event-stream"); + res.setHeader("Cache-Control", "no-cache"); + res.setHeader("Connection", "keep-alive"); + req.body = data; + } catch (error) { + console.error("Error in request processing:", error); + const errorCompletion: AsyncIterable = + { + async *[Symbol.asyncIterator]() { + yield { + id: `error_${Date.now()}`, + created: Math.floor(Date.now() / 1000), + model: "gpt-3.5-turbo", + object: "chat.completion.chunk", + choices: [ + { + index: 0, + delta: { + content: `Error: ${(error as Error).message}`, + }, + finish_reason: "stop", + }, + ], + }; + }, + }; + await streamOpenAIResponse(res, errorCompletion, model); + } + next(); +}; diff --git a/src/middlewares/rewriteBody.ts b/src/middlewares/rewriteBody.ts new file mode 100644 index 0000000..e15d92b --- /dev/null +++ b/src/middlewares/rewriteBody.ts @@ -0,0 +1,43 @@ +import { Request, Response, NextFunction } from "express"; +import Module from "node:module"; +import { streamOpenAIResponse } from "../utils/stream"; +import { log } from "../utils/log"; +import { PLUGINS_DIR } from "../constants"; +import path from "node:path"; +import { access } from "node:fs/promises"; +import { OpenAI } from "openai"; +import { createClient } from "../utils"; + +// @ts-ignore +const originalLoad = Module._load; +// @ts-ignore +Module._load = function (request, parent, isMain) { + if (request === "claude-code-router") { + return { + streamOpenAIResponse, + log, + OpenAI, + createClient, + }; + } + return originalLoad.call(this, request, parent, isMain); +}; + +export const rewriteBody = async ( + req: Request, + res: Response, + next: NextFunction +) => { + if (!process.env.usePlugin) { + return next(); + } + const pluginPath = path.join(PLUGINS_DIR, `${process.env.usePlugin}.js`); + try { + await access(pluginPath); + const rewritePlugin = require(pluginPath); + rewritePlugin(req, res, next); + } catch (e) { + console.error(e); + next(); + } +}; diff --git a/src/middlewares/rewriteToolsPrompt.ts b/src/middlewares/rewriteToolsPrompt.ts deleted file mode 100644 index 699e52d..0000000 --- a/src/middlewares/rewriteToolsPrompt.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { readFile, access } from "node:fs/promises"; -import { join } from "node:path"; -import { PROMPTS_DIR } from "../constants"; - -const getPrompt = async (name: string) => { - try { - const promptPath = join(PROMPTS_DIR, `${name}.md`); - await access(promptPath); - const prompt = await readFile(promptPath, "utf-8"); - return prompt; - } catch { - return null; - } -}; - -export const rewriteToolsPrompt = async ( - req: Request, - res: Response, - next: NextFunction -) => { - const { tools } = req.body; - if (!Array.isArray(tools)) { - next(); - return; - } - for (const tool of tools) { - const prompt = await getPrompt(tool.name); - if (prompt) { - tool.description = prompt; - } - } - next(); -}; diff --git a/src/router copy.ts b/src/router copy.ts deleted file mode 100644 index 9516ada..0000000 --- a/src/router copy.ts +++ /dev/null @@ -1,206 +0,0 @@ -import { OpenAI } from "openai"; -import { createClient } from "./utils"; -import { log } from "./utils/log"; -export interface BaseRouter { - name: string; - description: string; - run: ( - args: OpenAI.Chat.Completions.ChatCompletionCreateParams - ) => Promise; -} - -const coderRouter: BaseRouter = { - name: "coder", - description: `This agent is solely responsible for helping users write code. This agent could not call tools. This agent is used for writing and modifying code when the user provides clear and specific coding requirements. For example, tasks like implementing a quicksort algorithm in JavaScript or creating an HTML layout. If the user's request is unclear or cannot be directly translated into code, please route the task to 'think' first for clarification or further processing.`, - run(args) { - const client = createClient({ - apiKey: process.env.CODER_AGENT_API_KEY, - baseURL: process.env.CODER_AGENT_BASE_URL, - }); - delete args.tools; - args.messages.forEach((item) => { - if (Array.isArray(item.content)) { - item.content = JSON.stringify(item.content); - } - }); - return client.chat.completions.create({ - ...args, - messages: [ - ...args.messages, - { - role: "system", - content: - "You are a code writer who helps users write code based on their specific requirements. You create algorithms, implement functionality, and build structures according to the clear instructions provided by the user. Your focus is solely on writing code, ensuring that the task is completed accurately and efficiently.", - }, - ], - model: process.env.CODER_AGENT_MODEL as string, - }); - }, -}; - - -const useToolRouter: BaseRouter = { - name: "use-tool", - description: `This agent can call user-specified tools to perform tasks. The user provides a list of tools to be used, and the agent integrates these tools to complete the specified tasks efficiently. The agent follows user instructions and ensures proper tool utilization for each request`, - run(args) { - const client = createClient({ - apiKey: process.env.TOOL_AGENT_API_KEY, - baseURL: process.env.TOOL_AGENT_BASE_URL, - }); - return client.chat.completions.create({ - ...args, - messages: [ - ...args.messages, - { - role: "system", - content: - "You need to select the appropriate tool for the task based on the user’s request. Review the requirements and choose the tool that fits the task best.", - }, - ], - model: process.env.TOOL_AGENT_MODEL as string, - }); - }, -}; - -const thinkRouter: BaseRouter = { - name: "think", - description: `This agent is used solely for complex reasoning and thinking tasks. It should not be called for information retrieval or repetitive, frequent requests. Only use this agent for tasks that require deep analysis or problem-solving. If there is an existing result from the Thinker agent, do not call this agent again.你只负责深度思考以拆分任务,不需要进行任何的编码和调用工具。最后讲拆分的步骤按照顺序返回。比如\n1. xxx\n2. xxx\n3. xxx`, - run(args) { - const client = createClient({ - apiKey: process.env.THINK_AGENT_API_KEY, - baseURL: process.env.THINK_AGENT_BASE_URL, - }); - const messages = JSON.parse(JSON.stringify(args.messages)); - messages.forEach((msg: any) => { - if (Array.isArray(msg.content)) { - msg.content = JSON.stringify(msg.content); - } - }); - - let startIdx = messages.findIndex((msg: any) => msg.role !== "system"); - if (startIdx === -1) startIdx = messages.length; - - for (let i = startIdx; i < messages.length; i++) { - const expectedRole = (i - startIdx) % 2 === 0 ? "user" : "assistant"; - messages[i].role = expectedRole; - } - - if ( - messages.length > 0 && - messages[messages.length - 1].role === "assistant" - ) { - messages.push({ - role: "user", - content: - "Please follow the instructions provided above to resolve the issue.", - }); - } - delete args.tools; - return client.chat.completions.create({ - ...args, - messages, - model: process.env.THINK_AGENT_MODEL as string, - }); - }, -}; - -export class Router { - routers: BaseRouter[]; - client: OpenAI; - constructor() { - this.routers = [coderRouter, useToolRouter, thinkRouter]; - this.client = createClient({ - apiKey: process.env.ROUTER_AGENT_API_KEY, - baseURL: process.env.ROUTER_AGENT_BASE_URL, - }); - } - async route( - args: OpenAI.Chat.Completions.ChatCompletionCreateParams - ): Promise { - log(`Route: ${JSON.stringify(args, null, 2)}`); - const res: OpenAI.Chat.Completions.ChatCompletion = - await this.client.chat.completions.create({ - ...args, - messages: [ - ...args.messages, - { - role: "system", - content: `You are an AI task router and executor, responsible for understanding user requests and directing them to the appropriate processing mode or tool based on the task type and requirements. Your main responsibility is to determine the nature of the request, execute the task when possible, and respond appropriately. - -### **Guidelines:** -- **If an external tool is required to complete the task (such as searching for information, generating images, or modifying code), route the task to \`use-tool\` rather than handling it directly.** -- If the task requires generating an image, route to \`use-tool\` and specify the image generation tool. -- If the task requires searching for information, route to \`use-tool\` and specify the search tool. -- If the task requires modifying or executing code, route to \`use-tool\` and specify the code handling tool. -- **Do NOT execute the tool action directly; always trigger it through \`use-tool\`.** - -- **If the user is chatting casually or having a general conversation, respond naturally and conversationally. Improving the user experience through friendly interactions is one of your main responsibilities.** - -- **If the user's request involves deep thinking, complex reasoning, or multi-step analysis, use the "think" mode to break down and solve the problem.** - -- **If the user's request involves coding or technical implementation, use the "coder" mode to generate or modify code.** - - **After generating the code, if the task requires applying or integrating the code, route to \`use-tool\` and specify the code execution tool.** - - **Do NOT re-trigger "coder" to apply code — route to \`use-tool\` instead.** - -### **Format requirements:** -- When you need to trigger a specific mode (such as "think", "coder", or "use-tool"), return the following JSON format: - -### IMPORTANT: -- 你不能也不会调用BatchTool,如果你需要使用工具请路由到\`use-tool\`,由\`use-tool\`来调用BatchTool。 - -\`\`\`json -{ - "use": "", -} -\`\`\` -`, - }, - ], - model: process.env.ROUTER_AGENT_MODEL as string, - stream: false, - }); - let result; - try { - const text = res.choices[0].message.content; - if (!text) { - throw new Error("No text"); - } - result = JSON.parse( - text.slice(text.indexOf("{"), text.lastIndexOf("}") + 1) - ); - } catch (e) { - (res.choices[0] as any).delta = res.choices[0].message; - return [res]; - } - const router = this.routers.find((item) => item.name === result.use); - if (!router) { - (res.choices[0] as any).delta = res.choices[0].message; - log(`No Router: ${JSON.stringify(res.choices[0].message)}`); - return [res]; - } - log(`Use Router: ${router.name}`); - if (router.name === "think" || router.name === "coder") { - const agentResult = await router.run({ - ...args, - stream: false, - }); - try { - args.messages.push({ - role: "user", - content: - `${router.name} Agent Result: ` + - agentResult.choices[0].message.content, - }); - log( - `${router.name} Agent Result: ` + - agentResult.choices[0].message.content - ); - return await this.route(args); - } catch (error) { - console.log(agentResult); - throw error; - } - } - return router.run(args); - } -} diff --git a/src/server.ts b/src/server.ts index 328ebf8..3fd2468 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,18 +1,4 @@ import express, { RequestHandler } from "express"; -import { - ContentBlockParam, - MessageCreateParamsBase, -} from "@anthropic-ai/sdk/resources/messages"; -import { OpenAI } from "openai"; -import { Router } from "./deepseek"; -import { getOpenAICommonOptions } from "./utils"; -import { streamOpenAIResponse } from "./utils/stream"; - -interface Client { - call: ( - data: OpenAI.Chat.Completions.ChatCompletionCreateParams - ) => Promise; -} interface Server { app: express.Application; @@ -23,128 +9,6 @@ interface Server { export const createServer = (port: number): Server => { const app = express(); app.use(express.json({ limit: "500mb" })); - - let client: Client; - if (process.env.ENABLE_ROUTER && process.env.ENABLE_ROUTER === "true") { - const router = new Router(); - client = { - call: (data) => { - return router.route(data); - }, - }; - } else { - const openai = new OpenAI({ - apiKey: process.env.OPENAI_API_KEY, - baseURL: process.env.OPENAI_BASE_URL, - ...getOpenAICommonOptions(), - }); - client = { - call: (data) => { - if (process.env.OPENAI_MODEL) { - data.model = process.env.OPENAI_MODEL; - } - return openai.chat.completions.create(data); - }, - }; - } - - app.post("/v1/messages", async (req, res) => { - try { - let { - model, - max_tokens, - messages, - system = [], - temperature, - metadata, - tools, - }: MessageCreateParamsBase = req.body; - - const openAIMessages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = - messages.map((item) => { - if (item.content instanceof Array) { - return { - role: item.role, - content: item.content - .map((it: ContentBlockParam) => { - if (it.type === "text") { - return typeof it.text === "string" - ? it.text - : JSON.stringify(it); - } - return JSON.stringify(it); - }) - .join(""), - } as OpenAI.Chat.Completions.ChatCompletionMessageParam; - } - return { - role: item.role, - content: - typeof item.content === "string" - ? item.content - : JSON.stringify(item.content), - }; - }); - const systemMessages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = - Array.isArray(system) - ? system.map((item) => ({ - role: "system", - content: item.text, - })) - : [{ role: "system", content: system }]; - const data: OpenAI.Chat.Completions.ChatCompletionCreateParams = { - model, - messages: [...systemMessages, ...openAIMessages], - temperature, - stream: true, - }; - if (tools) { - data.tools = tools - .filter((tool) => !["StickerRequest"].includes(tool.name)) - .map((item: any) => ({ - type: "function", - function: { - name: item.name, - description: item.description, - parameters: item.input_schema, - }, - })); - } - res.setHeader("Content-Type", "text/event-stream"); - res.setHeader("Cache-Control", "no-cache"); - res.setHeader("Connection", "keep-alive"); - try { - const completion = await client.call(data); - await streamOpenAIResponse(res, completion, model); - } catch (e) { - console.error("Error in OpenAI API call:", e); - } - } catch (error) { - console.error("Error in request processing:", error); - const errorCompletion: AsyncIterable = - { - async *[Symbol.asyncIterator]() { - yield { - id: `error_${Date.now()}`, - created: Math.floor(Date.now() / 1000), - model: "gpt-3.5-turbo", - object: "chat.completion.chunk", - choices: [ - { - index: 0, - delta: { - content: `Error: ${(error as Error).message}`, - }, - finish_reason: "stop", - }, - ], - }; - }, - }; - await streamOpenAIResponse(res, errorCompletion, "gpt-3.5-turbo"); - } - }); - return { app, useMiddleware: (middleware: RequestHandler) => { diff --git a/src/utils/index.ts b/src/utils/index.ts index 57478a4..d218bb6 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,11 +1,12 @@ import { HttpsProxyAgent } from "https-proxy-agent"; import OpenAI, { ClientOptions } from "openai"; import fs from "node:fs/promises"; +import readline from "node:readline"; import { CONFIG_FILE, DEFAULT_CONFIG, HOME_DIR, - PROMPTS_DIR, + PLUGINS_DIR, } from "../constants"; export function getOpenAICommonOptions(): ClientOptions { @@ -26,7 +27,29 @@ const ensureDir = async (dir_path: string) => { export const initDir = async () => { await ensureDir(HOME_DIR); - await ensureDir(PROMPTS_DIR); + await ensureDir(PLUGINS_DIR); +}; + +const createReadline = () => { + return readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); +}; + +const question = (query: string): Promise => { + return new Promise((resolve) => { + const rl = createReadline(); + rl.question(query, (answer) => { + rl.close(); + resolve(answer); + }); + }); +}; + +const confirm = async (query: string): Promise => { + const answer = await question(query); + return answer.toLowerCase() !== "n"; }; export const readConfigFile = async () => { @@ -34,8 +57,24 @@ export const readConfigFile = async () => { const config = await fs.readFile(CONFIG_FILE, "utf-8"); return JSON.parse(config); } catch { - await writeConfigFile(DEFAULT_CONFIG); - return DEFAULT_CONFIG; + const useRouter = await confirm( + "No config file found. Enable router mode? (Y/n)" + ); + if (!useRouter) { + const apiKey = await question("Enter OPENAI_API_KEY: "); + const baseUrl = await question("Enter OPENAI_BASE_URL: "); + const model = await question("Enter OPENAI_MODEL: "); + const config = Object.assign({}, DEFAULT_CONFIG, { + OPENAI_API_KEY: apiKey, + OPENAI_BASE_URL: baseUrl, + OPENAI_MODEL: model, + }); + await writeConfigFile(config); + return config; + } else { + const router = await question("Enter OPENAI_API_KEY: "); + return DEFAULT_CONFIG; + } } }; From 2cc91ada5cbd150bf5c3006b8b0489ca19263c38 Mon Sep 17 00:00:00 2001 From: "jinhui.li" Date: Tue, 10 Jun 2025 12:55:25 +0800 Subject: [PATCH 3/3] add cli --- .env.example | 31 - .npmignore | 9 + CLAUDE.md | 12 + README.md | 33 +- config.json | 7 + package-lock.json | 1013 ------------------------------ package.json | 8 +- plugins/deepseek.js | 2 +- src/cli.ts | 88 +++ src/constants.ts | 7 +- src/index.ts | 36 +- src/middlewares/formatRequest.ts | 166 ++++- src/utils/close.ts | 23 + src/utils/codeCommand.ts | 31 + src/utils/index.ts | 29 +- src/utils/log.ts | 26 +- src/utils/processCheck.ts | 60 ++ src/utils/status.ts | 27 + src/utils/stream.ts | 63 +- 19 files changed, 521 insertions(+), 1150 deletions(-) delete mode 100644 .env.example create mode 100644 .npmignore create mode 100644 CLAUDE.md create mode 100644 config.json delete mode 100644 package-lock.json create mode 100644 src/cli.ts create mode 100644 src/utils/close.ts create mode 100644 src/utils/codeCommand.ts create mode 100644 src/utils/processCheck.ts create mode 100644 src/utils/status.ts diff --git a/.env.example b/.env.example deleted file mode 100644 index 671d4eb..0000000 --- a/.env.example +++ /dev/null @@ -1,31 +0,0 @@ -## If you don't want to use multi-model routing -## set ENABLE_ROUTER to false, and define the following variables -## the model needs to support function calling -ENABLE_ROUTER=false -OPENAI_API_KEY="" -OPENAI_BASE_URL="" -OPENAI_MODEL="" - - -## If you want to use multi-model routing, set ENABLE_ROUTER to true -# ENABLE_ROUTER=true - -## Define the model for the tool agent, the model needs to support function calling -# TOOL_AGENT_API_KEY="" -# TOOL_AGENT_BASE_URL="" -# TOOL_AGENT_MODEL="" - -## Define the model for the coder agent -# CODER_AGENT_API_KEY="" -# CODER_AGENT_BASE_URL="" -# CODER_AGENT_MODEL="" - -## Define the model for the thinker agent, using a model that supports reasoning will yield better results -# THINK_AGENT_API_KEY="" -# THINK_AGENT_BASE_URL="" -# THINK_AGENT_MODEL="" - -## Define the model for the router agent, this model is the entry point for each request, it will consume a lot of tokens, please choose a small model to reduce costs -# ROUTER_AGENT_API_KEY="" -# ROUTER_AGENT_BASE_URL="" -# ROUTER_AGENT_MODEL="" diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..b6a2878 --- /dev/null +++ b/.npmignore @@ -0,0 +1,9 @@ +src +node_modules +.claude +CLAUDE.md +screenshoots +.DS_Store +.vscode +.idea +.env \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..50bfac0 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,12 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.You need use English to write text. + +## Key Development Commands +- Build: `npm run build` +- Start: `npm start` + +## Architecture +- Uses `express` for routing (see `src/server.ts`) +- Bundles with `esbuild` for CLI distribution +- Plugins are loaded from `$HOME/.claude-code-router/plugins` \ No newline at end of file diff --git a/README.md b/README.md index a17cf44..cb9ace1 100644 --- a/README.md +++ b/README.md @@ -2,44 +2,26 @@ > This is a repository for testing routing Claude Code requests to different models. -![demo.png](https://github.com/musistudio/claude-code-router/blob/main/screenshoots/demo.png) - -## Implemented - -- [x] Support writing custom plugins for rewriting prompts. - -- [x] Support writing custom plugins for implementing routers. - ## Usage -0. Install Claude Code +1. Install Claude Code ```shell npm install -g @anthropic-ai/claude-code ``` -1. Clone this repo and install dependencies +2. Install Claude Code Router ```shell -git clone https://github.com/musistudio/claude-code-router -cd claude-code-router && pnpm i -npm run build +npm install -g @musistudio/claude-code-router ``` -2. Start claude-code-router server +3. Start Claude Code by claude-code-router ```shell -node dist/cli.js +ccr code ``` -3. Set environment variable to start claude code - -```shell -export DISABLE_PROMPT_CACHING=1 -export ANTHROPIC_BASE_URL="http://127.0.0.1:3456" -export API_TIMEOUT_MS=600000 -claude -``` ## Plugin @@ -58,3 +40,8 @@ You need to move them to the `$HOME/.claude-code-router/plugins` directory and c "OPENAI_MODEL": "" } ``` + +## Features +- [x] Plugins +- [] Support change models +- [] Suport scheduled tasks \ No newline at end of file diff --git a/config.json b/config.json new file mode 100644 index 0000000..9641bf6 --- /dev/null +++ b/config.json @@ -0,0 +1,7 @@ +{ + "usePlugin": "", + "LOG": true, + "OPENAI_API_KEY": "", + "OPENAI_BASE_URL": "", + "OPENAI_MODEL": "" +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 54b732e..0000000 --- a/package-lock.json +++ /dev/null @@ -1,1013 +0,0 @@ -{ - "name": "claude-code-reverse", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "claude-code-reverse", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "dotenv": "^16.4.7", - "express": "^4.21.2", - "openai": "^4.85.4" - } - }, - "node_modules/@types/node": { - "version": "18.19.76", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.76.tgz", - "integrity": "sha512-yvR7Q9LdPz2vGpmpJX5LolrgRdWvB67MJKDPSgIIzpFbaf9a1j/f5DnLp5VDyHGMR0QZHlTr1afsD87QCXFHKw==", - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/@types/node-fetch": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz", - "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==", - "dependencies": { - "@types/node": "*", - "form-data": "^4.0.0" - } - }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/agentkeepalive": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", - "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", - "dependencies": { - "humanize-ms": "^1.2.1" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", - "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/dotenv": { - "version": "16.4.7", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", - "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", - "proxy-addr": "~2.0.7", - "qs": "6.13.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/form-data-encoder": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", - "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==" - }, - "node_modules/formdata-node": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", - "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", - "dependencies": { - "node-domexception": "1.0.0", - "web-streams-polyfill": "4.0.0-beta.3" - }, - "engines": { - "node": ">= 12.20" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/humanize-ms": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", - "dependencies": { - "ms": "^2.0.0" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/openai": { - "version": "4.85.4", - "resolved": "https://registry.npmjs.org/openai/-/openai-4.85.4.tgz", - "integrity": "sha512-Nki51PBSu+Aryo7WKbdXvfm0X/iKkQS2fq3O0Uqb/O3b4exOZFid2te1BZ52bbO5UwxQZ5eeHJDCTqtrJLPw0w==", - "dependencies": { - "@types/node": "^18.11.18", - "@types/node-fetch": "^2.6.4", - "abort-controller": "^3.0.0", - "agentkeepalive": "^4.2.1", - "form-data-encoder": "1.7.2", - "formdata-node": "^4.3.2", - "node-fetch": "^2.6.7" - }, - "bin": { - "openai": "bin/cli" - }, - "peerDependencies": { - "ws": "^8.18.0", - "zod": "^3.23.8" - }, - "peerDependenciesMeta": { - "ws": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.19.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/web-streams-polyfill": { - "version": "4.0.0-beta.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", - "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", - "engines": { - "node": ">= 14" - } - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - } - } -} diff --git a/package.json b/package.json index 9b043b3..f0b94ab 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,17 @@ { - "name": "claude-code-router", + "name": "@musistudio/claude-code-router", "version": "1.0.0", "description": "Use Claude Code without an Anthropics account and route it to another LLM provider", "bin": { - "claude-code-router": "./dist/cli.js" + "ccr": "./dist/cli.js" }, "scripts": { - "start": "node dist/cli.js", - "build": "tsc && esbuild src/index.ts --bundle --platform=node --outfile=dist/cli.js" + "build": "esbuild src/cli.ts --bundle --platform=node --outfile=dist/cli.js" }, "keywords": ["claude", "code", "router", "llm", "anthropic"], "author": "musistudio", "license": "MIT", "dependencies": { - "@anthropic-ai/claude-code": "^0.2.53", "@anthropic-ai/sdk": "^0.39.0", "dotenv": "^16.4.7", "express": "^4.21.2", diff --git a/plugins/deepseek.js b/plugins/deepseek.js index 7de7038..7d81ab9 100644 --- a/plugins/deepseek.js +++ b/plugins/deepseek.js @@ -6,7 +6,7 @@ const { const thinkRouter = { name: "think", - description: `This agent is used solely for complex reasoning and thinking tasks. It should not be called for information retrieval or repetitive, frequent requests. Only use this agent for tasks that require deep analysis or problem-solving. If there is an existing result from the Thinker agent, do not call this agent again.你只负责深度思考以拆分任务,不需要进行任何的编码和调用工具。最后讲拆分的步骤按照顺序返回。比如\n1. xxx\n2. xxx\n3. xxx`, + description: `This agent is used solely for complex reasoning and thinking tasks. It should not be called for information retrieval or repetitive, frequent requests. Only use this agent for tasks that require deep analysis or problem-solving. If there is an existing result from the Thinker agent, do not call this agent again. You are only responsible for deep thinking to break down tasks, no coding or tool calls are needed. Finally, return the broken-down steps in order, for example:\n1. xxx\n2. xxx\n3. xxx`, run(args) { const client = createClient({ apiKey: process.env.THINK_AGENT_API_KEY, diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 0000000..bd47cc5 --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,88 @@ +#!/usr/bin/env node +import { run } from "./index"; +import { closeService } from "./utils/close"; +import { showStatus } from "./utils/status"; +import { executeCodeCommand } from "./utils/codeCommand"; +import { isServiceRunning } from "./utils/processCheck"; +import { version } from "../package.json"; + +const command = process.argv[2]; + +const HELP_TEXT = ` +Usage: claude-code [command] + +Commands: + start Start service + stop Stop service + status Show service status + code Execute code command + -v, version Show version information + -h, help Show help information + +Example: + claude-code start + claude-code code "Write a Hello World" +`; + +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()) { + // Wait for an additional short period to ensure service is fully ready + await new Promise((resolve) => setTimeout(resolve, 500)); + return true; + } + await new Promise((resolve) => setTimeout(resolve, 100)); + } + return false; +} + +async function main() { + switch (command) { + case "start": + await run({ daemon: true }); + break; + case "stop": + await closeService(); + break; + case "status": + showStatus(); + break; + case "code": + if (!isServiceRunning()) { + console.log("Service not running, starting service..."); + await run({ daemon: true }); + // Wait for service to start, exit with error if timeout + if (await waitForService()) { + executeCodeCommand(process.argv.slice(3)); + } else { + console.error( + "Service startup timeout, please manually run claude-code start to start the service" + ); + process.exit(1); + } + } else { + executeCodeCommand(process.argv.slice(3)); + } + break; + case "-v": + case "version": + console.log(`claude-code version: ${version}`); + break; + case "-h": + case "help": + console.log(HELP_TEXT); + break; + default: + console.log(HELP_TEXT); + process.exit(1); + } +} + +main().catch(console.error); diff --git a/src/constants.ts b/src/constants.ts index 644bf6f..c143533 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -7,9 +7,12 @@ export const CONFIG_FILE = `${HOME_DIR}/config.json`; export const PLUGINS_DIR = `${HOME_DIR}/plugins`; +export const PID_FILE = path.join(HOME_DIR, '.claude-code-router.pid'); + + export const DEFAULT_CONFIG = { log: false, OPENAI_API_KEY: "", - OPENAI_BASE_URL: "https://openrouter.ai/api/v1", - OPENAI_MODEL: "openai/o3-mini", + OPENAI_BASE_URL: "", + OPENAI_MODEL: "", }; diff --git a/src/index.ts b/src/index.ts index 6fd70c9..498d0b8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,8 @@ import { formatRequest } from "./middlewares/formatRequest"; import { rewriteBody } from "./middlewares/rewriteBody"; import OpenAI from "openai"; import { streamOpenAIResponse } from "./utils/stream"; +import { isServiceRunning, savePid } from "./utils/processCheck"; +import { fork } from "child_process"; async function initializeClaudeConfig() { const homeDir = process.env.HOME; @@ -20,18 +22,40 @@ async function initializeClaudeConfig() { autoUpdaterStatus: "enabled", userID, hasCompletedOnboarding: true, - lastOnboardingVersion: "0.2.9", + lastOnboardingVersion: "1.0.17", projects: {}, }; await writeFile(configPath, JSON.stringify(configContent, null, 2)); } } -async function run() { +interface RunOptions { + port?: number; + daemon?: boolean; +} + +async function run(options: RunOptions = {}) { + const port = options.port || 3456; + + // Check if service is already running + if (isServiceRunning()) { + console.log("✅ Service is already running in the background."); + return; + } + await initializeClaudeConfig(); await initDir(); await initConfig(); - const server = createServer(3456); + + // Save the PID of the background process + savePid(process.pid); + + // 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(servicePort); server.useMiddleware(formatRequest); server.useMiddleware(rewriteBody); @@ -46,11 +70,13 @@ async function run() { req.body.model = process.env.OPENAI_MODEL; } const completion: any = await openai.chat.completions.create(req.body); - await streamOpenAIResponse(res, completion, req.body.model); + await streamOpenAIResponse(res, completion, req.body.model, req.body); } catch (e) { console.error("Error in OpenAI API call:", e); } }); server.start(); + console.log(`🚀 Claude Code Router is running on port ${servicePort}`); } -run(); + +export { run }; diff --git a/src/middlewares/formatRequest.ts b/src/middlewares/formatRequest.ts index e5ab184..bbb5177 100644 --- a/src/middlewares/formatRequest.ts +++ b/src/middlewares/formatRequest.ts @@ -3,6 +3,7 @@ import { ContentBlockParam } from "@anthropic-ai/sdk/resources"; import { MessageCreateParamsBase } from "@anthropic-ai/sdk/resources/messages"; import OpenAI from "openai"; import { streamOpenAIResponse } from "../utils/stream"; +import { log } from "../utils/log"; export const formatRequest = async ( req: Request, @@ -17,33 +18,138 @@ export const formatRequest = async ( temperature, metadata, tools, + stream, }: MessageCreateParamsBase = req.body; + log("formatRequest: ", req.body); try { - const openAIMessages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = - messages.map((item) => { - if (item.content instanceof Array) { - return { - role: item.role, - content: item.content - .map((it: ContentBlockParam) => { - if (it.type === "text") { - return typeof it.text === "string" - ? it.text - : JSON.stringify(it); - } - return JSON.stringify(it); - }) - .join(""), - } as OpenAI.Chat.Completions.ChatCompletionMessageParam; - } - return { - role: item.role, - content: - typeof item.content === "string" - ? item.content - : JSON.stringify(item.content), - }; - }); + // @ts-ignore + const openAIMessages = Array.isArray(messages) + ? messages.flatMap((anthropicMessage) => { + const openAiMessagesFromThisAnthropicMessage = []; + + if (!Array.isArray(anthropicMessage.content)) { + // Handle simple string content + if (typeof anthropicMessage.content === "string") { + openAiMessagesFromThisAnthropicMessage.push({ + role: anthropicMessage.role, + content: anthropicMessage.content, + }); + } + // If content is not string and not array (e.g. null/undefined), it will result in an empty array, effectively skipping this message. + return openAiMessagesFromThisAnthropicMessage; + } + + // Handle array content + if (anthropicMessage.role === "assistant") { + const assistantMessage = { + role: "assistant", + content: null, // Will be populated if text parts exist + }; + let textContent = ""; + // @ts-ignore + const toolCalls = []; // Corrected type here + + anthropicMessage.content.forEach((contentPart) => { + if (contentPart.type === "text") { + textContent += + (typeof contentPart.text === "string" + ? contentPart.text + : JSON.stringify(contentPart.text)) + "\\n"; + } else if (contentPart.type === "tool_use") { + toolCalls.push({ + id: contentPart.id, + type: "function", + function: { + name: contentPart.name, + arguments: JSON.stringify(contentPart.input), + }, + }); + } + }); + + const trimmedTextContent = textContent.trim(); + if (trimmedTextContent.length > 0) { + // @ts-ignore + assistantMessage.content = trimmedTextContent; + } + if (toolCalls.length > 0) { + // @ts-ignore + assistantMessage.tool_calls = toolCalls; + } + // @ts-ignore + if ( + assistantMessage.content || + // @ts-ignore + (assistantMessage.tool_calls && + // @ts-ignore + assistantMessage.tool_calls.length > 0) + ) { + openAiMessagesFromThisAnthropicMessage.push(assistantMessage); + } + } else if (anthropicMessage.role === "user") { + // For user messages, text parts are combined into one message. + // Tool results are transformed into subsequent, separate 'tool' role messages. + let userTextMessageContent = ""; + // @ts-ignore + const subsequentToolMessages = []; + + anthropicMessage.content.forEach((contentPart) => { + if (contentPart.type === "text") { + userTextMessageContent += + (typeof contentPart.text === "string" + ? contentPart.text + : JSON.stringify(contentPart.text)) + "\\n"; + } else if (contentPart.type === "tool_result") { + // Each tool_result becomes a separate 'tool' message + subsequentToolMessages.push({ + role: "tool", + tool_call_id: contentPart.tool_use_id, + content: + typeof contentPart.content === "string" + ? contentPart.content + : JSON.stringify(contentPart.content), + }); + } + }); + + const trimmedUserText = userTextMessageContent.trim(); + if (trimmedUserText.length > 0) { + openAiMessagesFromThisAnthropicMessage.push({ + role: "user", + content: trimmedUserText, + }); + } + // @ts-ignore + openAiMessagesFromThisAnthropicMessage.push( + // @ts-ignore + ...subsequentToolMessages + ); + } else { + // Fallback for other roles (e.g. system, or custom roles if they were to appear here with array content) + // This will combine all text parts into a single message for that role. + let combinedContent = ""; + anthropicMessage.content.forEach((contentPart) => { + if (contentPart.type === "text") { + combinedContent += + (typeof contentPart.text === "string" + ? contentPart.text + : JSON.stringify(contentPart.text)) + "\\n"; + } else { + // For non-text parts in other roles, stringify them or handle as appropriate + combinedContent += JSON.stringify(contentPart) + "\\n"; + } + }); + const trimmedCombinedContent = combinedContent.trim(); + if (trimmedCombinedContent.length > 0) { + openAiMessagesFromThisAnthropicMessage.push({ + role: anthropicMessage.role, // Cast needed as role could be other than 'user'/'assistant' + content: trimmedCombinedContent, + }); + } + } + return openAiMessagesFromThisAnthropicMessage; + }) + : []; const systemMessages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = Array.isArray(system) ? system.map((item) => ({ @@ -51,11 +157,11 @@ export const formatRequest = async ( content: item.text, })) : [{ role: "system", content: system }]; - const data: OpenAI.Chat.Completions.ChatCompletionCreateParams = { + const data: any = { model, messages: [...systemMessages, ...openAIMessages], temperature, - stream: true, + stream, }; if (tools) { data.tools = tools @@ -69,7 +175,9 @@ export const formatRequest = async ( }, })); } - res.setHeader("Content-Type", "text/event-stream"); + if (stream) { + res.setHeader("Content-Type", "text/event-stream"); + } res.setHeader("Cache-Control", "no-cache"); res.setHeader("Connection", "keep-alive"); req.body = data; @@ -95,7 +203,7 @@ export const formatRequest = async ( }; }, }; - await streamOpenAIResponse(res, errorCompletion, model); + await streamOpenAIResponse(res, errorCompletion, model, req.body); } next(); }; diff --git a/src/utils/close.ts b/src/utils/close.ts new file mode 100644 index 0000000..b0368da --- /dev/null +++ b/src/utils/close.ts @@ -0,0 +1,23 @@ +import { isServiceRunning, cleanupPidFile } from './processCheck'; +import { existsSync, readFileSync } from 'fs'; +import { homedir } from 'os'; +import { join } from 'path'; + +export async function closeService() { + const PID_FILE = join(homedir(), '.claude-code-router.pid'); + + if (!isServiceRunning()) { + console.log("No service is currently running."); + return; + } + + try { + const pid = parseInt(readFileSync(PID_FILE, 'utf-8')); + process.kill(pid); + cleanupPidFile(); + console.log("Service has been successfully stopped."); + } catch (e) { + console.log("Failed to stop the service. It may have already been stopped."); + cleanupPidFile(); + } +} diff --git a/src/utils/codeCommand.ts b/src/utils/codeCommand.ts new file mode 100644 index 0000000..5cd9564 --- /dev/null +++ b/src/utils/codeCommand.ts @@ -0,0 +1,31 @@ +import { spawn } from 'child_process'; +import { isServiceRunning } from './processCheck'; + +export async function executeCodeCommand(args: string[] = []) { + // Service check is now handled in cli.ts + + // Set environment variables + const env = { + ...process.env, + DISABLE_PROMPT_CACHING: '1', + ANTHROPIC_BASE_URL: 'http://127.0.0.1:3456', + API_TIMEOUT_MS: '600000' + }; + + // Execute claude command + const claudeProcess = spawn('claude', args, { + env, + stdio: 'inherit', + shell: true + }); + + claudeProcess.on('error', (error) => { + console.error('Failed to start claude command:', error.message); + console.log('Make sure Claude Code is installed: npm install -g @anthropic-ai/claude-code'); + process.exit(1); + }); + + claudeProcess.on('close', (code) => { + process.exit(code || 0); + }); +} diff --git a/src/utils/index.ts b/src/utils/index.ts index d218bb6..12367b3 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -57,28 +57,21 @@ export const readConfigFile = async () => { const config = await fs.readFile(CONFIG_FILE, "utf-8"); return JSON.parse(config); } catch { - const useRouter = await confirm( - "No config file found. Enable router mode? (Y/n)" - ); - if (!useRouter) { - const apiKey = await question("Enter OPENAI_API_KEY: "); - const baseUrl = await question("Enter OPENAI_BASE_URL: "); - const model = await question("Enter OPENAI_MODEL: "); - const config = Object.assign({}, DEFAULT_CONFIG, { - OPENAI_API_KEY: apiKey, - OPENAI_BASE_URL: baseUrl, - OPENAI_MODEL: model, - }); - await writeConfigFile(config); - return config; - } else { - const router = await question("Enter OPENAI_API_KEY: "); - return DEFAULT_CONFIG; - } + const apiKey = await question("Enter OPENAI_API_KEY: "); + const baseUrl = await question("Enter OPENAI_BASE_URL: "); + const model = await question("Enter OPENAI_MODEL: "); + const config = Object.assign({}, DEFAULT_CONFIG, { + OPENAI_API_KEY: apiKey, + OPENAI_BASE_URL: baseUrl, + OPENAI_MODEL: model, + }); + await writeConfigFile(config); + return config; } }; export const writeConfigFile = async (config: any) => { + await ensureDir(HOME_DIR); await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2)); }; diff --git a/src/utils/log.ts b/src/utils/log.ts index 8f9f271..6999726 100644 --- a/src/utils/log.ts +++ b/src/utils/log.ts @@ -1,8 +1,8 @@ -import fs from 'node:fs'; -import path from 'node:path'; -import { HOME_DIR } from '../constants'; +import fs from "node:fs"; +import path from "node:path"; +import { HOME_DIR } from "../constants"; -const LOG_FILE = path.join(HOME_DIR, 'claude-code-router.log'); +const LOG_FILE = path.join(HOME_DIR, "claude-code-router.log"); // Ensure log directory exists if (!fs.existsSync(HOME_DIR)) { @@ -11,17 +11,23 @@ if (!fs.existsSync(HOME_DIR)) { export function log(...args: any[]) { // Check if logging is enabled via environment variable - const isLogEnabled = process.env.LOG === 'true'; - + const isLogEnabled = process.env.LOG === "true"; + if (!isLogEnabled) { return; } const timestamp = new Date().toISOString(); - const logMessage = `[${timestamp}] ${args.map(arg => - typeof arg === 'object' ? JSON.stringify(arg) : String(arg) - ).join(' ')}\n`; + const logMessage = `[${timestamp}] ${ + Array.isArray(args) + ? args + .map((arg) => + typeof arg === "object" ? JSON.stringify(arg) : String(arg) + ) + .join(" ") + : "" + }\n`; // Append to log file - fs.appendFileSync(LOG_FILE, logMessage, 'utf8'); + fs.appendFileSync(LOG_FILE, logMessage, "utf8"); } diff --git a/src/utils/processCheck.ts b/src/utils/processCheck.ts new file mode 100644 index 0000000..152acf1 --- /dev/null +++ b/src/utils/processCheck.ts @@ -0,0 +1,60 @@ +import { existsSync, readFileSync, writeFileSync } from 'fs'; +import { PID_FILE } from '../constants'; + + +export function isServiceRunning(): boolean { + if (!existsSync(PID_FILE)) { + return false; + } + + try { + const pid = parseInt(readFileSync(PID_FILE, 'utf-8')); + process.kill(pid, 0); + return true; + } catch (e) { + // Process not running, clean up pid file + cleanupPidFile(); + return false; + } +} + +export function savePid(pid: number) { + writeFileSync(PID_FILE, pid.toString()); +} + +export function cleanupPidFile() { + if (existsSync(PID_FILE)) { + try { + const fs = require('fs'); + fs.unlinkSync(PID_FILE); + } catch (e) { + // Ignore cleanup errors + } + } +} + +export function getServicePid(): number | null { + if (!existsSync(PID_FILE)) { + return null; + } + + try { + const pid = parseInt(readFileSync(PID_FILE, 'utf-8')); + return isNaN(pid) ? null : pid; + } catch (e) { + return null; + } +} + +export function getServiceInfo() { + const pid = getServicePid(); + const running = isServiceRunning(); + + return { + running, + pid, + port: 3456, + endpoint: 'http://127.0.0.1:3456', + pidFile: PID_FILE + }; +} diff --git a/src/utils/status.ts b/src/utils/status.ts new file mode 100644 index 0000000..d58d0b1 --- /dev/null +++ b/src/utils/status.ts @@ -0,0 +1,27 @@ +import { getServiceInfo } from './processCheck'; + +export function showStatus() { + const info = getServiceInfo(); + + console.log('\n📊 Claude Code Router Status'); + console.log('═'.repeat(40)); + + if (info.running) { + console.log('✅ Status: Running'); + console.log(`🆔 Process ID: ${info.pid}`); + console.log(`🌐 Port: ${info.port}`); + console.log(`📡 API Endpoint: ${info.endpoint}`); + console.log(`📄 PID File: ${info.pidFile}`); + console.log(''); + console.log('🚀 Ready to use! Run the following commands:'); + console.log(' claude-code-router code # Start coding with Claude'); + console.log(' claude-code-router close # Stop the service'); + } else { + console.log('❌ Status: Not Running'); + console.log(''); + console.log('💡 To start the service:'); + console.log(' claude-code-router start'); + } + + console.log(''); +} diff --git a/src/utils/stream.ts b/src/utils/stream.ts index b71ec50..7f3c4e7 100644 --- a/src/utils/stream.ts +++ b/src/utils/stream.ts @@ -1,5 +1,6 @@ import { Response } from "express"; import { OpenAI } from "openai"; +import { log } from "./log"; interface ContentBlock { type: string; @@ -42,10 +43,40 @@ interface MessageEvent { export async function streamOpenAIResponse( res: Response, - completion: AsyncIterable, - model: string + completion: any, + model: string, + body: any ) { + const write = (data: string) => { + log("response: ", data); + res.write(data); + }; const messageId = "msg_" + Date.now(); + if (!body.stream) { + res.json({ + id: messageId, + type: "message", + role: "assistant", + // @ts-ignore + content: completion.choices[0].message.content || completion.choices[0].message.tool_calls?.map((item) => { + return { + type: 'tool_use', + id: item.id, + name: item.function?.name, + input: item.function?.arguments ? JSON.parse(item.function.arguments) : {}, + }; + }) || '', + stop_reason: completion.choices[0].finish_reason === 'tool_calls' ? "tool_use" : "end_turn", + stop_sequence: null, + usage: { + input_tokens: 100, + output_tokens: 50, + }, + }); + res.end(); + return; + } + let contentBlockIndex = 0; let currentContentBlocks: ContentBlock[] = []; @@ -63,7 +94,7 @@ export async function streamOpenAIResponse( usage: { input_tokens: 1, output_tokens: 1 }, }, }; - res.write(`event: message_start\ndata: ${JSON.stringify(messageStart)}\n\n`); + write(`event: message_start\ndata: ${JSON.stringify(messageStart)}\n\n`); let isToolUse = false; let toolUseJson = ""; @@ -71,6 +102,7 @@ export async function streamOpenAIResponse( try { for await (const chunk of completion) { + log("Processing chunk:", chunk); const delta = chunk.choices[0].delta; if (delta.tool_calls && delta.tool_calls.length > 0) { @@ -94,7 +126,7 @@ export async function streamOpenAIResponse( currentContentBlocks.push(toolBlock); - res.write( + write( `event: content_block_start\ndata: ${JSON.stringify( toolBlockStart )}\n\n` @@ -119,23 +151,25 @@ export async function streamOpenAIResponse( const parsedJson = JSON.parse(toolUseJson); currentContentBlocks[contentBlockIndex].input = parsedJson; } catch (e) { + log(e); // JSON not yet complete, continue accumulating } - res.write( + write( `event: content_block_delta\ndata: ${JSON.stringify(jsonDelta)}\n\n` ); } } else if (delta.content) { // Handle regular text content if (isToolUse) { + log("Tool call ended here:", delta); // End previous tool call block const contentBlockStop: MessageEvent = { type: "content_block_stop", index: contentBlockIndex, }; - res.write( + write( `event: content_block_stop\ndata: ${JSON.stringify( contentBlockStop )}\n\n` @@ -161,7 +195,7 @@ export async function streamOpenAIResponse( currentContentBlocks.push(textBlock); - res.write( + write( `event: content_block_start\ndata: ${JSON.stringify( textBlockStart )}\n\n` @@ -184,7 +218,7 @@ export async function streamOpenAIResponse( currentContentBlocks[contentBlockIndex].text += delta.content; } - res.write( + write( `event: content_block_delta\ndata: ${JSON.stringify( contentDelta )}\n\n` @@ -207,7 +241,7 @@ export async function streamOpenAIResponse( currentContentBlocks.push(textBlock); - res.write( + write( `event: content_block_start\ndata: ${JSON.stringify( textBlockStart )}\n\n` @@ -230,7 +264,7 @@ export async function streamOpenAIResponse( currentContentBlocks[contentBlockIndex].text += JSON.stringify(e); } - res.write( + write( `event: content_block_delta\ndata: ${JSON.stringify(contentDelta)}\n\n` ); } @@ -241,7 +275,7 @@ export async function streamOpenAIResponse( index: contentBlockIndex, }; - res.write( + write( `event: content_block_stop\ndata: ${JSON.stringify(contentBlockStop)}\n\n` ); @@ -255,14 +289,17 @@ export async function streamOpenAIResponse( }, usage: { input_tokens: 100, output_tokens: 150 }, }; + if (!isToolUse) { + log("body: ", body, "messageDelta: ", messageDelta); + } - res.write(`event: message_delta\ndata: ${JSON.stringify(messageDelta)}\n\n`); + write(`event: message_delta\ndata: ${JSON.stringify(messageDelta)}\n\n`); // Send message_stop event const messageStop: MessageEvent = { type: "message_stop", }; - res.write(`event: message_stop\ndata: ${JSON.stringify(messageStop)}\n\n`); + write(`event: message_stop\ndata: ${JSON.stringify(messageStop)}\n\n`); res.end(); }