[
  {
    "path": ".github/workflows/docker-publish.yml",
    "content": "name: Build and Publish Docker Image\n\non:\n  push:\n    tags:\n      - 'v*.*.*'\n  workflow_dispatch:\n\nenv:\n  DOCKER_IMAGE: musistudio/claude-code-router\n\njobs:\n  build-and-push:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      packages: write\n      id-token: write\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n\n      - name: Install pnpm\n        uses: pnpm/action-setup@v4\n        with:\n          version: latest\n\n      - name: Get version\n        id: version\n        run: |\n          if [[ $GITHUB_REF == refs/tags/* ]]; then\n            VERSION=${GITHUB_REF#refs/tags/}\n          else\n            VERSION=$(node -p \"require('./package.json').version\")\n          fi\n          echo \"version=$VERSION\" >> $GITHUB_OUTPUT\n          echo \"Docker image version: $VERSION\"\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Build packages\n        run: |\n          pnpm build\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Login to Docker Hub\n        uses: docker/login-action@v3\n        with:\n          username: ${{ secrets.DOCKER_USERNAME }}\n          password: ${{ secrets.DOCKER_PASSWORD }}\n\n      - name: Extract metadata\n        id: meta\n        uses: docker/metadata-action@v5\n        with:\n          images: ${{ env.DOCKER_IMAGE }}\n          tags: |\n            type=semver,pattern={{version}},value=${{ steps.version.outputs.version }}\n            type=semver,pattern={{major}}.{{minor}},value=${{ steps.version.outputs.version }}\n            type=raw,value=latest\n            type=sha\n\n      - name: Build and push Docker image\n        uses: docker/build-push-action@v5\n        with:\n          context: .\n          file: ./packages/server/Dockerfile\n          platforms: linux/amd64,linux/arm64\n          push: true\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n          cache-from: type=gha\n          cache-to: type=gha,mode=max\n\n      - name: Image digest\n        run: echo \"Image pushed with digest ${{ steps.meta.outputs.digest }}\"\n"
  },
  {
    "path": ".github/workflows/docs.yml",
    "content": "name: Deploy Docs to GitHub Pages\n\non:\n  push:\n    branches:\n      - main\n    paths:\n      - 'docs/**'\n      - '.github/workflows/docs.yml'\n  workflow_dispatch:\n\npermissions:\n  contents: read\n  pages: write\n  id-token: write\n\nconcurrency:\n  group: pages\n  cancel-in-progress: false\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n\n      - name: Install dependencies\n        working-directory: ./docs\n        run: npm install\n\n      - name: Build Docusaurus\n        working-directory: ./docs\n        run: npm run build\n\n      - name: Upload artifact\n        uses: actions/upload-pages-artifact@v3\n        with:\n          path: ./docs/build\n\n  deploy:\n    environment:\n      name: github-pages\n      url: ${{ steps.deployment.outputs.page_url }}\n    runs-on: ubuntu-latest\n    needs: build\n    steps:\n      - name: Deploy to GitHub Pages\n        id: deployment\n        uses: actions/deploy-pages@v4\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules\n.env\nlog.txt\n.idea\ndist\n.DS_Store\n.vscode\ntsconfig.tsbuildinfo\n\n# Documentation build output\ndocs/build\ndocs/.docusaurus\n"
  },
  {
    "path": ".npmignore",
    "content": "src\nnode_modules\n.claude\nCLAUDE.md\nscreenshoots\n.DS_Store\n.vscode\n.idea\n.env\n.blog\ndocs\n.log\nblog\nconfig.json\nui\nscripts\npackages\ncustom-router.example.js\nexamples\n"
  },
  {
    "path": "CLAUDE.md",
    "content": "# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Project Overview\n\nClaude Code Router is a tool that routes Claude Code requests to different LLM providers. It uses a Monorepo architecture with four main packages:\n\n- **cli** (`@musistudio/claude-code-router`): Command-line tool providing the `ccr` command\n- **server** (`@CCR/server`): Core server handling API routing and transformations\n- **shared** (`@CCR/shared`): Shared constants, utilities, and preset management\n- **ui** (`@CCR/ui`): Web management interface (React + Vite)\n\n## Build Commands\n\n### Build all packages\n```bash\npnpm build\n```\n\n### Build individual packages\n```bash\npnpm build:cli      # Build CLI\npnpm build:server   # Build Server\npnpm build:ui       # Build UI\n```\n\n### Development mode\n```bash\npnpm dev:cli        # Develop CLI (ts-node)\npnpm dev:server     # Develop Server (ts-node)\npnpm dev:ui         # Develop UI (Vite)\n```\n\n### Publish\n```bash\npnpm release        # Build and publish all packages\n```\n\n## Core Architecture\n\n### 1. Routing System (packages/server/src/utils/router.ts)\n\nThe routing logic determines which model a request should be sent to:\n\n- **Default routing**: Uses `Router.default` configuration\n- **Project-level routing**: Checks `~/.claude/projects/<project-id>/claude-code-router.json`\n- **Custom routing**: Loads custom JavaScript router function via `CUSTOM_ROUTER_PATH`\n- **Built-in scenario routing**:\n  - `background`: Background tasks (typically lightweight models)\n  - `think`: Thinking-intensive tasks (Plan Mode)\n  - `longContext`: Long context (exceeds `longContextThreshold` tokens)\n  - `webSearch`: Web search tasks\n  - `image`: Image-related tasks\n\nToken calculation uses `tiktoken` (cl100k_base) to estimate request size.\n\n### 2. Transformer System\n\nThe project uses the `@musistudio/llms` package (external dependency) to handle request/response transformations. Transformers adapt to different provider API differences:\n\n- Built-in transformers: `anthropic`, `deepseek`, `gemini`, `openrouter`, `groq`, `maxtoken`, `tooluse`, `reasoning`, `enhancetool`, etc.\n- Custom transformers: Load external plugins via `transformers` array in `config.json`\n\nTransformer configuration supports:\n- Global application (provider level)\n- Model-specific application\n- Option passing (e.g., `max_tokens` parameter for `maxtoken`)\n\n### 3. Agent System (packages/server/src/agents/)\n\nAgents are pluggable feature modules that can:\n- Detect whether to handle a request (`shouldHandle`)\n- Modify requests (`reqHandler`)\n- Provide custom tools (`tools`)\n\nBuilt-in agents:\n- **imageAgent**: Handles image-related tasks\n\nAgent tool call flow:\n1. Detect and mark agents in `preHandler` hook\n2. Add agent tools to the request\n3. Intercept tool call events in `onSend` hook\n4. Execute agent tool and initiate new LLM request\n5. Stream results back\n\n### 4. SSE Stream Processing\n\nThe server uses custom Transform streams to handle Server-Sent Events:\n- `SSEParserTransform`: Parses SSE text stream into event objects\n- `SSESerializerTransform`: Serializes event objects into SSE text stream\n- `rewriteStream`: Intercepts and modifies stream data (for agent tool calls)\n\n### 5. Configuration Management\n\nConfiguration file location: `~/.claude-code-router/config.json`\n\nKey features:\n- Supports environment variable interpolation (`$VAR_NAME` or `${VAR_NAME}`)\n- JSON5 format (supports comments)\n- Automatic backups (keeps last 3 backups)\n- Hot reload requires service restart (`ccr restart`)\n\nConfiguration validation:\n- If `Providers` are configured, both `HOST` and `APIKEY` must be set\n- Otherwise listens on `0.0.0.0` without authentication\n\n### 6. Logging System\n\nTwo separate logging systems:\n\n**Server-level logs** (pino):\n- Location: `~/.claude-code-router/logs/ccr-*.log`\n- Content: HTTP requests, API calls, server events\n- Configuration: `LOG_LEVEL` (fatal/error/warn/info/debug/trace)\n\n**Application-level logs**:\n- Location: `~/.claude-code-router/claude-code-router.log`\n- Content: Routing decisions, business logic events\n\n## CLI Commands\n\n```bash\nccr start      # Start server\nccr stop       # Stop server\nccr restart    # Restart server\nccr status     # Show status\nccr code       # Execute claude command\nccr model      # Interactive model selection and configuration\nccr preset     # Manage presets (export, install, list, info, delete)\nccr activate   # Output shell environment variables (for integration)\nccr ui         # Open Web UI\nccr statusline # Integrated statusline (reads JSON from stdin)\n```\n\n### Preset Commands\n\n```bash\nccr preset export <name>      # Export current configuration as a preset\nccr preset install <source>   # Install a preset from file, URL, or name\nccr preset list               # List all installed presets\nccr preset info <name>        # Show preset information\nccr preset delete <name>      # Delete a preset\n```\n\n## Subagent Routing\n\nUse special tags in subagent prompts to specify models:\n```\n<CCR-SUBAGENT-MODEL>provider,model</CCR-SUBAGENT-MODEL>\nPlease help me analyze this code...\n```\n\n## Preset System\n\nThe preset system allows users to save, share, and reuse configurations easily.\n\n### Preset Structure\n\nPresets are stored in `~/.claude-code-router/presets/<preset-name>/manifest.json`\n\nEach preset contains:\n- **Metadata**: name, version, description, author, keywords, etc.\n- **Configuration**: Providers, Router, transformers, and other settings\n- **Dynamic Schema** (optional): Input fields for collecting required information during installation\n- **Required Inputs** (optional): Fields that need to be filled during installation (e.g., API keys)\n\n### Core Functions\n\nLocated in `packages/shared/src/preset/`:\n\n- **export.ts**: Export current configuration as a preset directory\n  - `exportPreset(presetName, config, options)`: Creates preset directory with manifest.json\n  - Automatically sanitizes sensitive data (api_key fields become `{{field}}` placeholders)\n\n- **install.ts**: Install and manage presets\n  - `installPreset(preset, config, options)`: Install preset to config\n  - `loadPreset(source)`: Load preset from directory\n  - `listPresets()`: List all installed presets\n  - `isPresetInstalled(presetName)`: Check if preset is installed\n  - `validatePreset(preset)`: Validate preset structure\n\n- **merge.ts**: Merge preset configuration with existing config\n  - Handles conflicts using different strategies (ask, overwrite, merge, skip)\n\n- **sensitiveFields.ts**: Identify and sanitize sensitive fields\n  - Detects api_key, password, secret fields automatically\n  - Replaces sensitive values with environment variable placeholders\n\n### Preset File Format\n\n**manifest.json** (in preset directory):\n```json\n{\n  \"name\": \"my-preset\",\n  \"version\": \"1.0.0\",\n  \"description\": \"My configuration\",\n  \"author\": \"Author Name\",\n  \"keywords\": [\"openai\", \"production\"],\n  \"Providers\": [...],\n  \"Router\": {...},\n  \"schema\": [\n    {\n      \"id\": \"apiKey\",\n      \"type\": \"password\",\n      \"label\": \"OpenAI API Key\",\n      \"prompt\": \"Enter your OpenAI API key\"\n    }\n  ]\n}\n```\n\n### CLI Integration\n\nThe CLI layer (`packages/cli/src/utils/preset/`) handles:\n- User interaction and prompts\n- File operations\n- Display formatting\n\nKey files:\n- `commands.ts`: Command handlers for `ccr preset` subcommands\n- `export.ts`: CLI wrapper for export functionality\n- `install.ts`: CLI wrapper for install functionality\n\n## Dependencies\n\n```\ncli → server → shared\nserver → @musistudio/llms (core routing and transformation logic)\nui (standalone frontend application)\n```\n\n## Development Notes\n\n1. **Node.js version**: Requires >= 18.0.0\n2. **Package manager**: Uses pnpm (monorepo depends on workspace protocol)\n3. **TypeScript**: All packages use TypeScript, but UI package is ESM module\n4. **Build tools**:\n   - cli/server/shared: esbuild\n   - ui: Vite + TypeScript\n5. **@musistudio/llms**: This is an external dependency package providing the core server framework and transformer functionality, type definitions in `packages/server/src/types.d.ts`\n6. **Code comments**: All comments in code MUST be written in English\n7. **Documentation**: When implementing new features, add documentation to the docs project instead of creating standalone md files\n\n## Configuration Example Locations\n\n- Main configuration example: Complete example in README.md\n- Custom router example: `custom-router.example.js`\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 musistudio\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "![](blog/images/claude-code-router-img.png)\n\n[![](https://img.shields.io/badge/%F0%9F%87%A8%F0%9F%87%B3-%E4%B8%AD%E6%96%87%E7%89%88-ff0000?style=flat)](README_zh.md)\n[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?&logo=discord&logoColor=white)](https://discord.gg/rdftVMaUcS)\n[![](https://img.shields.io/github/license/musistudio/claude-code-router)](https://github.com/musistudio/claude-code-router/blob/main/LICENSE)\n\n<hr>\n\n![](blog/images/sponsors/glm-en.jpg)\n> This project is sponsored by Z.ai, supporting us with their GLM CODING PLAN.\n\n> GLM CODING PLAN is a subscription service designed for AI coding, starting at just $10/month. It provides access to their flagship GLM-4.7 & （GLM-5 Only Available  for Pro Users）model across 10+ popular AI coding tools (Claude Code, Cline, Roo Code, etc.), offering developers top-tier, fast, and stable coding experiences.\n\n> Get 10% OFF GLM CODING PLAN：https://z.ai/subscribe?ic=8JVLJQFSKB  \n\n> [Progressive Disclosure of Agent Tools from the Perspective of CLI Tool Style](/blog/en/progressive-disclosure-of-agent-tools-from-the-perspective-of-cli-tool-style.md)\n\n> A powerful tool to route Claude Code requests to different models and customize any request.\n\n![](blog/images/claude-code.png)\n\n## ✨ Features\n\n- **Model Routing**: Route requests to different models based on your needs (e.g., background tasks, thinking, long context).\n- **Multi-Provider Support**: Supports various model providers like OpenRouter, DeepSeek, Ollama, Gemini, Volcengine, and SiliconFlow.\n- **Request/Response Transformation**: Customize requests and responses for different providers using transformers.\n- **Dynamic Model Switching**: Switch models on-the-fly within Claude Code using the `/model` command.\n- **CLI Model Management**: Manage models and providers directly from the terminal with `ccr model`.\n- **GitHub Actions Integration**: Trigger Claude Code tasks in your GitHub workflows.\n- **Plugin System**: Extend functionality with custom transformers.\n\n## 🚀 Getting Started\n\n### 1. Installation\n\nFirst, ensure you have [Claude Code](https://docs.anthropic.com/en/docs/claude-code/quickstart) installed:\n\n```shell\nnpm install -g @anthropic-ai/claude-code\n```\n\nThen, install Claude Code Router:\n\n```shell\nnpm install -g @musistudio/claude-code-router\n```\n\n### 2. Configuration\n\nCreate and configure your `~/.claude-code-router/config.json` file. For more details, you can refer to `config.example.json`.\n\nThe `config.json` file has several key sections:\n\n- **`PROXY_URL`** (optional): You can set a proxy for API requests, for example: `\"PROXY_URL\": \"http://127.0.0.1:7890\"`.\n- **`LOG`** (optional): You can enable logging by setting it to `true`. When set to `false`, no log files will be created. Default is `true`.\n- **`LOG_LEVEL`** (optional): Set the logging level. Available options are: `\"fatal\"`, `\"error\"`, `\"warn\"`, `\"info\"`, `\"debug\"`, `\"trace\"`. Default is `\"debug\"`.\n- **Logging Systems**: The Claude Code Router uses two separate logging systems:\n  - **Server-level logs**: HTTP requests, API calls, and server events are logged using pino in the `~/.claude-code-router/logs/` directory with filenames like `ccr-*.log`\n  - **Application-level logs**: Routing decisions and business logic events are logged in `~/.claude-code-router/claude-code-router.log`\n- **`APIKEY`** (optional): You can set a secret key to authenticate requests. When set, clients must provide this key in the `Authorization` header (e.g., `Bearer your-secret-key`) or the `x-api-key` header. Example: `\"APIKEY\": \"your-secret-key\"`.\n- **`HOST`** (optional): You can set the host address for the server. If `APIKEY` is not set, the host will be forced to `127.0.0.1` for security reasons to prevent unauthorized access. Example: `\"HOST\": \"0.0.0.0\"`.\n- **`NON_INTERACTIVE_MODE`** (optional): When set to `true`, enables compatibility with non-interactive environments like GitHub Actions, Docker containers, or other CI/CD systems. This sets appropriate environment variables (`CI=true`, `FORCE_COLOR=0`, etc.) and configures stdin handling to prevent the process from hanging in automated environments. Example: `\"NON_INTERACTIVE_MODE\": true`.\n\n- **`Providers`**: Used to configure different model providers.\n- **`Router`**: Used to set up routing rules. `default` specifies the default model, which will be used for all requests if no other route is configured.\n- **`API_TIMEOUT_MS`**: Specifies the timeout for API calls in milliseconds.\n\n#### Environment Variable Interpolation\n\nClaude Code Router supports environment variable interpolation for secure API key management. You can reference environment variables in your `config.json` using either `$VAR_NAME` or `${VAR_NAME}` syntax:\n\n```json\n{\n  \"OPENAI_API_KEY\": \"$OPENAI_API_KEY\",\n  \"GEMINI_API_KEY\": \"${GEMINI_API_KEY}\",\n  \"Providers\": [\n    {\n      \"name\": \"openai\",\n      \"api_base_url\": \"https://api.openai.com/v1/chat/completions\",\n      \"api_key\": \"$OPENAI_API_KEY\",\n      \"models\": [\"gpt-5\", \"gpt-5-mini\"]\n    }\n  ]\n}\n```\n\nThis allows you to keep sensitive API keys in environment variables instead of hardcoding them in configuration files. The interpolation works recursively through nested objects and arrays.\n\nHere is a comprehensive example:\n\n```json\n{\n  \"APIKEY\": \"your-secret-key\",\n  \"PROXY_URL\": \"http://127.0.0.1:7890\",\n  \"LOG\": true,\n  \"API_TIMEOUT_MS\": 600000,\n  \"NON_INTERACTIVE_MODE\": false,\n  \"Providers\": [\n    {\n      \"name\": \"openrouter\",\n      \"api_base_url\": \"https://openrouter.ai/api/v1/chat/completions\",\n      \"api_key\": \"sk-xxx\",\n      \"models\": [\n        \"google/gemini-2.5-pro-preview\",\n        \"anthropic/claude-sonnet-4\",\n        \"anthropic/claude-3.5-sonnet\",\n        \"anthropic/claude-3.7-sonnet:thinking\"\n      ],\n      \"transformer\": {\n        \"use\": [\"openrouter\"]\n      }\n    },\n    {\n      \"name\": \"deepseek\",\n      \"api_base_url\": \"https://api.deepseek.com/chat/completions\",\n      \"api_key\": \"sk-xxx\",\n      \"models\": [\"deepseek-chat\", \"deepseek-reasoner\"],\n      \"transformer\": {\n        \"use\": [\"deepseek\"],\n        \"deepseek-chat\": {\n          \"use\": [\"tooluse\"]\n        }\n      }\n    },\n    {\n      \"name\": \"ollama\",\n      \"api_base_url\": \"http://localhost:11434/v1/chat/completions\",\n      \"api_key\": \"ollama\",\n      \"models\": [\"qwen2.5-coder:latest\"]\n    },\n    {\n      \"name\": \"gemini\",\n      \"api_base_url\": \"https://generativelanguage.googleapis.com/v1beta/models/\",\n      \"api_key\": \"sk-xxx\",\n      \"models\": [\"gemini-2.5-flash\", \"gemini-2.5-pro\"],\n      \"transformer\": {\n        \"use\": [\"gemini\"]\n      }\n    },\n    {\n      \"name\": \"volcengine\",\n      \"api_base_url\": \"https://ark.cn-beijing.volces.com/api/v3/chat/completions\",\n      \"api_key\": \"sk-xxx\",\n      \"models\": [\"deepseek-v3-250324\", \"deepseek-r1-250528\"],\n      \"transformer\": {\n        \"use\": [\"deepseek\"]\n      }\n    },\n    {\n      \"name\": \"modelscope\",\n      \"api_base_url\": \"https://api-inference.modelscope.cn/v1/chat/completions\",\n      \"api_key\": \"\",\n      \"models\": [\"Qwen/Qwen3-Coder-480B-A35B-Instruct\", \"Qwen/Qwen3-235B-A22B-Thinking-2507\"],\n      \"transformer\": {\n        \"use\": [\n          [\n            \"maxtoken\",\n            {\n              \"max_tokens\": 65536\n            }\n          ],\n          \"enhancetool\"\n        ],\n        \"Qwen/Qwen3-235B-A22B-Thinking-2507\": {\n          \"use\": [\"reasoning\"]\n        }\n      }\n    },\n    {\n      \"name\": \"dashscope\",\n      \"api_base_url\": \"https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions\",\n      \"api_key\": \"\",\n      \"models\": [\"qwen3-coder-plus\"],\n      \"transformer\": {\n        \"use\": [\n          [\n            \"maxtoken\",\n            {\n              \"max_tokens\": 65536\n            }\n          ],\n          \"enhancetool\"\n        ]\n      }\n    },\n    {\n      \"name\": \"aihubmix\",\n      \"api_base_url\": \"https://aihubmix.com/v1/chat/completions\",\n      \"api_key\": \"sk-\",\n      \"models\": [\n        \"Z/glm-4.5\",\n        \"claude-opus-4-20250514\",\n        \"gemini-2.5-pro\"\n      ]\n    }\n  ],\n  \"Router\": {\n    \"default\": \"deepseek,deepseek-chat\",\n    \"background\": \"ollama,qwen2.5-coder:latest\",\n    \"think\": \"deepseek,deepseek-reasoner\",\n    \"longContext\": \"openrouter,google/gemini-2.5-pro-preview\",\n    \"longContextThreshold\": 60000,\n    \"webSearch\": \"gemini,gemini-2.5-flash\"\n  }\n}\n```\n\n### 3. Running Claude Code with the Router\n\nStart Claude Code using the router:\n\n```shell\nccr code\n```\n\n> **Note**: After modifying the configuration file, you need to restart the service for the changes to take effect:\n>\n> ```shell\n> ccr restart\n> ```\n\n### 4. UI Mode\n\nFor a more intuitive experience, you can use the UI mode to manage your configuration:\n\n```shell\nccr ui\n```\n\nThis will open a web-based interface where you can easily view and edit your `config.json` file.\n\n![UI](/blog/images/ui.png)\n\n### 5. CLI Model Management\n\nFor users who prefer terminal-based workflows, you can use the interactive CLI model selector:\n\n```shell\nccr model\n```\n![](blog/images/models.gif)\n\nThis command provides an interactive interface to:\n\n- View current configuration:\n- See all configured models (default, background, think, longContext, webSearch, image)\n- Switch models: Quickly change which model is used for each router type\n- Add new models: Add models to existing providers\n- Create new providers: Set up complete provider configurations including:\n   - Provider name and API endpoint\n   - API key\n   - Available models\n   - Transformer configuration with support for:\n     - Multiple transformers (openrouter, deepseek, gemini, etc.)\n     - Transformer options (e.g., maxtoken with custom limits)\n     - Provider-specific routing (e.g., OpenRouter provider preferences)\n\nThe CLI tool validates all inputs and provides helpful prompts to guide you through the configuration process, making it easy to manage complex setups without editing JSON files manually.\n\n### 6. Presets Management\n\nPresets allow you to save, share, and reuse configurations easily. You can export your current configuration as a preset and install presets from files or URLs.\n\n```shell\n# Export current configuration as a preset\nccr preset export my-preset\n\n# Export with metadata\nccr preset export my-preset --description \"My OpenAI config\" --author \"Your Name\" --tags \"openai,production\"\n\n# Install a preset from local directory\nccr preset install /path/to/preset\n\n# List all installed presets\nccr preset list\n\n# Show preset information\nccr preset info my-preset\n\n# Delete a preset\nccr preset delete my-preset\n```\n\n**Preset Features:**\n- **Export**: Save your current configuration as a preset directory (with manifest.json)\n- **Install**: Install presets from local directories\n- **Sensitive Data Handling**: API keys and other sensitive data are automatically sanitized during export (marked as `{{field}}` placeholders)\n- **Dynamic Configuration**: Presets can include input schemas for collecting required information during installation\n- **Version Control**: Each preset includes version metadata for tracking updates\n\n**Preset File Structure:**\n```\n~/.claude-code-router/presets/\n├── my-preset/\n│   └── manifest.json    # Contains configuration and metadata\n```\n\n### 7. Activate Command (Environment Variables Setup)\n\nThe `activate` command allows you to set up environment variables globally in your shell, enabling you to use the `claude` command directly or integrate Claude Code Router with applications built using the Agent SDK.\n\nTo activate the environment variables, run:\n\n```shell\neval \"$(ccr activate)\"\n```\n\nThis command outputs the necessary environment variables in shell-friendly format, which are then set in your current shell session. After activation, you can:\n\n- **Use `claude` command directly**: Run `claude` commands without needing to use `ccr code`. The `claude` command will automatically route requests through Claude Code Router.\n- **Integrate with Agent SDK applications**: Applications built with the Anthropic Agent SDK will automatically use the configured router and models.\n\nThe `activate` command sets the following environment variables:\n\n- `ANTHROPIC_AUTH_TOKEN`: API key from your configuration\n- `ANTHROPIC_BASE_URL`: The local router endpoint (default: `http://127.0.0.1:3456`)\n- `NO_PROXY`: Set to `127.0.0.1` to prevent proxy interference\n- `DISABLE_TELEMETRY`: Disables telemetry\n- `DISABLE_COST_WARNINGS`: Disables cost warnings\n- `API_TIMEOUT_MS`: API timeout from your configuration\n\n> **Note**: Make sure the Claude Code Router service is running (`ccr start`) before using the activated environment variables. The environment variables are only valid for the current shell session. To make them persistent, you can add `eval \"$(ccr activate)\"` to your shell configuration file (e.g., `~/.zshrc` or `~/.bashrc`).\n\n#### Providers\n\nThe `Providers` array is where you define the different model providers you want to use. Each provider object requires:\n\n- `name`: A unique name for the provider.\n- `api_base_url`: The full API endpoint for chat completions.\n- `api_key`: Your API key for the provider.\n- `models`: A list of model names available from this provider.\n- `transformer` (optional): Specifies transformers to process requests and responses.\n\n#### Transformers\n\nTransformers allow you to modify the request and response payloads to ensure compatibility with different provider APIs.\n\n- **Global Transformer**: Apply a transformer to all models from a provider. In this example, the `openrouter` transformer is applied to all models under the `openrouter` provider.\n  ```json\n  {\n    \"name\": \"openrouter\",\n    \"api_base_url\": \"https://openrouter.ai/api/v1/chat/completions\",\n    \"api_key\": \"sk-xxx\",\n    \"models\": [\n      \"google/gemini-2.5-pro-preview\",\n      \"anthropic/claude-sonnet-4\",\n      \"anthropic/claude-3.5-sonnet\"\n    ],\n    \"transformer\": { \"use\": [\"openrouter\"] }\n  }\n  ```\n- **Model-Specific Transformer**: Apply a transformer to a specific model. In this example, the `deepseek` transformer is applied to all models, and an additional `tooluse` transformer is applied only to the `deepseek-chat` model.\n\n  ```json\n  {\n    \"name\": \"deepseek\",\n    \"api_base_url\": \"https://api.deepseek.com/chat/completions\",\n    \"api_key\": \"sk-xxx\",\n    \"models\": [\"deepseek-chat\", \"deepseek-reasoner\"],\n    \"transformer\": {\n      \"use\": [\"deepseek\"],\n      \"deepseek-chat\": { \"use\": [\"tooluse\"] }\n    }\n  }\n  ```\n\n- **Passing Options to a Transformer**: Some transformers, like `maxtoken`, accept options. To pass options, use a nested array where the first element is the transformer name and the second is an options object.\n  ```json\n  {\n    \"name\": \"siliconflow\",\n    \"api_base_url\": \"https://api.siliconflow.cn/v1/chat/completions\",\n    \"api_key\": \"sk-xxx\",\n    \"models\": [\"moonshotai/Kimi-K2-Instruct\"],\n    \"transformer\": {\n      \"use\": [\n        [\n          \"maxtoken\",\n          {\n            \"max_tokens\": 16384\n          }\n        ]\n      ]\n    }\n  }\n  ```\n\n**Available Built-in Transformers:**\n\n- `Anthropic`:If you use only the `Anthropic` transformer, it will preserve the original request and response parameters(you can use it to connect directly to an Anthropic endpoint).\n- `deepseek`: Adapts requests/responses for DeepSeek API.\n- `gemini`: Adapts requests/responses for Gemini API.\n- `openrouter`: Adapts requests/responses for OpenRouter API. It can also accept a `provider` routing parameter to specify which underlying providers OpenRouter should use. For more details, refer to the [OpenRouter documentation](https://openrouter.ai/docs/features/provider-routing). See an example below:\n  ```json\n    \"transformer\": {\n      \"use\": [\"openrouter\"],\n      \"moonshotai/kimi-k2\": {\n        \"use\": [\n          [\n            \"openrouter\",\n            {\n              \"provider\": {\n                \"only\": [\"moonshotai/fp8\"]\n              }\n            }\n          ]\n        ]\n      }\n    }\n  ```\n- `groq`: Adapts requests/responses for groq API.\n- `maxtoken`: Sets a specific `max_tokens` value.\n- `tooluse`: Optimizes tool usage for certain models via `tool_choice`.\n- `gemini-cli` (experimental): Unofficial support for Gemini via Gemini CLI [gemini-cli.js](https://gist.github.com/musistudio/1c13a65f35916a7ab690649d3df8d1cd).\n- `reasoning`: Used to process the `reasoning_content` field.\n- `sampling`: Used to process sampling information fields such as `temperature`, `top_p`, `top_k`, and `repetition_penalty`.\n- `enhancetool`: Adds a layer of error tolerance to the tool call parameters returned by the LLM (this will cause the tool call information to no longer be streamed).\n- `cleancache`: Clears the `cache_control` field from requests.\n- `vertex-gemini`: Handles the Gemini API using Vertex authentication.\n- `chutes-glm` Unofficial support for GLM 4.5 model via Chutes [chutes-glm-transformer.js](https://gist.github.com/vitobotta/2be3f33722e05e8d4f9d2b0138b8c863).\n- `qwen-cli` (experimental): Unofficial support for qwen3-coder-plus model via Qwen CLI [qwen-cli.js](https://gist.github.com/musistudio/f5a67841ced39912fd99e42200d5ca8b).\n- `rovo-cli` (experimental): Unofficial support for gpt-5 via Atlassian Rovo Dev CLI [rovo-cli.js](https://gist.github.com/SaseQ/c2a20a38b11276537ec5332d1f7a5e53).\n\n**Custom Transformers:**\n\nYou can also create your own transformers and load them via the `transformers` field in `config.json`.\n\n```json\n{\n  \"transformers\": [\n    {\n      \"path\": \"/User/xxx/.claude-code-router/plugins/gemini-cli.js\",\n      \"options\": {\n        \"project\": \"xxx\"\n      }\n    }\n  ]\n}\n```\n\n#### Router\n\nThe `Router` object defines which model to use for different scenarios:\n\n- `default`: The default model for general tasks.\n- `background`: A model for background tasks. This can be a smaller, local model to save costs.\n- `think`: A model for reasoning-heavy tasks, like Plan Mode.\n- `longContext`: A model for handling long contexts (e.g., > 60K tokens).\n- `longContextThreshold` (optional): The token count threshold for triggering the long context model. Defaults to 60000 if not specified.\n- `webSearch`: Used for handling web search tasks and this requires the model itself to support the feature. If you're using openrouter, you need to add the `:online` suffix after the model name.\n- `image` (beta): Used for handling image-related tasks (supported by CCR’s built-in agent). If the model does not support tool calling, you need to set the `config.forceUseImageAgent` property to `true`.\n\n- You can also switch models dynamically in Claude Code with the `/model` command:\n`/model provider_name,model_name`\nExample: `/model openrouter,anthropic/claude-3.5-sonnet`\n\n#### Custom Router\n\nFor more advanced routing logic, you can specify a custom router script via the `CUSTOM_ROUTER_PATH` in your `config.json`. This allows you to implement complex routing rules beyond the default scenarios.\n\nIn your `config.json`:\n\n```json\n{\n  \"CUSTOM_ROUTER_PATH\": \"/User/xxx/.claude-code-router/custom-router.js\"\n}\n```\n\nThe custom router file must be a JavaScript module that exports an `async` function. This function receives the request object and the config object as arguments and should return the provider and model name as a string (e.g., `\"provider_name,model_name\"`), or `null` to fall back to the default router.\n\nHere is an example of a `custom-router.js` based on `custom-router.example.js`:\n\n```javascript\n// /User/xxx/.claude-code-router/custom-router.js\n\n/**\n * A custom router function to determine which model to use based on the request.\n *\n * @param {object} req - The request object from Claude Code, containing the request body.\n * @param {object} config - The application's config object.\n * @returns {Promise<string|null>} - A promise that resolves to the \"provider,model_name\" string, or null to use the default router.\n */\nmodule.exports = async function router(req, config) {\n  const userMessage = req.body.messages.find((m) => m.role === \"user\")?.content;\n\n  if (userMessage && userMessage.includes(\"explain this code\")) {\n    // Use a powerful model for code explanation\n    return \"openrouter,anthropic/claude-3.5-sonnet\";\n  }\n\n  // Fallback to the default router configuration\n  return null;\n};\n```\n\n##### Subagent Routing\n\nFor routing within subagents, you must specify a particular provider and model by including `<CCR-SUBAGENT-MODEL>provider,model</CCR-SUBAGENT-MODEL>` at the **beginning** of the subagent's prompt. This allows you to direct specific subagent tasks to designated models.\n\n**Example:**\n\n```\n<CCR-SUBAGENT-MODEL>openrouter,anthropic/claude-3.5-sonnet</CCR-SUBAGENT-MODEL>\nPlease help me analyze this code snippet for potential optimizations...\n```\n\n## Status Line (Beta)\nTo better monitor the status of claude-code-router at runtime, version v1.0.40 includes a built-in statusline tool, which you can enable in the UI.\n![statusline-config.png](/blog/images/statusline-config.png)\n\nThe effect is as follows:\n![statusline](/blog/images/statusline.png)\n\n## 🤖 GitHub Actions\n\nIntegrate Claude Code Router into your CI/CD pipeline. After setting up [Claude Code Actions](https://docs.anthropic.com/en/docs/claude-code/github-actions), modify your `.github/workflows/claude.yaml` to use the router:\n\n```yaml\nname: Claude Code\n\non:\n  issue_comment:\n    types: [created]\n  # ... other triggers\n\njobs:\n  claude:\n    if: |\n      (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||\n      # ... other conditions\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      pull-requests: read\n      issues: read\n      id-token: write\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 1\n\n      - name: Prepare Environment\n        run: |\n          curl -fsSL https://bun.sh/install | bash\n          mkdir -p $HOME/.claude-code-router\n          cat << 'EOF' > $HOME/.claude-code-router/config.json\n          {\n            \"log\": true,\n            \"NON_INTERACTIVE_MODE\": true,\n            \"OPENAI_API_KEY\": \"${{ secrets.OPENAI_API_KEY }}\",\n            \"OPENAI_BASE_URL\": \"https://api.deepseek.com\",\n            \"OPENAI_MODEL\": \"deepseek-chat\"\n          }\n          EOF\n        shell: bash\n\n      - name: Start Claude Code Router\n        run: |\n          nohup ~/.bun/bin/bunx @musistudio/claude-code-router@1.0.8 start &\n        shell: bash\n\n      - name: Run Claude Code\n        id: claude\n        uses: anthropics/claude-code-action@beta\n        env:\n          ANTHROPIC_BASE_URL: http://localhost:3456\n        with:\n          anthropic_api_key: \"any-string-is-ok\"\n```\n\n> **Note**: When running in GitHub Actions or other automation environments, make sure to set `\"NON_INTERACTIVE_MODE\": true` in your configuration to prevent the process from hanging due to stdin handling issues.\n\nThis setup allows for interesting automations, like running tasks during off-peak hours to reduce API costs.\n\n## 📝 Further Reading\n\n- [Project Motivation and How It Works](blog/en/project-motivation-and-how-it-works.md)\n- [Maybe We Can Do More with the Router](blog/en/maybe-we-can-do-more-with-the-route.md)\n- [GLM-4.6 Supports Reasoning and Interleaved Thinking](blog/en/glm-4.6-supports-reasoning.md)\n\n## ❤️ Support & Sponsoring\n\nIf you find this project helpful, please consider sponsoring its development. Your support is greatly appreciated!\n\n[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/F1F31GN2GM)\n\n[Paypal](https://paypal.me/musistudio1999)\n\n<table>\n  <tr>\n    <td><img src=\"/blog/images/alipay.jpg\" width=\"200\" alt=\"Alipay\" /></td>\n    <td><img src=\"/blog/images/wechat.jpg\" width=\"200\" alt=\"WeChat Pay\" /></td>\n  </tr>\n</table>\n\n### Our Sponsors\n\nA huge thank you to all our sponsors for their generous support!\n\n\n- [AIHubmix](https://aihubmix.com/)\n- [BurnCloud](https://ai.burncloud.com)\n- [302.AI](https://share.302.ai/ZGVF9w)\n- [Z智谱](https://www.bigmodel.cn/claude-code?ic=FPF9IVAGFJ)\n- @Simon Leischnig\n- [@duanshuaimin](https://github.com/duanshuaimin)\n- [@vrgitadmin](https://github.com/vrgitadmin)\n- @\\*o\n- [@ceilwoo](https://github.com/ceilwoo)\n- @\\*说\n- @\\*更\n- @K\\*g\n- @R\\*R\n- [@bobleer](https://github.com/bobleer)\n- @\\*苗\n- @\\*划\n- [@Clarence-pan](https://github.com/Clarence-pan)\n- [@carter003](https://github.com/carter003)\n- @S\\*r\n- @\\*晖\n- @\\*敏\n- @Z\\*z\n- @\\*然\n- [@cluic](https://github.com/cluic)\n- @\\*苗\n- [@PromptExpert](https://github.com/PromptExpert)\n- @\\*应\n- [@yusnake](https://github.com/yusnake)\n- @\\*飞\n- @董\\*\n- @\\*汀\n- @\\*涯\n- @\\*:-）\n- @\\*\\*磊\n- @\\*琢\n- @\\*成\n- @Z\\*o\n- @\\*琨\n- [@congzhangzh](https://github.com/congzhangzh)\n- @\\*\\_\n- @Z\\*m\n- @*鑫\n- @c\\*y\n- @\\*昕\n- [@witsice](https://github.com/witsice)\n- @b\\*g\n- @\\*亿\n- @\\*辉\n- @JACK\n- @\\*光\n- @W\\*l\n- [@kesku](https://github.com/kesku)\n- [@biguncle](https://github.com/biguncle)\n- @二吉吉\n- @a\\*g\n- @\\*林\n- @\\*咸\n- @\\*明\n- @S\\*y\n- @f\\*o\n- @\\*智\n- @F\\*t\n- @r\\*c\n- [@qierkang](http://github.com/qierkang)\n- @\\*军\n- [@snrise-z](http://github.com/snrise-z)\n- @\\*王\n- [@greatheart1000](http://github.com/greatheart1000)\n- @\\*王\n- @zcutlip\n- [@Peng-YM](http://github.com/Peng-YM)\n- @\\*更\n- @\\*.\n- @F\\*t\n- @\\*政\n- @\\*铭\n- @\\*叶\n- @七\\*o\n- @\\*青\n- @\\*\\*晨\n- @\\*远\n- @\\*霄\n- @\\*\\*吉\n- @\\*\\*飞\n- @\\*\\*驰\n- @x\\*g\n- @\\*\\*东\n- @\\*落\n- @哆\\*k\n- @\\*涛\n- [@苗大](https://github.com/WitMiao)\n- @\\*呢\n- @\\d*u\n- @crizcraig\n- s\\*s\n- \\*火\n- \\*勤\n- \\*\\*锟\n- \\*涛\n- \\*\\*明\n- \\*知\n- \\*语\n- \\*瓜\n\n\n(If your name is masked, please contact me via my homepage email to update it with your GitHub username.)\n"
  },
  {
    "path": "README_zh.md",
    "content": "![](blog/images/claude-code-router-img.png)\n\n[![](https://img.shields.io/badge/%F0%9F%87%AC%F0%9F%87%A7-English-000aff?style=flat)](README.md)\n[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?&logo=discord&logoColor=white)](https://discord.gg/rdftVMaUcS)\n[![](https://img.shields.io/github/license/musistudio/claude-code-router)](https://github.com/musistudio/claude-code-router/blob/main/LICENSE)\n\n<hr>\n\n![](blog/images/sponsors/glm-zh.jpg)\n> 本项目由 Z智谱 提供赞助, 他们通过 GLM CODING PLAN 对本项目提供技术支持。\n> GLM CODING PLAN 是专为AI编码打造的订阅套餐，每月最低仅需20元，即可在十余款主流AI编码工具如 Claude Code、Cline、Roo Code 中畅享智谱旗舰模型GLM-4.7（受限于算力，目前仅限Pro用户开放），为开发者提供顶尖的编码体验。\n> 智谱AI为本产品提供了特别优惠，使用以下链接购买可以享受九折优惠：https://www.bigmodel.cn/claude-code?ic=RRVJPB5SII\n\n> [从CLI工具风格看工具渐进式披露](/blog/zh/从CLI工具风格看工具渐进式披露.md)\n\n> 一款强大的工具，可将 Claude Code 请求路由到不同的模型，并自定义任何请求。\n\n![](blog/images/claude-code.png)\n\n\n## ✨ 功能\n\n-   **模型路由**: 根据您的需求将请求路由到不同的模型（例如，后台任务、思考、长上下文）。\n-   **多提供商支持**: 支持 OpenRouter、DeepSeek、Ollama、Gemini、Volcengine 和 SiliconFlow 等各种模型提供商。\n-   **请求/响应转换**: 使用转换器为不同的提供商自定义请求和响应。\n-   **动态模型切换**: 在 Claude Code 中使用 `/model` 命令动态切换模型。\n-   **GitHub Actions 集成**: 在您的 GitHub 工作流程中触发 Claude Code 任务。\n-   **插件系统**: 使用自定义转换器扩展功能。\n\n## 🚀 快速入门\n\n### 1. 安装\n\n首先，请确保您已安装 [Claude Code](https://docs.anthropic.com/en/docs/claude-code/quickstart)：\n\n```shell\nnpm install -g @anthropic-ai/claude-code\n```\n\n然后，安装 Claude Code Router：\n\n```shell\nnpm install -g @musistudio/claude-code-router\n```\n\n### 2. 配置\n\n创建并配置您的 `~/.claude-code-router/config.json` 文件。有关更多详细信息，您可以参考 `config.example.json`。\n\n`config.json` 文件有几个关键部分：\n- **`PROXY_URL`** (可选): 您可以为 API 请求设置代理，例如：`\"PROXY_URL\": \"http://127.0.0.1:7890\"`。\n- **`LOG`** (可选): 您可以通过将其设置为 `true` 来启用日志记录。当设置为 `false` 时，将不会创建日志文件。默认值为 `true`。\n- **`LOG_LEVEL`** (可选): 设置日志级别。可用选项包括：`\"fatal\"`、`\"error\"`、`\"warn\"`、`\"info\"`、`\"debug\"`、`\"trace\"`。默认值为 `\"debug\"`。\n- **日志系统**: Claude Code Router 使用两个独立的日志系统：\n  - **服务器级别日志**: HTTP 请求、API 调用和服务器事件使用 pino 记录在 `~/.claude-code-router/logs/` 目录中，文件名类似于 `ccr-*.log`\n  - **应用程序级别日志**: 路由决策和业务逻辑事件记录在 `~/.claude-code-router/claude-code-router.log` 文件中\n- **`APIKEY`** (可选): 您可以设置一个密钥来进行身份验证。设置后，客户端请求必须在 `Authorization` 请求头 (例如, `Bearer your-secret-key`) 或 `x-api-key` 请求头中提供此密钥。例如：`\"APIKEY\": \"your-secret-key\"`。\n- **`HOST`** (可选): 您可以设置服务的主机地址。如果未设置 `APIKEY`，出于安全考虑，主机地址将强制设置为 `127.0.0.1`，以防止未经授权的访问。例如：`\"HOST\": \"0.0.0.0\"`。\n- **`NON_INTERACTIVE_MODE`** (可选): 当设置为 `true` 时，启用与非交互式环境（如 GitHub Actions、Docker 容器或其他 CI/CD 系统）的兼容性。这会设置适当的环境变量（`CI=true`、`FORCE_COLOR=0` 等）并配置 stdin 处理，以防止进程在自动化环境中挂起。例如：`\"NON_INTERACTIVE_MODE\": true`。\n- **`Providers`**: 用于配置不同的模型提供商。\n- **`Router`**: 用于设置路由规则。`default` 指定默认模型，如果未配置其他路由，则该模型将用于所有请求。\n- **`API_TIMEOUT_MS`**: API 请求超时时间，单位为毫秒。\n\n这是一个综合示例：\n\n```json\n{\n  \"APIKEY\": \"your-secret-key\",\n  \"PROXY_URL\": \"http://127.0.0.1:7890\",\n  \"LOG\": true,\n  \"API_TIMEOUT_MS\": 600000,\n  \"NON_INTERACTIVE_MODE\": false,\n  \"Providers\": [\n    {\n      \"name\": \"openrouter\",\n      \"api_base_url\": \"https://openrouter.ai/api/v1/chat/completions\",\n      \"api_key\": \"sk-xxx\",\n      \"models\": [\n        \"google/gemini-2.5-pro-preview\",\n        \"anthropic/claude-sonnet-4\",\n        \"anthropic/claude-3.5-sonnet\",\n        \"anthropic/claude-3.7-sonnet:thinking\"\n      ],\n      \"transformer\": {\n        \"use\": [\"openrouter\"]\n      }\n    },\n    {\n      \"name\": \"deepseek\",\n      \"api_base_url\": \"https://api.deepseek.com/chat/completions\",\n      \"api_key\": \"sk-xxx\",\n      \"models\": [\"deepseek-chat\", \"deepseek-reasoner\"],\n      \"transformer\": {\n        \"use\": [\"deepseek\"],\n        \"deepseek-chat\": {\n          \"use\": [\"tooluse\"]\n        }\n      }\n    },\n    {\n      \"name\": \"ollama\",\n      \"api_base_url\": \"http://localhost:11434/v1/chat/completions\",\n      \"api_key\": \"ollama\",\n      \"models\": [\"qwen2.5-coder:latest\"]\n    },\n    {\n      \"name\": \"gemini\",\n      \"api_base_url\": \"https://generativelanguage.googleapis.com/v1beta/models/\",\n      \"api_key\": \"sk-xxx\",\n      \"models\": [\"gemini-2.5-flash\", \"gemini-2.5-pro\"],\n      \"transformer\": {\n        \"use\": [\"gemini\"]\n      }\n    },\n    {\n      \"name\": \"volcengine\",\n      \"api_base_url\": \"https://ark.cn-beijing.volces.com/api/v3/chat/completions\",\n      \"api_key\": \"sk-xxx\",\n      \"models\": [\"deepseek-v3-250324\", \"deepseek-r1-250528\"],\n      \"transformer\": {\n        \"use\": [\"deepseek\"]\n      }\n    },\n    {\n      \"name\": \"modelscope\",\n      \"api_base_url\": \"https://api-inference.modelscope.cn/v1/chat/completions\",\n      \"api_key\": \"\",\n      \"models\": [\"Qwen/Qwen3-Coder-480B-A35B-Instruct\", \"Qwen/Qwen3-235B-A22B-Thinking-2507\"],\n      \"transformer\": {\n        \"use\": [\n          [\n            \"maxtoken\",\n            {\n              \"max_tokens\": 65536\n            }\n          ],\n          \"enhancetool\"\n        ],\n        \"Qwen/Qwen3-235B-A22B-Thinking-2507\": {\n          \"use\": [\"reasoning\"]\n        }\n      }\n    },\n    {\n      \"name\": \"dashscope\",\n      \"api_base_url\": \"https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions\",\n      \"api_key\": \"\",\n      \"models\": [\"qwen3-coder-plus\"],\n      \"transformer\": {\n        \"use\": [\n          [\n            \"maxtoken\",\n            {\n              \"max_tokens\": 65536\n            }\n          ],\n          \"enhancetool\"\n        ]\n      }\n    },\n    {\n      \"name\": \"aihubmix\",\n      \"api_base_url\": \"https://aihubmix.com/v1/chat/completions\",\n      \"api_key\": \"sk-\",\n      \"models\": [\n        \"Z/glm-4.5\",\n        \"claude-opus-4-20250514\",\n        \"gemini-2.5-pro\"\n      ]\n    }\n  ],\n  \"Router\": {\n    \"default\": \"deepseek,deepseek-chat\",\n    \"background\": \"ollama,qwen2.5-coder:latest\",\n    \"think\": \"deepseek,deepseek-reasoner\",\n    \"longContext\": \"openrouter,google/gemini-2.5-pro-preview\",\n    \"longContextThreshold\": 60000,\n    \"webSearch\": \"gemini,gemini-2.5-flash\"\n  }\n}\n```\n\n\n### 3. 使用 Router 运行 Claude Code\n\n使用 router 启动 Claude Code：\n\n```shell\nccr code\n```\n\n> **注意**: 修改配置文件后，需要重启服务使配置生效：\n> ```shell\n> ccr restart\n> ```\n\n### 4. UI 模式\n\n为了获得更直观的体验，您可以使用 UI 模式来管理您的配置：\n\n```shell\nccr ui\n```\n\n这将打开一个基于 Web 的界面，您可以在其中轻松查看和编辑您的 `config.json` 文件。\n\n![UI](/blog/images/ui.png)\n\n### 5. CLI 模型管理\n\n对于偏好终端工作流的用户，可以使用交互式 CLI 模型选择器：\n\n```shell\nccr model\n```\n\n该命令提供交互式界面来：\n\n- 查看当前配置\n- 查看所有配置的模型（default、background、think、longContext、webSearch、image）\n- 切换模型：快速更改每个路由器类型使用的模型\n- 添加新模型：向现有提供商添加模型\n- 创建新提供商：设置完整的提供商配置，包括：\n   - 提供商名称和 API 端点\n   - API 密钥\n   - 可用模型\n   - Transformer 配置，支持：\n     - 多个转换器（openrouter、deepseek、gemini 等）\n     - Transformer 选项（例如，带自定义限制的 maxtoken）\n     - 特定于提供商的路由（例如，OpenRouter 提供商偏好）\n\nCLI 工具验证所有输入并提供有用的提示来引导您完成配置过程，使管理复杂的设置变得容易，无需手动编辑 JSON 文件。\n\n### 6. 预设管理\n\n预设允许您轻松保存、共享和重用配置。您可以将当前配置导出为预设，并从文件或 URL 安装预设。\n\n```shell\n# 将当前配置导出为预设\nccr preset export my-preset\n\n# 使用元数据导出\nccr preset export my-preset --description \"我的 OpenAI 配置\" --author \"您的名字\" --tags \"openai,生产环境\"\n\n# 从本地目录安装预设\nccr preset install /path/to/preset\n\n# 列出所有已安装的预设\nccr preset list\n\n# 显示预设信息\nccr preset info my-preset\n\n# 删除预设\nccr preset delete my-preset\n```\n\n**预设功能：**\n- **导出**：将当前配置保存为预设目录（包含 manifest.json）\n- **安装**：从本地目录安装预设\n- **敏感数据处理**：导出期间自动清理 API 密钥和其他敏感数据（标记为 `{{field}}` 占位符）\n- **动态配置**：预设可以包含输入架构，用于在安装期间收集所需信息\n- **版本控制**：每个预设包含版本元数据，用于跟踪更新\n\n**预设文件结构：**\n```\n~/.claude-code-router/presets/\n├── my-preset/\n│   └── manifest.json    # 包含配置和元数据\n```\n\n### 7. Activate 命令（环境变量设置）\n\n`activate` 命令允许您在 shell 中全局设置环境变量，使您能够直接使用 `claude` 命令或将 Claude Code Router 与使用 Agent SDK 构建的应用程序集成。\n\n要激活环境变量，请运行：\n\n```shell\neval \"$(ccr activate)\"\n```\n\n此命令会以 shell 友好的格式输出必要的环境变量，这些变量将在当前的 shell 会话中设置。激活后，您可以：\n\n- **直接使用 `claude` 命令**：无需使用 `ccr code` 即可运行 `claude` 命令。`claude` 命令将自动通过 Claude Code Router 路由请求。\n- **与 Agent SDK 应用程序集成**：使用 Anthropic Agent SDK 构建的应用程序将自动使用配置的路由器和模型。\n\n`activate` 命令设置以下环境变量：\n\n- `ANTHROPIC_AUTH_TOKEN`: 来自配置的 API 密钥\n- `ANTHROPIC_BASE_URL`: 本地路由器端点（默认：`http://127.0.0.1:3456`）\n- `NO_PROXY`: 设置为 `127.0.0.1` 以防止代理干扰\n- `DISABLE_TELEMETRY`: 禁用遥测\n- `DISABLE_COST_WARNINGS`: 禁用成本警告\n- `API_TIMEOUT_MS`: 来自配置的 API 超时时间\n\n> **注意**：在使用激活的环境变量之前，请确保 Claude Code Router 服务正在运行（`ccr start`）。环境变量仅在当前 shell 会话中有效。要使其持久化，您可以将 `eval \"$(ccr activate)\"` 添加到您的 shell 配置文件（例如 `~/.zshrc` 或 `~/.bashrc`）中。\n\n#### Providers\n\n`Providers` 数组是您定义要使用的不同模型提供商的地方。每个提供商对象都需要：\n\n-   `name`: 提供商的唯一名称。\n-   `api_base_url`: 聊天补全的完整 API 端点。\n-   `api_key`: 您提供商的 API 密钥。\n-   `models`: 此提供商可用的模型名称列表。\n-   `transformer` (可选): 指定用于处理请求和响应的转换器。\n\n#### Transformers\n\nTransformers 允许您修改请求和响应负载，以确保与不同提供商 API 的兼容性。\n\n-   **全局 Transformer**: 将转换器应用于提供商的所有模型。在此示例中，`openrouter` 转换器将应用于 `openrouter` 提供商下的所有模型。\n    ```json\n     {\n       \"name\": \"openrouter\",\n       \"api_base_url\": \"https://openrouter.ai/api/v1/chat/completions\",\n       \"api_key\": \"sk-xxx\",\n       \"models\": [\n         \"google/gemini-2.5-pro-preview\",\n         \"anthropic/claude-sonnet-4\",\n         \"anthropic/claude-3.5-sonnet\"\n       ],\n       \"transformer\": { \"use\": [\"openrouter\"] }\n     }\n    ```\n-   **特定于模型的 Transformer**: 将转换器应用于特定模型。在此示例中，`deepseek` 转换器应用于所有模型，而额外的 `tooluse` 转换器仅应用于 `deepseek-chat` 模型。\n    ```json\n     {\n       \"name\": \"deepseek\",\n       \"api_base_url\": \"https://api.deepseek.com/chat/completions\",\n       \"api_key\": \"sk-xxx\",\n       \"models\": [\"deepseek-chat\", \"deepseek-reasoner\"],\n       \"transformer\": {\n         \"use\": [\"deepseek\"],\n         \"deepseek-chat\": { \"use\": [\"tooluse\"] }\n       }\n     }\n    ```\n\n-   **向 Transformer 传递选项**: 某些转换器（如 `maxtoken`）接受选项。要传递选项，请使用嵌套数组，其中第一个元素是转换器名称，第二个元素是选项对象。\n    ```json\n    {\n      \"name\": \"siliconflow\",\n      \"api_base_url\": \"https://api.siliconflow.cn/v1/chat/completions\",\n      \"api_key\": \"sk-xxx\",\n      \"models\": [\"moonshotai/Kimi-K2-Instruct\"],\n      \"transformer\": {\n        \"use\": [\n          [\n            \"maxtoken\",\n            {\n              \"max_tokens\": 16384\n            }\n          ]\n        ]\n      }\n    }\n    ```\n\n**可用的内置 Transformer：**\n\n-   `Anthropic`: 如果你只使用这一个转换器，则会直接透传请求和响应(你可以用它来接入其他支持Anthropic端点的服务商)。\n-   `deepseek`: 适配 DeepSeek API 的请求/响应。\n-   `gemini`: 适配 Gemini API 的请求/响应。\n-   `openrouter`: 适配 OpenRouter API 的请求/响应。它还可以接受一个 `provider` 路由参数，以指定 OpenRouter 应使用哪些底层提供商。有关更多详细信息，请参阅 [OpenRouter 文档](https://openrouter.ai/docs/features/provider-routing)。请参阅下面的示例：\n    ```json\n      \"transformer\": {\n        \"use\": [\"openrouter\"],\n        \"moonshotai/kimi-k2\": {\n          \"use\": [\n            [\n              \"openrouter\",\n              {\n                \"provider\": {\n                  \"only\": [\"moonshotai/fp8\"]\n                }\n              }\n            ]\n          ]\n        }\n      }\n    ```\n-   `groq`: 适配 groq API 的请求/响应\n-   `maxtoken`: 设置特定的 `max_tokens` 值。\n-   `tooluse`: 优化某些模型的工具使用(通过`tool_choice`参数)。\n-   `gemini-cli` (实验性): 通过 Gemini CLI [gemini-cli.js](https://gist.github.com/musistudio/1c13a65f35916a7ab690649d3df8d1cd) 对 Gemini 的非官方支持。\n-   `reasoning`: 用于处理 `reasoning_content` 字段。\n-   `sampling`: 用于处理采样信息字段，如 `temperature`、`top_p`、`top_k` 和 `repetition_penalty`。\n-   `enhancetool`: 对 LLM 返回的工具调用参数增加一层容错处理（这会导致不再流式返回工具调用信息）。\n-   `cleancache`: 清除请求中的 `cache_control` 字段。\n-   `vertex-gemini`: 处理使用 vertex 鉴权的 gemini api。\n-   `qwen-cli` (实验性): 通过 Qwen CLI [qwen-cli.js](https://gist.github.com/musistudio/f5a67841ced39912fd99e42200d5ca8b) 对 qwen3-coder-plus 的非官方支持。\n-   `rovo-cli` (experimental): 通过 Atlassian Rovo Dev CLI [rovo-cli.js](https://gist.github.com/SaseQ/c2a20a38b11276537ec5332d1f7a5e53) 对 GPT-5 的非官方支持。\n\n**自定义 Transformer:**\n\n您还可以创建自己的转换器，并通过 `config.json` 中的 `transformers` 字段加载它们。\n\n```json\n{\n  \"transformers\": [\n      {\n        \"path\": \"/User/xxx/.claude-code-router/plugins/gemini-cli.js\",\n        \"options\": {\n          \"project\": \"xxx\"\n        }\n      }\n  ]\n}\n```\n\n#### Router\n\n`Router` 对象定义了在不同场景下使用哪个模型：\n\n-   `default`: 用于常规任务的默认模型。\n-   `background`: 用于后台任务的模型。这可以是一个较小的本地模型以节省成本。\n-   `think`: 用于推理密集型任务（如计划模式）的模型。\n-   `longContext`: 用于处理长上下文（例如，> 60K 令牌）的模型。\n-   `longContextThreshold` (可选): 触发长上下文模型的令牌数阈值。如果未指定，默认为 60000。\n-   `webSearch`: 用于处理网络搜索任务，需要模型本身支持。如果使用`openrouter`需要在模型后面加上`:online`后缀。\n-   `image`(测试版): 用于处理图片类任务（采用CCR内置的agent支持），如果该模型不支持工具调用，需要将`config.forceUseImageAgent`属性设置为`true`。\n\n您还可以使用 `/model` 命令在 Claude Code 中动态切换模型：\n`/model provider_name,model_name`\n示例: `/model openrouter,anthropic/claude-3.5-sonnet`\n\n#### 自定义路由器\n\n对于更高级的路由逻辑，您可以在 `config.json` 中通过 `CUSTOM_ROUTER_PATH` 字段指定一个自定义路由器脚本。这允许您实现超出默认场景的复杂路由规则。\n\n在您的 `config.json` 中配置:\n\n```json\n{\n  \"CUSTOM_ROUTER_PATH\": \"/User/xxx/.claude-code-router/custom-router.js\"\n}\n```\n\n自定义路由器文件必须是一个导出 `async` 函数的 JavaScript 模块。该函数接收请求对象和配置对象作为参数，并应返回提供商和模型名称的字符串（例如 `\"provider_name,model_name\"`），如果返回 `null` 则回退到默认路由。\n\n这是一个基于 `custom-router.example.js` 的 `custom-router.js` 示例：\n\n```javascript\n// /User/xxx/.claude-code-router/custom-router.js\n\n/**\n * 一个自定义路由函数，用于根据请求确定使用哪个模型。\n *\n * @param {object} req - 来自 Claude Code 的请求对象，包含请求体。\n * @param {object} config - 应用程序的配置对象。\n * @returns {Promise<string|null>} - 一个解析为 \"provider,model_name\" 字符串的 Promise，如果返回 null，则使用默认路由。\n */\nmodule.exports = async function router(req, config) {\n  const userMessage = req.body.messages.find(m => m.role === 'user')?.content;\n\n  if (userMessage && userMessage.includes('解释这段代码')) {\n    // 为代码解释任务使用更强大的模型\n    return 'openrouter,anthropic/claude-3.5-sonnet';\n  }\n\n  // 回退到默认的路由配置\n  return null;\n};\n```\n\n##### 子代理路由\n\n对于子代理内的路由，您必须在子代理提示词的**开头**包含 `<CCR-SUBAGENT-MODEL>provider,model</CCR-SUBAGENT-MODEL>` 来指定特定的提供商和模型。这样可以将特定的子代理任务定向到指定的模型。\n\n**示例：**\n\n```\n<CCR-SUBAGENT-MODEL>openrouter,anthropic/claude-3.5-sonnet</CCR-SUBAGENT-MODEL>\n请帮我分析这段代码是否存在潜在的优化空间...\n```\n\n## Status Line (Beta)\n为了在运行时更好的查看claude-code-router的状态，claude-code-router在v1.0.40内置了一个statusline工具，你可以在UI中启用它，\n![statusline-config.png](/blog/images/statusline-config.png)\n\n效果如下：\n![statusline](/blog/images/statusline.png)\n\n## 🤖 GitHub Actions\n\n将 Claude Code Router 集成到您的 CI/CD 管道中。在设置 [Claude Code Actions](https://docs.anthropic.com/en/docs/claude-code/github-actions) 后，修改您的 `.github/workflows/claude.yaml` 以使用路由器：\n\n```yaml\nname: Claude Code\n\non:\n  issue_comment:\n    types: [created]\n  # ... other triggers\n\njobs:\n  claude:\n    if: |\n      (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||\n      # ... other conditions\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      pull-requests: read\n      issues: read\n      id-token: write\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 1\n\n      - name: Prepare Environment\n        run: |\n          curl -fsSL https://bun.sh/install | bash\n          mkdir -p $HOME/.claude-code-router\n          cat << 'EOF' > $HOME/.claude-code-router/config.json\n          {\n            \"log\": true,\n            \"NON_INTERACTIVE_MODE\": true,\n            \"OPENAI_API_KEY\": \"${{ secrets.OPENAI_API_KEY }}\",\n            \"OPENAI_BASE_URL\": \"https://api.deepseek.com\",\n            \"OPENAI_MODEL\": \"deepseek-chat\"\n          }\n          EOF\n        shell: bash\n\n      - name: Start Claude Code Router\n        run: |\n          nohup ~/.bun/bin/bunx @musistudio/claude-code-router@1.0.8 start &\n        shell: bash\n\n      - name: Run Claude Code\n        id: claude\n        uses: anthropics/claude-code-action@beta\n        env:\n          ANTHROPIC_BASE_URL: http://localhost:3456\n        with:\n          anthropic_api_key: \"any-string-is-ok\"\n```\n\n这种设置可以实现有趣的自动化，例如在非高峰时段运行任务以降低 API 成本。\n\n## 📝 深入阅读\n\n-   [项目动机和工作原理](blog/zh/项目初衷及原理.md)\n-   [也许我们可以用路由器做更多事情](blog/zh/或许我们能在Router中做更多事情.md)\n\n## ❤️ 支持与赞助\n\n如果您觉得这个项目有帮助，请考虑赞助它的开发。非常感谢您的支持！\n\n[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/F1F31GN2GM)\n\n[Paypal](https://paypal.me/musistudio1999)\n\n<table>\n  <tr>\n    <td><img src=\"/blog/images/alipay.jpg\" width=\"200\" alt=\"Alipay\" /></td>\n    <td><img src=\"/blog/images/wechat.jpg\" width=\"200\" alt=\"WeChat Pay\" /></td>\n  </tr>\n</table>\n\n### 我们的赞助商\n\n非常感谢所有赞助商的慷慨支持！\n\n- [AIHubmix](https://aihubmix.com/)\n- [BurnCloud](https://ai.burncloud.com)\n- [302.AI](https://share.302.ai/ZGVF9w)\n- [Z智谱](https://www.bigmodel.cn/claude-code?ic=FPF9IVAGFJ)\n- @Simon Leischnig\n- [@duanshuaimin](https://github.com/duanshuaimin)\n- [@vrgitadmin](https://github.com/vrgitadmin)\n- @*o\n- [@ceilwoo](https://github.com/ceilwoo)\n- @*说\n- @*更\n- @K*g\n- @R*R\n- [@bobleer](https://github.com/bobleer)\n- @*苗\n- @*划\n- [@Clarence-pan](https://github.com/Clarence-pan)\n- [@carter003](https://github.com/carter003)\n- @S*r\n- @*晖\n- @*敏\n- @Z*z\n- @*然\n- [@cluic](https://github.com/cluic)\n- @*苗\n- [@PromptExpert](https://github.com/PromptExpert)\n- @*应\n- [@yusnake](https://github.com/yusnake)\n- @*飞\n- @董*\n- @*汀\n- @*涯\n- @*:-）\n- @**磊\n- @*琢\n- @*成\n- @Z*o\n- @\\*琨\n- [@congzhangzh](https://github.com/congzhangzh)\n- @*_\n- @Z\\*m\n- @*鑫\n- @c\\*y\n- @\\*昕\n- [@witsice](https://github.com/witsice)\n- @b\\*g\n- @\\*亿\n- @\\*辉\n- @JACK \n- @\\*光\n- @W\\*l\n- [@kesku](https://github.com/kesku)\n- [@biguncle](https://github.com/biguncle)\n- @二吉吉\n- @a\\*g\n- @\\*林\n- @\\*咸\n- @\\*明\n- @S\\*y\n- @f\\*o\n- @\\*智\n- @F\\*t\n- @r\\*c\n- [@qierkang](http://github.com/qierkang)\n- @\\*军\n- [@snrise-z](http://github.com/snrise-z)\n- @\\*王\n- [@greatheart1000](http://github.com/greatheart1000)\n- @\\*王\n- @zcutlip\n- [@Peng-YM](http://github.com/Peng-YM)\n- @\\*更\n- @\\*.\n- @F\\*t\n- @\\*政\n- @\\*铭\n- @\\*叶\n- @七\\*o\n- @\\*青\n- @\\*\\*晨\n- @\\*远\n- @\\*霄\n- @\\*\\*吉\n- @\\*\\*飞\n- @\\*\\*驰\n- @x\\*g\n- @\\*\\*东\n- @\\*落\n- @哆\\*k\n- @\\*涛\n- [@苗大](https://github.com/WitMiao)\n- @\\*呢\n- @\\d*u\n- @crizcraig\n- s\\*s\n- \\*火\n- \\*勤\n- \\*\\*锟\n- \\*涛\n- \\*\\*明\n- \\*知\n- \\*语\n- \\*瓜\n\n（如果您的名字被屏蔽，请通过我的主页电子邮件与我联系，以便使用您的 GitHub 用户名进行更新。）\n\n\n## 交流群\n<img src=\"/blog/images/wechat_group.jpg\" width=\"200\" alt=\"wechat_group\" />\n"
  },
  {
    "path": "blog/en/glm-4.6-supports-reasoning.md",
    "content": "# GLM-4.6 Supports Reasoning and Interleaved Thinking\n\n## Enabling Reasoning in Claude Code with GLM-4.6\n\nStarting from version 4.5, GLM has supported Claude Code. I’ve been following its progress closely, and many users have reported that reasoning could not be enabled within Claude Code. Recently, thanks to sponsorship from Zhipu, I decided to investigate this issue in depth. According to the [official documentation](https://docs.z.ai/api-reference/llm/chat-completion), the`/chat/completions` endpoint has reasoning enabled by default, but the model itself decides whether to think:\n\n```\nthinking.type enum<string> default:enabled\n\nWhether to enable the chain of thought(When enabled, GLM-4.6, GLM-4.5 and others will automatically determine whether to think, while GLM-4.5V will think compulsorily), default: enabled\n\nAvailable options: enabled, disabled \n```\n\nHowever, within Claude Code, its heavy system prompt interference disrupts GLM’s internal reasoning judgment, causing the model to rarely think.\nTherefore, we need to explicitly guide the model to believe reasoning is required. Since claude-code-router functions as a proxy, the only feasible approach is modifying prompts or parameters.\n\nInitially, I tried completely removing Claude Code’s system prompt — and indeed, the model started reasoning — but that broke Claude Code’s workflow.\nSo instead, I used prompt injection to clearly instruct the model to think step by step.\n\n\n```javascript\n// transformer.ts\nimport { UnifiedChatRequest } from \"../types/llm\";\nimport { Transformer } from \"../types/transformer\";\n\nexport class ForceReasoningTransformer implements Transformer {\n  name = \"forcereasoning\";\n\n  async transformRequestIn(\n    request: UnifiedChatRequest\n  ): Promise<UnifiedChatRequest> {\n    const systemMessage = request.messages.find(\n      (item) => item.role === \"system\"\n    );\n    if (Array.isArray(systemMessage?.content)) {\n      systemMessage.content.push({\n        type: \"text\",\n        text: \"You are an expert reasoning model.\\nAlways think step by step before answering. Even if the problem seems simple, always write down your reasoning process explicitly.\\nNever skip your chain of thought.\\nUse the following output format:\\n<reasoning_content>(Write your full detailed thinking here.)</reasoning_content>\\n\\nWrite your final conclusion here.\",\n      });\n    }\n    const lastMessage = request.messages[request.messages.length - 1];\n    if (lastMessage.role === \"user\" && Array.isArray(lastMessage.content)) {\n      lastMessage.content.push({\n        type: \"text\",\n        text: \"You are an expert reasoning model.\\nAlways think step by step before answering. Even if the problem seems simple, always write down your reasoning process explicitly.\\nNever skip your chain of thought.\\nUse the following output format:\\n<reasoning_content>(Write your full detailed thinking here.)</reasoning_content>\\n\\nWrite your final conclusion here.\",\n      });\n    }\n    if (lastMessage.role === \"tool\") {\n      request.messages.push({\n        role: \"user\",\n        content: [\n          {\n            type: \"text\",\n            text: \"You are an expert reasoning model.\\nAlways think step by step before answering. Even if the problem seems simple, always write down your reasoning process explicitly.\\nNever skip your chain of thought.\\nUse the following output format:\\n<reasoning_content>(Write your full detailed thinking here.)</reasoning_content>\\n\\nWrite your final conclusion here.\",\n          },\n        ],\n      });\n    }\n    return request;\n  }\n}\n```\n\nWhy use <reasoning_content> instead of the <think> tag? Two reasons:\n\n1. Using the <think> tag doesn’t effectively trigger reasoning — likely because the model was trained on data where <think> had special behavior.\n\n2. If we use <think>, the reasoning output is split into a separate field, which directly relates to the chain-of-thought feedback problem discussed below.\n\n## Chain-of-Thought Feedback\nRecently, Minimax released `Minimax-m2`, along with [an article](https://www.minimaxi.com/news/why-is-interleaved-thinking-important-for-m2) explaining interleaved thinking.\nWhile the idea isn’t entirely new, it’s a good opportunity to analyze it.\n\nWhy do we need to interleaved thinking?\nMinimax’s article mentions that the Chat Completion API does not support passing reasoning content between requests.\nWe know ChatGPT was the first to support reasoning, but OpenAI initially didn’t expose the chain of thought to users.\nTherefore, the Chat Completion API didn’t need to support it. Even the CoT field was first introduced by DeepSeek.\n\nDo we really need explicit CoT fields? What happens if we don’t have them? Will it affect reasoning?\nBy inspecting [sglang’s source code](https://github.com/sgl-project/sglang/blob/main/python/sglang/srt/parser/reasoning_parser.py), we can see that reasoning content is naturally emitted in messages with specific markers.\nIf we don’t split it out, the next-round conversation will naturally include it.\nThus, the only reason we need interleaved thinking is because we separated the reasoning content from the normal messages.\n\nWith fewer than 40 lines of code above, I implemented a simple exploration of enabling reasoning and chain-of-thought feedback for GLM-4.5/4.6.\n(It’s only simple because I haven’t implemented parsing logic yet — you could easily modify the transformer to split reasoning output on response and merge it back on request, improving Claude Code’s frontend display compatibility.)\n\nIf you have better ideas, feel free to reach out — I’d love to discuss further.\n"
  },
  {
    "path": "blog/en/maybe-we-can-do-more-with-the-route.md",
    "content": "# Maybe We Can Do More with the Router\n\nSince the release of `claude-code-router`, I’ve received a lot of user feedback, and quite a few issues are still open. Most of them are related to support for different providers and the lack of tool usage from the deepseek model.\n\nOriginally, I created this project for personal use, mainly to access claude code at a lower cost. So, multi-provider support wasn’t part of the initial design. But during troubleshooting, I discovered that even though most providers claim to be compatible with the OpenAI-style `/chat/completions` interface, there are many subtle differences. For example:\n\n1. When Gemini's tool parameter type is string, the `format` field only supports `date` and `date-time`, and there’s no tool call ID.\n\n2. OpenRouter requires `cache_control` for caching.\n\n3. The official DeepSeek API has a `max_output` of 8192, but Volcano Engine’s limit is even higher.\n\nAside from these, smaller providers often have quirks in their parameter handling. So I decided to create a new project, [musistudio/llms](https://github.com/musistudio/llms), to deal with these compatibility issues. It uses the OpenAI format as a base and introduces a generic Transformer interface for transforming both requests and responses.\n\nOnce a `Transformer` is implemented for each provider, it becomes possible to mix-and-match requests between them. For example, I implemented bidirectional conversion between Anthropic and OpenAI formats in `AnthropicTransformer`, which listens to the `/v1/messages` endpoint. Similarly, `GeminiTransformer` handles Gemini <-> OpenAI format conversions and listens to `/v1beta/models/:modelAndAction`.\n\nWhen both requests and responses are transformed into a common format, they can interoperate seamlessly:\n\n```\nAnthropicRequest -> AnthropicTransformer -> OpenAIRequest -> GeminiTransformer -> GeminiRequest -> GeminiServer\n```\n\n```\nGeminiResponse -> GeminiTransformer -> OpenAIResponse -> AnthropicTransformer -> AnthropicResponse\n```\n\nUsing a middleware layer to smooth out differences may introduce some performance overhead, but the main goal here is to enable `claude-code-router` to support multiple providers.\n\nAs for the issue of DeepSeek’s lackluster tool usage — I found that it stems from poor instruction adherence in long conversations. Initially, the model actively calls tools, but after several rounds, it starts responding with plain text instead. My first workaround was injecting a system prompt to remind the model to use tools proactively. But in long contexts, the model tends to forget this instruction.\n\nAfter reading the DeepSeek documentation, I noticed it supports the `tool_choice` parameter, which can be set to `\"required\"` to force the model to use at least one tool. I tested this by enabling the parameter, and it significantly improved the model’s tool usage. We can remove the setting when it's no longer necessary. With the help of the `Transformer` interface in [musistudio/llms](https://github.com/musistudio/llms), we can modify the request before it’s sent and adjust the response after it’s received.\n\nInspired by the Plan Mode in `claude code`, I implemented a similar Tool Mode for DeepSeek:\n\n```typescript\nexport class TooluseTransformer implements Transformer {\n  name = \"tooluse\";\n\n  transformRequestIn(request: UnifiedChatRequest): UnifiedChatRequest {\n    if (request.tools?.length) {\n      request.messages.push({\n        role: \"system\",\n        content: `<system-reminder>Tool mode is active. The user expects you to proactively execute the most suitable tool to help complete the task. \nBefore invoking a tool, you must carefully evaluate whether it matches the current task. If no available tool is appropriate for the task, you MUST call the \\`ExitTool\\` to exit tool mode — this is the only valid way to terminate tool mode.\nAlways prioritize completing the user's task effectively and efficiently by using tools whenever appropriate.</system-reminder>`,\n      });\n      request.tool_choice = \"required\";\n      request.tools.unshift({\n        type: \"function\",\n        function: {\n          name: \"ExitTool\",\n          description: `Use this tool when you are in tool mode and have completed the task. This is the only valid way to exit tool mode.\nIMPORTANT: Before using this tool, ensure that none of the available tools are applicable to the current task. You must evaluate all available options — only if no suitable tool can help you complete the task should you use ExitTool to terminate tool mode.\nExamples:\n1. Task: \"Use a tool to summarize this document\" — Do not use ExitTool if a summarization tool is available.\n2. Task: \"What’s the weather today?\" — If no tool is available to answer, use ExitTool after reasoning that none can fulfill the task.`,\n          parameters: {\n            type: \"object\",\n            properties: {\n              response: {\n                type: \"string\",\n                description:\n                  \"Your response will be forwarded to the user exactly as returned — the tool will not modify or post-process it in any way.\",\n              },\n            },\n            required: [\"response\"],\n          },\n        },\n      });\n    }\n    return request;\n  }\n\n  async transformResponseOut(response: Response): Promise<Response> {\n    if (response.headers.get(\"Content-Type\")?.includes(\"application/json\")) {\n      const jsonResponse = await response.json();\n      if (\n        jsonResponse?.choices[0]?.message.tool_calls?.length &&\n        jsonResponse?.choices[0]?.message.tool_calls[0]?.function?.name ===\n          \"ExitTool\"\n      ) {\n        const toolArguments = JSON.parse(toolCall.function.arguments || \"{}\");\n        jsonResponse.choices[0].message.content = toolArguments.response || \"\";\n        delete jsonResponse.choices[0].message.tool_calls;\n      }\n\n      // Handle non-streaming response if needed\n      return new Response(JSON.stringify(jsonResponse), {\n        status: response.status,\n        statusText: response.statusText,\n        headers: response.headers,\n      });\n    } else if (response.headers.get(\"Content-Type\")?.includes(\"stream\")) {\n      // ...\n    }\n    return response;\n  }\n}\n```\n\nThis transformer ensures the model calls at least one tool. If no tools are appropriate or the task is finished, it can exit using `ExitTool`. Since this relies on the `tool_choice` parameter, it only works with models that support it.\n\nIn practice, this approach noticeably improves tool usage for DeepSeek. The tradeoff is that sometimes the model may invoke irrelevant or unnecessary tools, which could increase latency and token usage.\n\nThis update is just a small experiment — adding an `“agent”` to the router. Maybe there are more interesting things we can explore from here."
  },
  {
    "path": "blog/en/progressive-disclosure-of-agent-tools-from-the-perspective-of-cli-tool-style.md",
    "content": "# Progressive Disclosure of Agent Tools from the Perspective of CLI Tool Style\n\nIt has been nearly two months since Anthropic released [Skills](https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills). In this release, Anthropic mentioned a term: Progressive Disclosure. What exactly is this? What problem does it solve?\n\nActually, in my Vibe Coding workflow, I rarely use MCP. The reason is that I find the implementation quality of MCP to be inconsistent. At its core, it’s about context injection (the essence of tools is also context injection), and I’m not sure if prompts written by others might affect my workflow, so I simply avoid using it altogether. The current implementation of MCP essentially wraps all functionalities as tools exposed to the Agent (one functionality wrapped as one tool, given a detailed description, telling the agent when to call it and what the parameter format is). This has led to the current explosion of prompts.\n\nIt wasn’t until Anthropic released Skills and I looked into it that I realized its essence is still prompt injection. If MCP provides a specification for injecting tools, then what Skills advocates is somewhat \"unconventional.\" Skills provides a Markdown document to describe the purpose and best practices of the skill, along with some attached scripts (different from MCP).\n![image](https://www.anthropic.com/_next/image?url=https%3A%2F%2Fwww-cdn.anthropic.com%2Fimages%2F4zrzovbb%2Fwebsite%2F6f22d8913dbc6228e7f11a41e0b3c124d817b6d2-1650x929.jpg&w=3840&q=75)\n\nSince these scripts run directly on the user’s local machine, there are significant security risks. If users cannot review the script code, it can easily lead to serious security issues such as data leakage or virus infections. Compared to MCP, which provides a standardized interface, Skills offer a series of script files. Different skills may have different types of script files—for example, some scripts are implemented in Node.js, while others use Python. To use these scripts, users also need to install the corresponding runtime and dependencies. This is why I describe it as \"unconventional.\"\n\n\nIs this really the best practice?\n\nRegarding Progressive Disclosure, here is how Anthropic describes it:\n> Progressive disclosure is the core design principle that makes Agent Skills flexible and scalable. Like a well-organized manual that starts with a table of contents, then specific chapters, and finally a detailed appendix, skills let Claude load information only as needed:\n> ![image](https://www.anthropic.com/_next/image?url=https%3A%2F%2Fwww-cdn.anthropic.com%2Fimages%2F4zrzovbb%2Fwebsite%2Fa3bca2763d7892982a59c28aa4df7993aaae55ae-2292x673.jpg&w=3840&q=75)\n> Agents with a filesystem and code execution tools don’t need to read the entirety of a skill into their context window when working on a particular task. This means that the amount of context that can be bundled into a skill is effectively unbounded.\n\nThe following diagram shows how the context window changes when a skill is triggered by a user’s message.\n![image](https://www.anthropic.com/_next/image?url=https%3A%2F%2Fwww-cdn.anthropic.com%2Fimages%2F4zrzovbb%2Fwebsite%2F441b9f6cc0d2337913c1f41b05357f16f51f702e-1650x929.jpg&w=3840&q=75)\n\nDo we really need to implement it this way?\n\nIn our daily use of CLI tools, most CLI tools come with a `--help` parameter for viewing the tool's usage and instructions. Isn't that essentially the tool's user manual? For example:\n```shell\n> npm --help\nnpm <command>\n\nUsage:\n\nnpm install        install all the dependencies in your project\nnpm install <foo>  add the <foo> dependency to your project\nnpm test           run this project's tests\nnpm run <foo>      run the script named <foo>\nnpm <command> -h   quick help on <command>\nnpm -l             display usage info for all commands\nnpm help <term>    search for help on <term>\nnpm help npm       more involved overview\n\nAll commands:\n\n    access, adduser, audit, bugs, cache, ci, completion,\n    config, dedupe, deprecate, diff, dist-tag, docs, doctor,\n    edit, exec, explain, explore, find-dupes, fund, get, help,\n    help-search, hook, init, install, install-ci-test,\n    install-test, link, ll, login, logout, ls, org, outdated,\n    owner, pack, ping, pkg, prefix, profile, prune, publish,\n    query, rebuild, repo, restart, root, run-script, sbom,\n    search, set, shrinkwrap, star, stars, start, stop, team,\n    test, token, uninstall, unpublish, unstar, update, version,\n    view, whoami\n\nSpecify configs in the ini-formatted file:\n    /Users/xxx/.npmrc\nor on the command line via: npm <command> --key=value\n\nMore configuration info: npm help config\nConfiguration fields: npm help 7 config\n```\n\nThis manual doesn't return every possible usage of every command either. It only lists which commands are available and what functions they can perform. For the specific usage of a command, you can still obtain it by using the `--help` parameter:\n```shell\n> npm install --help\nInstall a package\n\nUsage:\nnpm install [<package-spec> ...]\n\nOptions:\n[-S|--save|--no-save|--save-prod|--save-dev|--save-optional|--save-peer|--save-bundle]\n[-E|--save-exact] [-g|--global]\n[--install-strategy <hoisted|nested|shallow|linked>] [--legacy-bundling]\n[--global-style] [--omit <dev|optional|peer> [--omit <dev|optional|peer> ...]]\n[--include <prod|dev|optional|peer> [--include <prod|dev|optional|peer> ...]]\n[--strict-peer-deps] [--prefer-dedupe] [--no-package-lock] [--package-lock-only]\n[--foreground-scripts] [--ignore-scripts] [--no-audit] [--no-bin-links]\n[--no-fund] [--dry-run] [--cpu <cpu>] [--os <os>] [--libc <libc>]\n[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]\n[-ws|--workspaces] [--include-workspace-root] [--install-links]\n\naliases: add, i, in, ins, inst, insta, instal, isnt, isnta, isntal, isntall\n\nRun \"npm help install\" for more info\n```\nDoesn't this resemble the definition of progressive disclosure mentioned above?\n\nCan we implement an MCP in this style to achieve progressive disclosure of tools without needing skills? I used Codex to convert the official PDF Skill into an MCP, exposing only a single tool:\n```json\n{\n    \"name\": \"mcp__pdf__pdf\",\n    \"description\": \"Comprehensive PDF manipulation toolkit for extracting text and tables, creating new PDFs, merging/splitting documents, and handling forms. When Claude needs to fill in a PDF form or programmatically process, generate, or analyze PDF documents at scale.You need to pass in the --help parameter to obtain the usage of this tool first.\",\n    \"input_schema\": {\n        \"type\": \"object\",\n        \"properties\": {\n            \"params\": {\n                \"$ref\": \"#/$defs/PdfCommandInput\"\n            }\n        },\n        \"required\": [\n            \"params\"\n        ],\n        \"$defs\": {\n            \"PdfCommandInput\": {\n                \"additionalProperties\": false,\n                \"properties\": {\n                    \"argv\": {\n                        \"description\": \"Command tokens excluding the leading 'pdf' executable name\",\n                        \"items\": {\n                            \"type\": \"string\"\n                        },\n                        \"title\": \"Argv\",\n                        \"type\": \"array\"\n                    },\n                    \"command\": {\n                        \"anyOf\": [\n                            {\n                                \"type\": \"string\"\n                            },\n                            {\n                                \"type\": \"null\"\n                            }\n                        ],\n                        \"default\": null,\n                        \"description\": \"Optional command string; if set, it will be split like a shell\",\n                        \"title\": \"Command\"\n                    }\n                },\n                \"title\": \"PdfCommandInput\",\n                \"type\": \"object\"\n            }\n        },\n        \"title\": \"pdfArguments\"\n    }\n}\n```\n\nBelow is the change in the context window when using this MCP:\n```json\nUser: Extract all the text from test.pdf and write it into result.txt\nAgent: Tool(name=\"pdf\", args=[\"--help\"])\nTool: pdf - Comprehensive PDF manipulation toolkit for extracting text and tables, creating new PDFs, merging/splitting documents, and handling forms. When Claude needs to fill in a PDF form or programmatically process, generate, or analyze PDF documents at scale在使用之前你需要先传入--help参数以获取该工具的用法\\n\\nUsage:\\n  pdf [--help] <domain> <command> [options]\\n\\nDomains:\\n  forms      Fill, extract, and validate PDF form fields.\\n  documents  Create, merge, and split PDFs.\\n  text       Extract text content from PDFs.\\n  tables     Extract tables (heuristic, whitespace-based).\\n  images     Render PDF pages to PNG images.\\n\\nRun `pdf <domain> --help` for domain help.\\nRun `pdf <domain> <command> --help` for command help.\nAgent: Tool(name=\"pdf\", args=[\"text\", \"--help\"])\nTool: text - Extract text content from PDFs.\\n\\nUsage:\\n  pdf text <command> [options]\\n\\nCommands:\\n  extract  Extract text content from a PDF.\\n\\nRun `pdf text <command> --help` for details.\nAgent: Tool(name=\"pdf\", args=[\"text\", \"extract\", \"--help\"])\nTool: usage: pdf text extract --pdf PDF_PATH [--pages PAGES] [--out OUTPUT_PATH]\\n                        [--format {text,json}] [--overwrite]\\n\\nExtract text content from a PDF.\\n\\noptions:\\n  --pdf PDF_PATH        Input PDF\\n  --pages PAGES         Page selection like 1,3-5 or 'all' (default: all)\\n  --out OUTPUT_PATH     Write extracted text to a file\\n  --format {text,json}  Output format (default: text)\\n  --overwrite           Overwrite output file if it exists\\n\nAgent: Tool(name=\"pdf\", args=[\"text\", \"extract\", \"--pdf\", \"test.pdf\", \"--out\", \"result.txt\", \"--overwrite\"])\nTool: {\"result\": {\"stdout\": \"xx\",\"data\": {\"pdf_path\": \"test.pdf\",\"page_count\": 6,\"pages\": [xxx],\"format\": \"text\",\"output_path\": \"result.txt\"}}}\nAgent: xxx\n```\n\nFrom the perspective of context changes, progressive disclosure has been fully realized. This MCP code is open-source (entirely written by Codex, serving only to validate the idea without any review): https://github.com/musistudio/pdf-skill-mcp \n\nIf you have any thoughts or ideas, I’d also welcome the opportunity to discuss them with you. email: [m@musiiot.top](mailto://m@musiiot.top )\n"
  },
  {
    "path": "blog/en/project-motivation-and-how-it-works.md",
    "content": "# Project Motivation and Principles\n\nAs early as the day after Claude Code was released (2025-02-25), I began and completed a reverse engineering attempt of the project. At that time, using Claude Code required registering for an Anthropic account, applying for a waitlist, and waiting for approval. However, due to well-known reasons, Anthropic blocks users from mainland China, making it impossible for me to use the service through normal means. Based on known information, I discovered the following:\n\n1. Claude Code is installed via npm, so it's very likely developed with Node.js.\n2. Node.js offers various debugging methods: simple `console.log` usage, launching with `--inspect` to hook into Chrome DevTools, or even debugging obfuscated code using `d8`.\n\nMy goal was to use Claude Code without an Anthropic account. I didn’t need the full source code—just a way to intercept and reroute requests made by Claude Code to Anthropic’s models to my own custom endpoint. So I started the reverse engineering process:\n\n1. First, install Claude Code:\n```bash\nnpm install -g @anthropic-ai/claude-code\n```\n\n2. After installation, the project is located at `~/.nvm/versions/node/v20.10.0/lib/node_modules/@anthropic-ai/claude-code`(this may vary depending on your Node version manager and version).\n\n3. Open the package.json to analyze the entry point:\n```package.json\n{\n  \"name\": \"@anthropic-ai/claude-code\",\n  \"version\": \"1.0.24\",\n  \"main\": \"sdk.mjs\",\n  \"types\": \"sdk.d.ts\",\n  \"bin\": {\n    \"claude\": \"cli.js\"\n  },\n  \"engines\": {\n    \"node\": \">=18.0.0\"\n  },\n  \"type\": \"module\",\n  \"author\": \"Boris Cherny <boris@anthropic.com>\",\n  \"license\": \"SEE LICENSE IN README.md\",\n  \"description\": \"Use Claude, Anthropic's AI assistant, right from your terminal. Claude can understand your codebase, edit files, run terminal commands, and handle entire workflows for you.\",\n  \"homepage\": \"https://github.com/anthropics/claude-code\",\n  \"bugs\": {\n    \"url\": \"https://github.com/anthropics/claude-code/issues\"\n  },\n  \"scripts\": {\n    \"prepare\": \"node -e \\\"if (!process.env.AUTHORIZED) { console.error('ERROR: Direct publishing is not allowed.\\\\nPlease use the publish-external.sh script to publish this package.'); process.exit(1); }\\\"\",\n    \"preinstall\": \"node scripts/preinstall.js\"\n  },\n  \"dependencies\": {},\n  \"optionalDependencies\": {\n    \"@img/sharp-darwin-arm64\": \"^0.33.5\",\n    \"@img/sharp-darwin-x64\": \"^0.33.5\",\n    \"@img/sharp-linux-arm\": \"^0.33.5\",\n    \"@img/sharp-linux-arm64\": \"^0.33.5\",\n    \"@img/sharp-linux-x64\": \"^0.33.5\",\n    \"@img/sharp-win32-x64\": \"^0.33.5\"\n  }\n}\n```\n\nThe key entry is `\"claude\": \"cli.js\"`. Opening cli.js, you'll see the code is minified and obfuscated. But using WebStorm’s `Format File` feature, you can reformat it for better readability:\n![webstorm-formate-file](../images/webstorm-formate-file.png)\n\nNow you can begin understanding Claude Code’s internal logic and prompt structure by reading the code. To dig deeper, you can insert console.log statements or launch in debug mode with Chrome DevTools using:\n\n```bash\nNODE_OPTIONS=\"--inspect-brk=9229\" claude\n```\n\nThis command starts Claude Code in debug mode and opens port 9229. Visit chrome://inspect/ in Chrome and click inspect to begin debugging:\n![chrome-devtools](../images/chrome-inspect.png)\n![chrome-devtools](../images/chrome-devtools.png)\n\nBy searching for the keyword api.anthropic.com, you can easily locate where Claude Code makes its API calls. From the surrounding code, it's clear that baseURL can be overridden with the `ANTHROPIC_BASE_URL` environment variable, and `apiKey` and `authToken` can be configured similarly:\n![search](../images/search.png)\n\nSo far, we’ve discovered some key information:\n\n1. Environment variables can override Claude Code's `baseURL` and `apiKey`.\n\n2. Claude Code adheres to the Anthropic API specification.\n\nTherefore, we need:\n1. A service to convert OpenAI API–compatible requests into Anthropic API format.\n\n2. Set the environment variables before launching Claude Code to redirect requests to this service.\n\nThus, `claude-code-router` was born. This project uses `Express.js` to implement the `/v1/messages` endpoint. It leverages middlewares to transform request/response formats and supports request rewriting (useful for prompt tuning per model).\n\nBack in February, the full DeepSeek model series had poor support for Function Calling, so I initially used `qwen-max`. It worked well—but without KV cache support, it consumed a large number of tokens and couldn’t provide the native `Claude Code` experience.\n\nSo I experimented with a Router-based mode using a lightweight model to dispatch tasks. The architecture included four roles: `router`, `tool`, `think`, and `coder`. Each request passed through a free lightweight model that would decide whether the task involved reasoning, coding, or tool usage. Reasoning and coding tasks looped until a tool was invoked to apply changes. However, the lightweight model lacked the capability to route tasks accurately, and architectural issues prevented it from effectively driving Claude Code.\n\nEverything changed at the end of May when the official Claude Code was launched, and `DeepSeek-R1` model (released 2025-05-28) added Function Call support. I redesigned the system. With the help of AI pair programming, I fixed earlier request/response transformation issues—especially the handling of models that return JSON instead of Function Call outputs.\n\nThis time, I used the `DeepSeek-V3`  model. It performed better than expected: supporting most tool calls, handling task decomposition and stepwise planning, and—most importantly—costing less than one-tenth the price of Claude 3.5 Sonnet.\n\nThe official Claude Code organizes agents differently from the beta version, so I restructured my Router mode to include four roles: the default model, `background`, `think`, and `longContext`.\n\n- The default model handles general tasks and acts as a fallback.\n\n- The `background` model manages lightweight background tasks. According to Anthropic, Claude Haiku 3.5 is often used here, so I routed this to a local `ollama` service.\n\n- The `think` model is responsible for reasoning and planning mode tasks. I use `DeepSeek-R1` here, though it doesn’t support cost control, so `Think` and `UltraThink` behave identically.\n\n- The `longContext` model handles long-context scenarios. The router uses `tiktoken` to calculate token lengths in real time, and if the context exceeds 32K, it switches to this model to compensate for DeepSeek's long-context limitations.\n\nThis describes the evolution and reasoning behind the project. By cleverly overriding environment variables, we can forward and modify requests without altering Claude Code’s source—allowing us to benefit from official updates while using our own models and custom prompts.\n\nThis project offers a practical approach to running Claude Code under Anthropic’s regional restrictions, balancing `cost`, `performance`, and `customizability`. That said, the official `Max Plan` still offers the best experience if available."
  },
  {
    "path": "blog/zh/GLM-4.6支持思考及思维链回传.md",
    "content": "# GLM-4.6支持思考及思维链回传\n\n## GLM-4.6在cluade code中启用思考\nGLM从4.5开始就对claude code进行了支持，我之前也一直在关注，很多用户反映在claude code中无法启用思考，刚好最近收到了来自智谱的赞助，就着手进行研究。\n\n首先根据[官方文档](https://docs.bigmodel.cn/api-reference/%E6%A8%A1%E5%9E%8B-api/%E5%AF%B9%E8%AF%9D%E8%A1%A5%E5%85%A8)，我们发现`/chat/completions`端点是默认启用思考的，但是是由模型判断是否需要进行思考\n\n```\nthinking object\n仅 GLM-4.5 及以上模型支持此参数配置. 控制大模型是否开启思维链。\n\nthinking.type enum<string> default:enabled\n是否开启思维链(当开启后 GLM-4.6 GLM-4.5 为模型自动判断是否思考，GLM-4.5V 为强制思考), 默认: enabled.\n\nAvailable options: enabled, disabled \n```\n\n在claude code本身大量的提示词干扰下，会严重阻碍GLM模型本身的判断机制，导致模型很少进行思考。所以我们需要对模型进行引导，让模型认为需要进行思考。但是`claude-code-router`作为proxy，能做的只能是修改提示词/参数。\n\n在最开始，我尝试直接删除claude code的系统提示词，模型确实进行了思考，但是这样就无法驱动claude code。所以我们需要进行提示词注入，明确告知模型需要进行思考。\n\n```javascript\n// transformer.ts\nimport { UnifiedChatRequest } from \"../types/llm\";\nimport { Transformer } from \"../types/transformer\";\n\nexport class ForceReasoningTransformer implements Transformer {\n  name = \"forcereasoning\";\n\n  async transformRequestIn(\n    request: UnifiedChatRequest\n  ): Promise<UnifiedChatRequest> {\n    const systemMessage = request.messages.find(\n      (item) => item.role === \"system\"\n    );\n    if (Array.isArray(systemMessage?.content)) {\n      systemMessage.content.push({\n        type: \"text\",\n        text: \"You are an expert reasoning model. \\nAlways think step by step before answering. Even if the problem seems simple, always write down your reasoning process explicitly. \\nNever skip your chain of thought. \\nUse the following output format:\\n<reasoning_content>(Write your full detailed thinking here.)</reasoning_content>\\n\\nWrite your final conclusion here.\",\n      });\n    }\n    const lastMessage = request.messages[request.messages.length - 1];\n    if (lastMessage.role === \"user\" && Array.isArray(lastMessage.content)) {\n      lastMessage.content.push({\n        type: \"text\",\n        text: \"You are an expert reasoning model. \\nAlways think step by step before answering. Even if the problem seems simple, always write down your reasoning process explicitly. \\nNever skip your chain of thought. \\nUse the following output format:\\n<reasoning_content>(Write your full detailed thinking here.)</reasoning_content>\\n\\nWrite your final conclusion here.\",\n      });\n    }\n    if (lastMessage.role === \"tool\") {\n      request.messages.push({\n        role: \"user\",\n        content: [\n          {\n            type: \"text\",\n            text: \"You are an expert reasoning model. \\nAlways think step by step before answering. Even if the problem seems simple, always write down your reasoning process explicitly. \\nNever skip your chain of thought. \\nUse the following output format:\\n<reasoning_content>(Write your full detailed thinking here.)</reasoning_content>\\n\\nWrite your final conclusion here.\",\n          },\n        ],\n      });\n    }\n    return request;\n  }\n}\n```\n\n至于为什么让模型将思考内容放入reasoning_content标签而不是think标签有两个原因：\n1. 直接使用think标签不能很好的激活思考，猜测是训练模型时以think标签作为数据集进行训练。\n2. 如果使用think标签，模型的推理内容会被拆分到单独的字段，这就涉及到我们接下来要说的思维链回传问题。\n\n\n## 思维链回传\n\n近期Minimax发布了Minimax-m2，与此同时，他们还发布了一篇[文章](https://www.minimaxi.com/news/why-is-interleaved-thinking-important-for-m2)介绍思维链回传。但是太阳底下无新鲜事，刚好借此来剖析一下。       \n1. 我们首先来看一下为什么需要回传思维链？     \nMinimax在文章中说的是Chat Completion API不支持在后续请求中传递推理内容。我们知道ChatGPT是最先支持推理的，但是OpenAI最初没有开放思维链给用户，所以对于Chat Completion API来讲并不需要支持思维链相关的东西。就连CoT的字段也是DeepSeek率先在Chat Completion API中加入的。\n\n2. 我们真的需要这些字段吗？\n如果没有这些字段会怎么样？会影响到模型的思考吗？可以查看一下[sglang的源码](https://github.com/sgl-project/sglang/blob/main/python/sglang/srt/parser/reasoning_parser.py)发现思维链的信息原本就会在消息中按照特定的标记进行输出，假如我们不对其进行拆分，正常情况下在下轮对话中会自然包含这些信息。所以需要思维链回传的原因就是我们对模型的思维链内容进行拆分。\n\n我用上面不到40行的代码完成了对GLM-4.5/6支持思考以及思维链回传的简单探索(单纯是因为没时间做拆分，完全可以在transformer中响应时先做拆分，请求时再进行合并，这样对cc前端的展示适配会更好)，如果你有什么更好的想法也欢迎与我联系。\n\n\n\n\n"
  },
  {
    "path": "blog/zh/从CLI工具风格看工具渐进式披露.md",
    "content": "# 从CLI工具风格看Agent工具渐进式披露\n\n距离Anthropic发布[Skills](https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills)也过去将近两个月的时间了，其中Anthropic提到了一个术语渐进式披露(Progressive Disclosure)，这到底是什么东西？解决了什么问题？\n\n其实在我的Vibe Coding流程中，我很少使用MCP。因为我觉得MCP实现质量层次不齐，本质是上下文注入(工具的本质也是上下文注入)，我不确定别人写的提示词会不会影响到我的工作流，干脆直接不用。现在的MCP实现基本上就是把所有的功能全都包装成工具暴露给Agent(一个功能包装成一个工具，给定详细的描述，告诉agent在什么时候进行调用，参数格式是什么)，这就导致了现在的提示词爆炸。    \n\n直到Anthropic发布了Skills，研究了一下发现本质仍然是提示词注入。如果说MCP是提供了一套注入工具的规范，那么Skills所提倡的则是“离经叛道”。Skills给了一个Markdown文档用于描述该skill的用途和最佳用法，附带提供了一些脚本(与MCP不同)。\n![image](https://www.anthropic.com/_next/image?url=https%3A%2F%2Fwww-cdn.anthropic.com%2Fimages%2F4zrzovbb%2Fwebsite%2F6f22d8913dbc6228e7f11a41e0b3c124d817b6d2-1650x929.jpg&w=3840&q=75)\n由于这些脚本直接在用户本地运行，存在极大的安全风险。如果用户不能对脚本代码进行review，很容易造成数据泄露、感染病毒等严重安全性问题。相比于MCP提供一个标准化的接口，Skill提供一系列的脚本文件，不同的skill可能拥有不同类型的脚本文件，比如有些脚本使用node.js实现，有些脚本使用Python实现，要使用这些脚本还需要用户安装对应的运行时和脚本所需要的依赖。这也是我说“离经叛道”的原因所在。\n\n这真的是最佳实践吗？\n\n关于渐进式披露，Anthropic是这样描述的：\n> 渐进式披露是使代理技能灵活且可扩展的核心设计原则。就像一本组织良好的手册，从目录开始，然后是具体章节，最后是详细的附录一样，技能允许 Claude 仅在需要时加载信息：\n> ![image](https://www.ant# 从CLI工具风格看Agent工具渐进式披露\n\n距离Anthropic发布[Skills](https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills)也过去将近两个月的时间了，其中Anthropic提到了一个术语渐进式披露(Progressive Disclosure)，这到底是什么东西？解决了什么问题？\n\n其实在我的Vibe Coding流程中，我很少使用MCP。因为我觉得MCP实现质量层次不齐，本质是上下文注入(工具的本质也是上下文注入)，我不确定别人写的提示词会不会影响到我的工作流，干脆直接不用。现在的MCP实现基本上就是把所有的功能全都包装成工具暴露给Agent(一个功能包装成一个工具，给定详细的描述，告诉agent在什么时候进行调用，参数格式是什么)，这就导致了现在的提示词爆炸。\n\n直到Anthropic发布了Skills，研究了一下发现本质仍然是提示词注入。如果说MCP是提供了一套注入工具的规范，那么Skills所提倡的则是“离经叛道”。Skills给了一个Markdown文档用于描述该skill的用途和最佳用法，附带提供了一些脚本(与MCP不同)。\n![image](https://www.anthropic.com/_next/image?url=https%3A%2F%2Fwww-cdn.anthropic.com%2Fimages%2F4zrzovbb%2Fwebsite%2F6f22d8913dbc6228e7f11a41e0b3c124d817b6d2-1650x929.jpg&w=3840&q=75)\n由于这些脚本直接在用户本地运行，存在极大的安全风险。如果用户不能对脚本代码进行review，很容易造成数据泄露、感染病毒等严重安全性问题。相比于MCP提供一个标准化的接口，Skill提供一系列的脚本文件，不同的skill可能拥有不同类型的脚本文件，比如有些脚本使用node.js实现，有些脚本使用Python实现，要使用这些脚本还需要用户安装对应的运行时和脚本所需要的依赖。这也是我说“离经叛道”的原因所在。\n\n这真的是最佳实践吗？\n\n关于渐进式披露，Anthropic是这样描述的：\n> 渐进式披露是使代理技能灵活且可扩展的核心设计原则。就像一本组织良好的手册，从目录开始，然后是具体章节，最后是详细的附录一样，技能允许 Claude 仅在需要时加载信息：\n> ![image](https://www.anthropic.com/_next/image?url=https%3A%2F%2Fwww-cdn.anthropic.com%2Fimages%2F4zrzovbb%2Fwebsite%2Fa3bca2763d7892982a59c28aa4df7993aaae55ae-2292x673.jpg&w=3840&q=75)\n> 拥有文件系统和代码执行工具的智能体在执行特定任务时，无需将技能的全部内容读取到上下文窗口中。这意味着技能中可以包含的上下文信息量实际上是无限的。\n\n下图是使用Skill的上下文窗口变化\n![image](https://www.anthropic.com/_next/image?url=https%3A%2F%2Fwww-cdn.anthropic.com%2Fimages%2F4zrzovbb%2Fwebsite%2F441b9f6cc0d2337913c1f41b05357f16f51f702e-1650x929.jpg&w=3840&q=75)\n\n我们真的需要这样去实现吗？\n\n在我们平时使用CLI工具时，一般的CLI工具都会带有一个`--help`参数，用于查看该工具的用法和说明，这不就是该工具的使用手册吗？比如：\n```shell\n> npm --help\nnpm <command>\n\nUsage:\n\nnpm install        install all the dependencies in your project\nnpm install <foo>  add the <foo> dependency to your project\nnpm test           run this project's tests\nnpm run <foo>      run the script named <foo>\nnpm <command> -h   quick help on <command>\nnpm -l             display usage info for all commands\nnpm help <term>    search for help on <term>\nnpm help npm       more involved overview\n\nAll commands:\n\n    access, adduser, audit, bugs, cache, ci, completion,\n    config, dedupe, deprecate, diff, dist-tag, docs, doctor,\n    edit, exec, explain, explore, find-dupes, fund, get, help,\n    help-search, hook, init, install, install-ci-test,\n    install-test, link, ll, login, logout, ls, org, outdated,\n    owner, pack, ping, pkg, prefix, profile, prune, publish,\n    query, rebuild, repo, restart, root, run-script, sbom,\n    search, set, shrinkwrap, star, stars, start, stop, team,\n    test, token, uninstall, unpublish, unstar, update, version,\n    view, whoami\n\nSpecify configs in the ini-formatted file:\n    /Users/xxx/.npmrc\nor on the command line via: npm <command> --key=value\n\nMore configuration info: npm help config\nConfiguration fields: npm help 7 config\n```\n\n这份手册也不会返回所有的命令所有的用法，它只会返回它有哪些命令可以实现什么功能，对于命令的具体用法你仍然可以通过`--help`参数获得：\n```shell\n> npm install --help\nInstall a package\n\nUsage:\nnpm install [<package-spec> ...]\n\nOptions:\n[-S|--save|--no-save|--save-prod|--save-dev|--save-optional|--save-peer|--save-bundle]\n[-E|--save-exact] [-g|--global]\n[--install-strategy <hoisted|nested|shallow|linked>] [--legacy-bundling]\n[--global-style] [--omit <dev|optional|peer> [--omit <dev|optional|peer> ...]]\n[--include <prod|dev|optional|peer> [--include <prod|dev|optional|peer> ...]]\n[--strict-peer-deps] [--prefer-dedupe] [--no-package-lock] [--package-lock-only]\n[--foreground-scripts] [--ignore-scripts] [--no-audit] [--no-bin-links]\n[--no-fund] [--dry-run] [--cpu <cpu>] [--os <os>] [--libc <libc>]\n[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]\n[-ws|--workspaces] [--include-workspace-root] [--install-links]\n\naliases: add, i, in, ins, inst, insta, instal, isnt, isnta, isntal, isntall\n\nRun \"npm help install\" for more info\n```\n这是不是很像上面的渐进式披露的定义？\n\n我们是不是可以按照这种风格去实现一个MCP来实现无需skill的工具渐进式披露？我使用Codex将官方的PDF Skill转换成了一个MCP，只暴露一个工具：\n```json\n{\n    \"name\": \"mcp__pdf__pdf\",\n    \"description\": \"Comprehensive PDF manipulation toolkit for extracting text and tables, creating new PDFs, merging/splitting documents, and handling forms. When Claude needs to fill in a PDF form or programmatically process, generate, or analyze PDF documents at scale.You need to pass in the --help parameter to obtain the usage of this tool first.\",\n    \"input_schema\": {\n        \"type\": \"object\",\n        \"properties\": {\n            \"params\": {\n                \"$ref\": \"#/$defs/PdfCommandInput\"\n            }\n        },\n        \"required\": [\n            \"params\"\n        ],\n        \"$defs\": {\n            \"PdfCommandInput\": {\n                \"additionalProperties\": false,\n                \"properties\": {\n                    \"argv\": {\n                        \"description\": \"Command tokens excluding the leading 'pdf' executable name\",\n                        \"items\": {\n                            \"type\": \"string\"\n                        },\n                        \"title\": \"Argv\",\n                        \"type\": \"array\"\n                    },\n                    \"command\": {\n                        \"anyOf\": [\n                            {\n                                \"type\": \"string\"\n                            },\n                            {\n                                \"type\": \"null\"\n                            }\n                        ],\n                        \"default\": null,\n                        \"description\": \"Optional command string; if set, it will be split like a shell\",\n                        \"title\": \"Command\"\n                    }\n                },\n                \"title\": \"PdfCommandInput\",\n                \"type\": \"object\"\n            }\n        },\n        \"title\": \"pdfArguments\"\n    }\n}\n```\n下面是使用该MCP的上下文窗口变化\n```json\nUser: Extract all the text from test.pdf and write it into result.txt\nAgent: Tool(name=\"pdf\", args=[\"--help\"])\nTool: pdf - Comprehensive PDF manipulation toolkit for extracting text and tables, creating new PDFs, merging/splitting documents, and handling forms. When Claude needs to fill in a PDF form or programmatically process, generate, or analyze PDF documents at scale在使用之前你需要先传入--help参数以获取该工具的用法\\n\\nUsage:\\n  pdf [--help] <domain> <command> [options]\\n\\nDomains:\\n  forms      Fill, extract, and validate PDF form fields.\\n  documents  Create, merge, and split PDFs.\\n  text       Extract text content from PDFs.\\n  tables     Extract tables (heuristic, whitespace-based).\\n  images     Render PDF pages to PNG images.\\n\\nRun `pdf <domain> --help` for domain help.\\nRun `pdf <domain> <command> --help` for command help.\nAgent: Tool(name=\"pdf\", args=[\"text\", \"--help\"])\nTool: text - Extract text content from PDFs.\\n\\nUsage:\\n  pdf text <command> [options]\\n\\nCommands:\\n  extract  Extract text content from a PDF.\\n\\nRun `pdf text <command> --help` for details.\nAgent: Tool(name=\"pdf\", args=[\"text\", \"extract\", \"--help\"])\nTool: usage: pdf text extract --pdf PDF_PATH [--pages PAGES] [--out OUTPUT_PATH]\\n                        [--format {text,json}] [--overwrite]\\n\\nExtract text content from a PDF.\\n\\noptions:\\n  --pdf PDF_PATH        Input PDF\\n  --pages PAGES         Page selection like 1,3-5 or 'all' (default: all)\\n  --out OUTPUT_PATH     Write extracted text to a file\\n  --format {text,json}  Output format (default: text)\\n  --overwrite           Overwrite output file if it exists\\n\nAgent: Tool(name=\"pdf\", args=[\"text\", \"extract\", \"--pdf\", \"test.pdf\", \"--out\", \"result.txt\", \"--overwrite\"])\nTool: {\"result\": {\"stdout\": \"xx\",\"data\": {\"pdf_path\": \"test.pdf\",\"page_count\": 6,\"pages\": [xxx],\"format\": \"text\",\"output_path\": \"result.txt\"}}}\nAgent: xxx\n```\n从上下文变化情况来看，完全实现了渐进式披露，该MCP代码开源(代码完全由codex编写，只验证想法，未做任何审查): https://github.com/musistudio/pdf-skill-mcp\n\n如果你有什么想法也欢迎与我进行交流hropic.com/_next/image?url=https%3A%2F%2Fwww-cdn.anthropic.com%2Fimages%2F4zrzovbb%2Fwebsite%2Fa3bca2763d7892982a59c28aa4df7993aaae55ae-2292x673.jpg&w=3840&q=75)\n> 拥有文件系统和代码执行工具的智能体在执行特定任务时，无需将技能的全部内容读取到上下文窗口中。这意味着技能中可以包含的上下文信息量实际上是无限的。    \n\n下图是使用Skill的上下文窗口变化\n![image](https://www.anthropic.com/_next/image?url=https%3A%2F%2Fwww-cdn.anthropic.com%2Fimages%2F4zrzovbb%2Fwebsite%2F441b9f6cc0d2337913c1f41b05357f16f51f702e-1650x929.jpg&w=3840&q=75)\n\n我们真的需要这样去实现吗？\n\n在我们平时使用CLI工具时，一般的CLI工具都会带有一个`--help`参数，用于查看该工具的用法和说明，这不就是该工具的使用手册吗？比如：\n```shell\n> npm --help\nnpm <command>\n\nUsage:\n\nnpm install        install all the dependencies in your project\nnpm install <foo>  add the <foo> dependency to your project\nnpm test           run this project's tests\nnpm run <foo>      run the script named <foo>\nnpm <command> -h   quick help on <command>\nnpm -l             display usage info for all commands\nnpm help <term>    search for help on <term>\nnpm help npm       more involved overview\n\nAll commands:\n\n    access, adduser, audit, bugs, cache, ci, completion,\n    config, dedupe, deprecate, diff, dist-tag, docs, doctor,\n    edit, exec, explain, explore, find-dupes, fund, get, help,\n    help-search, hook, init, install, install-ci-test,\n    install-test, link, ll, login, logout, ls, org, outdated,\n    owner, pack, ping, pkg, prefix, profile, prune, publish,\n    query, rebuild, repo, restart, root, run-script, sbom,\n    search, set, shrinkwrap, star, stars, start, stop, team,\n    test, token, uninstall, unpublish, unstar, update, version,\n    view, whoami\n\nSpecify configs in the ini-formatted file:\n    /Users/xxx/.npmrc\nor on the command line via: npm <command> --key=value\n\nMore configuration info: npm help config\nConfiguration fields: npm help 7 config\n```\n\n这份手册也不会返回所有的命令所有的用法，它只会返回它有哪些命令可以实现什么功能，对于命令的具体用法你仍然可以通过`--help`参数获得：\n```shell\n> npm install --help\nInstall a package\n\nUsage:\nnpm install [<package-spec> ...]\n\nOptions:\n[-S|--save|--no-save|--save-prod|--save-dev|--save-optional|--save-peer|--save-bundle]\n[-E|--save-exact] [-g|--global]\n[--install-strategy <hoisted|nested|shallow|linked>] [--legacy-bundling]\n[--global-style] [--omit <dev|optional|peer> [--omit <dev|optional|peer> ...]]\n[--include <prod|dev|optional|peer> [--include <prod|dev|optional|peer> ...]]\n[--strict-peer-deps] [--prefer-dedupe] [--no-package-lock] [--package-lock-only]\n[--foreground-scripts] [--ignore-scripts] [--no-audit] [--no-bin-links]\n[--no-fund] [--dry-run] [--cpu <cpu>] [--os <os>] [--libc <libc>]\n[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]\n[-ws|--workspaces] [--include-workspace-root] [--install-links]\n\naliases: add, i, in, ins, inst, insta, instal, isnt, isnta, isntal, isntall\n\nRun \"npm help install\" for more info\n```\n这是不是很像上面的渐进式披露的定义？\n\n我们是不是可以按照这种风格去实现一个MCP来实现无需skill的工具渐进式披露？我使用Codex将官方的PDF Skill转换成了一个MCP，只暴露一个工具：\n```json\n{\n    \"name\": \"mcp__pdf__pdf\",\n    \"description\": \"Comprehensive PDF manipulation toolkit for extracting text and tables, creating new PDFs, merging/splitting documents, and handling forms. When Claude needs to fill in a PDF form or programmatically process, generate, or analyze PDF documents at scale.You need to pass in the --help parameter to obtain the usage of this tool first.\",\n    \"input_schema\": {\n        \"type\": \"object\",\n        \"properties\": {\n            \"params\": {\n                \"$ref\": \"#/$defs/PdfCommandInput\"\n            }\n        },\n        \"required\": [\n            \"params\"\n        ],\n        \"$defs\": {\n            \"PdfCommandInput\": {\n                \"additionalProperties\": false,\n                \"properties\": {\n                    \"argv\": {\n                        \"description\": \"Command tokens excluding the leading 'pdf' executable name\",\n                        \"items\": {\n                            \"type\": \"string\"\n                        },\n                        \"title\": \"Argv\",\n                        \"type\": \"array\"\n                    },\n                    \"command\": {\n                        \"anyOf\": [\n                            {\n                                \"type\": \"string\"\n                            },\n                            {\n                                \"type\": \"null\"\n                            }\n                        ],\n                        \"default\": null,\n                        \"description\": \"Optional command string; if set, it will be split like a shell\",\n                        \"title\": \"Command\"\n                    }\n                },\n                \"title\": \"PdfCommandInput\",\n                \"type\": \"object\"\n            }\n        },\n        \"title\": \"pdfArguments\"\n    }\n}\n```\n下面是使用该MCP的上下文窗口变化\n```json\nUser: Extract all the text from test.pdf and write it into result.txt\nAgent: Tool(name=\"pdf\", args=[\"--help\"])\nTool: pdf - Comprehensive PDF manipulation toolkit for extracting text and tables, creating new PDFs, merging/splitting documents, and handling forms. When Claude needs to fill in a PDF form or programmatically process, generate, or analyze PDF documents at scale在使用之前你需要先传入--help参数以获取该工具的用法\\n\\nUsage:\\n  pdf [--help] <domain> <command> [options]\\n\\nDomains:\\n  forms      Fill, extract, and validate PDF form fields.\\n  documents  Create, merge, and split PDFs.\\n  text       Extract text content from PDFs.\\n  tables     Extract tables (heuristic, whitespace-based).\\n  images     Render PDF pages to PNG images.\\n\\nRun `pdf <domain> --help` for domain help.\\nRun `pdf <domain> <command> --help` for command help.\nAgent: Tool(name=\"pdf\", args=[\"text\", \"--help\"])\nTool: text - Extract text content from PDFs.\\n\\nUsage:\\n  pdf text <command> [options]\\n\\nCommands:\\n  extract  Extract text content from a PDF.\\n\\nRun `pdf text <command> --help` for details.\nAgent: Tool(name=\"pdf\", args=[\"text\", \"extract\", \"--help\"])\nTool: usage: pdf text extract --pdf PDF_PATH [--pages PAGES] [--out OUTPUT_PATH]\\n                        [--format {text,json}] [--overwrite]\\n\\nExtract text content from a PDF.\\n\\noptions:\\n  --pdf PDF_PATH        Input PDF\\n  --pages PAGES         Page selection like 1,3-5 or 'all' (default: all)\\n  --out OUTPUT_PATH     Write extracted text to a file\\n  --format {text,json}  Output format (default: text)\\n  --overwrite           Overwrite output file if it exists\\n\nAgent: Tool(name=\"pdf\", args=[\"text\", \"extract\", \"--pdf\", \"test.pdf\", \"--out\", \"result.txt\", \"--overwrite\"])\nTool: {\"result\": {\"stdout\": \"xx\",\"data\": {\"pdf_path\": \"test.pdf\",\"page_count\": 6,\"pages\": [xxx],\"format\": \"text\",\"output_path\": \"result.txt\"}}}\nAgent: xxx\n```\n从上下文变化情况来看，完全实现了渐进式披露，该MCP代码开源(代码完全由codex编写，只验证想法，未做任何审查): https://github.com/musistudio/pdf-skill-mcp \n\n如果你有什么想法也欢迎与我进行交流 email: [m@musiiot.top](mailto://m@musiiot.top )\n"
  },
  {
    "path": "blog/zh/或许我们能在Router中做更多事情.md",
    "content": "# 或许我们能在 Router 中做更多事情\n\n自从`claude-code-router`发布以来，我收到了很多用户的反馈，至今还有不少的 issues 未处理。其中大多都是关于不同的供应商的支持和`deepseek`模型调用工具不积极的问题。\n之前开发这个项目主要是为了我自己能以较低成本使用上`claude code`，所以一开始的设计并没有考虑到多供应商的情况。在实际的排查问题中，我发现尽管市面上所有的供应商几乎都宣称兼容`OpenAI`格式调用，即通过`/chat/compeletions`接口调用，但是其中的细节差异非常多。例如:\n\n1. Gemini 的工具参数类型是 string 时，`format`参数只支持`date`和`date-time`，并且没有工具调用 ID。\n\n2. OpenRouter 需要使用`cache_control`进行缓存。\n\n3. DeepSeek 官方 API 的 `max_output` 为 8192，而火山引擎的会更大。\n\n除了这些问题之外，还有一些其他的小的供应商，他们或多或少参数都有点问题。于是，我打算开发一个新的项目[musistudio/llms](https://github.com/musistudio/llms)来处理这种不同服务商的兼容问题。该项目使用 OpenAI 格式为基础的通用格式，提供了一个`Transformer`接口，该接口用于处理转换请求和响应。当我们给不同的服务商都实现了`Transformer`后，我们可以实现不同服务商的混合调用。比如我在`AnthropicTransformer`中实现了`Anthropic`<->`OpenAI`格式的互相转换，并监听了`/v1/messages`端点，在`GeminiTransformer`中实现了`Gemini`<->`OpenAI`格式的互相转换，并监听了`/v1beta/models/:modelAndAction`端点，当他们的请求和响应都被转换成一个通用格式的时候，就可以实现他们的互相调用。\n\n```\nAnthropicRequest -> AnthropicTransformer -> OpenAIRequest -> GeminiTransformer -> GeminiRequest -> GeminiServer\n```\n\n```\nGeminiReseponse -> GeminiTransformer -> OpenAIResponse -> AnthropicTransformer -> AnthropicResponse\n```\n\n虽然使用中间层抹平差异可能会带来一些性能问题，但是该项目最初的目的是为了让`claude-code-router`支持不同的供应商。\n\n至于`deepseek`模型调用工具不积极的问题，我发现这是由于`deepseek`在长上下文中的指令遵循不佳导致的。现象就是刚开始模型会主动调用工具，但是在经过几轮对话后模型只会返回文本。一开始的解决方案是通过注入一个系统提示词告知模型需要积极去使用工具以解决用户的问题，但是后面测试发现在长上下文中模型会遗忘该指令。\n查看`deepseek`文档后发现模型支持`tool_choice`参数，可以强制让模型最少调用 1 个工具，我尝试将该值设置为`required`，发现模型调用工具的积极性大大增加，现在我们只需要在合适的时候取消这个参数即可。借助[musistudio/llms](https://github.com/musistudio/llms)的`Transformer`可以让我们在发送请求前和收到响应后做点什么，所以我参考`claude code`的`Plan Mode`，实现了一个使用与`deepseek`的`Tool Mode`\n\n```typescript\nexport class TooluseTransformer implements Transformer {\n  name = \"tooluse\";\n\n  transformRequestIn(request: UnifiedChatRequest): UnifiedChatRequest {\n    if (request.tools?.length) {\n      request.messages.push({\n        role: \"system\",\n        content: `<system-reminder>Tool mode is active. The user expects you to proactively execute the most suitable tool to help complete the task. \nBefore invoking a tool, you must carefully evaluate whether it matches the current task. If no available tool is appropriate for the task, you MUST call the \\`ExitTool\\` to exit tool mode — this is the only valid way to terminate tool mode.\nAlways prioritize completing the user's task effectively and efficiently by using tools whenever appropriate.</system-reminder>`,\n      });\n      request.tool_choice = \"required\";\n      request.tools.unshift({\n        type: \"function\",\n        function: {\n          name: \"ExitTool\",\n          description: `Use this tool when you are in tool mode and have completed the task. This is the only valid way to exit tool mode.\nIMPORTANT: Before using this tool, ensure that none of the available tools are applicable to the current task. You must evaluate all available options — only if no suitable tool can help you complete the task should you use ExitTool to terminate tool mode.\nExamples:\n1. Task: \"Use a tool to summarize this document\" — Do not use ExitTool if a summarization tool is available.\n2. Task: \"What’s the weather today?\" — If no tool is available to answer, use ExitTool after reasoning that none can fulfill the task.`,\n          parameters: {\n            type: \"object\",\n            properties: {\n              response: {\n                type: \"string\",\n                description:\n                  \"Your response will be forwarded to the user exactly as returned — the tool will not modify or post-process it in any way.\",\n              },\n            },\n            required: [\"response\"],\n          },\n        },\n      });\n    }\n    return request;\n  }\n\n  async transformResponseOut(response: Response): Promise<Response> {\n    if (response.headers.get(\"Content-Type\")?.includes(\"application/json\")) {\n      const jsonResponse = await response.json();\n      if (\n        jsonResponse?.choices[0]?.message.tool_calls?.length &&\n        jsonResponse?.choices[0]?.message.tool_calls[0]?.function?.name ===\n          \"ExitTool\"\n      ) {\n        const toolArguments = JSON.parse(toolCall.function.arguments || \"{}\");\n        jsonResponse.choices[0].message.content = toolArguments.response || \"\";\n        delete jsonResponse.choices[0].message.tool_calls;\n      }\n\n      // Handle non-streaming response if needed\n      return new Response(JSON.stringify(jsonResponse), {\n        status: response.status,\n        statusText: response.statusText,\n        headers: response.headers,\n      });\n    } else if (response.headers.get(\"Content-Type\")?.includes(\"stream\")) {\n      // ...\n    }\n    return response;\n  }\n}\n```\n\n该工具将始终让模型至少调用一个工具，如果没有合适的工具或者任务已完成可以调用`ExitTool`来退出工具模式，因为是依靠`tool_choice`参数实现的，所以仅适用于支持该参数的模型。经过测试，该工具能显著增加`deepseek`的工具调用次数，弊端是可能会有跟任务无关或者没有必要的工具调用导致增加任务执行事件和消耗的 `token` 数。\n\n这次更新仅仅是在 Router 中实现一个`agent`的一次小探索，或许还能做更多其他有趣的事也说不定...\n"
  },
  {
    "path": "blog/zh/项目初衷及原理.md",
    "content": "# 项目初衷及原理\n\n早在 Claude Code 发布的第二天(2025-02-25)，我就尝试并完成了对该项目的逆向。当时要使用 Claude Code 你需要注册一个 Anthropic 账号，然后申请 waitlist，等待通过后才能使用。但是因为众所周知的原因，Anthropic 屏蔽了中国区的用户，所以通过正常手段我无法使用，通过已知的信息，我发现：\n\n1. Claude Code 使用 npm 进行安装，所以很大可能其使用 Node.js 进行开发。\n2. Node.js 调试手段众多，可以简单使用`console.log`获取想要的信息，也可以使用`--inspect`将其接入`Chrome Devtools`，甚至你可以使用`d8`去调试某些加密混淆的代码。\n\n由于我的目标是让我在没有 Anthropic 账号的情况下使用`Claude Code`，我并不需要获得完整的源代码，只需要将`Claude Code`请求 Anthropic 模型时将其转发到我自定义的接口即可。接下来我就开启了我的逆向过程：\n\n1. 首先安装`Claude Code`\n\n```bash\nnpm install -g @anthropic-ai/claude-code\n```\n\n2. 安装后该项目被放在了`~/.nvm/versions/node/v20.10.0/lib/node_modules/@anthropic-ai/claude-code`中，因为我使用了`nvm`作为我的 node 版本控制器，当前使用`node-v20.10.0`，所以该路径会因人而异。\n3. 找到项目路径之后可通过 package.json 分析包入口,内容如下：\n\n```package.json\n{\n  \"name\": \"@anthropic-ai/claude-code\",\n  \"version\": \"1.0.24\",\n  \"main\": \"sdk.mjs\",\n  \"types\": \"sdk.d.ts\",\n  \"bin\": {\n    \"claude\": \"cli.js\"\n  },\n  \"engines\": {\n    \"node\": \">=18.0.0\"\n  },\n  \"type\": \"module\",\n  \"author\": \"Boris Cherny <boris@anthropic.com>\",\n  \"license\": \"SEE LICENSE IN README.md\",\n  \"description\": \"Use Claude, Anthropic's AI assistant, right from your terminal. Claude can understand your codebase, edit files, run terminal commands, and handle entire workflows for you.\",\n  \"homepage\": \"https://github.com/anthropics/claude-code\",\n  \"bugs\": {\n    \"url\": \"https://github.com/anthropics/claude-code/issues\"\n  },\n  \"scripts\": {\n    \"prepare\": \"node -e \\\"if (!process.env.AUTHORIZED) { console.error('ERROR: Direct publishing is not allowed.\\\\nPlease use the publish-external.sh script to publish this package.'); process.exit(1); }\\\"\",\n    \"preinstall\": \"node scripts/preinstall.js\"\n  },\n  \"dependencies\": {},\n  \"optionalDependencies\": {\n    \"@img/sharp-darwin-arm64\": \"^0.33.5\",\n    \"@img/sharp-darwin-x64\": \"^0.33.5\",\n    \"@img/sharp-linux-arm\": \"^0.33.5\",\n    \"@img/sharp-linux-arm64\": \"^0.33.5\",\n    \"@img/sharp-linux-x64\": \"^0.33.5\",\n    \"@img/sharp-win32-x64\": \"^0.33.5\"\n  }\n}\n```\n\n其中`\"claude\": \"cli.js\"`就是我们要找的入口，打开 cli.js，发现代码被压缩混淆过了。没关系，借助`webstorm`的`Formate File`功能可以重新格式化，让代码变得稍微好看一点。就像这样：\n![webstorm-formate-file](../images/webstorm-formate-file.png)\n\n现在，你可以通过阅读部分代码来了解`Claude Code`的内容工具原理与提示词。你也可以在关键地方使用`console.log`来获得更多信息，当然，也可以使用`Chrome Devtools`来进行断点调试，使用以下命令启动`Claude Code`:\n\n```bash\nNODE_OPTIONS=\"--inspect-brk=9229\" claude\n```\n\n该命令会以调试模式启动`Claude Code`，并将调试的端口设置为`9229`。这时候通过 Chrome 访问`chrome://inspect/`即可看到当前的`Claude Code`进程，点击`inspect`即可进行调试。\n![chrome-devtools](../images/chrome-inspect.png)\n![chrome-devtools](../images/chrome-devtools.png)\n\n通过搜索关键字符`api.anthropic.com`很容易能找到`Claude Code`用来发请求的地方，根据上下文的查看，很容易发现这里的`baseURL`可以通过环境变量`ANTHROPIC_BASE_URL`进行覆盖，`apiKey`和`authToken`也同理。\n![search](../images/search.png)\n\n到目前为止，我们获得关键信息：\n\n1. 可以使用环境变量覆盖`Claude Code`的`BaseURL`和`apiKey`的配置\n\n2. `Claude Code`使用[Anthropic API](https://docs.anthropic.com/en/api/overview)的规范\n\n所以我们需要：\n\n1. 实现一个服务用来将`OpenAI API`的规范转换成`Anthropic API`格式。\n\n2. 启动`Claude Code`之前写入环境变量将`baseURL`指向到该服务。\n\n于是，`claude-code-router`就诞生了，该项目使用`Express.js`作为 HTTP 服务，实现`/v1/messages`端点，使用`middlewares`处理请求/响应的格式转换以及请求重写功能(可以用来重写 Claude Code 的提示词以针对单个模型进行调优)。\n在 2 月份由于`DeepSeek`全系列模型对`Function Call`的支持不佳导致无法直接使用`DeepSeek`模型，所以在当时我选择了`qwen-max`模型，一切表现的都很好，但是`qwen-max`不支持`KV Cache`，意味着我要消耗大量的 token，但是却无法获取`Claude Code`原生的体验。\n所以我又尝试了`Router`模式，即使用一个小模型对任务进行分发，一共分为四个模型:`router`、`tool`、`think`和`coder`，所有的请求先经过一个免费的小模型，由小模型去判断应该是进行思考还是编码还是调用工具，再进行任务的分发，如果是思考和编码任务将会进行循环调用，直到最终使用工具写入或修改文件。但是实践下来发现免费的小模型不足以很好的完成任务的分发，再加上整个 Agnet 的设计存在缺陷，导致并不能很好的驱动`Claude Code`。\n直到 5 月底，`Claude Code`被正式推出，这时`DeepSeek`全系列模型(R1 于 05-28)均支持`Function Call`，我开始重新设计该项目。在与 AI 的结对编程中我修复了之前的请求和响应转换问题，在某些场景下模型输出 JSON 响应而不是`Function Call`。这次直接使用`DeepSeek-v3`模型，它工作的比我想象中要好：能完成绝大多数工具调用，还支持用步骤规划解决任务，最关键的是`DeepSeek`的价格不到`claude Sonnet 3.5`的十分之一。正式发布的`Claude Code`对 Agent 的组织也不同于测试版，于是在分析了`Claude Code`的请求调用之后，我重新组织了`Router`模式：现在它还是四个模型：默认模型、`background`、`think`和`longContext`。\n\n- 默认模型作为最终的兜底和日常处理\n\n- `background`是用来处理一些后台任务，据 Anthropic 官方说主要用`Claude Haiku 3.5`模型去处理一些小任务，如俳句生成和对话摘要，于是我将其路由到了本地的`ollama`服务。\n\n- `think`模型用于让`Claude Code`进行思考或者在`Plan Mode`下使用，这里我使用的是`DeepSeek-R1`，由于其不支持推理成本控制，所以`Think`和`UltraThink`是一样的逻辑。\n\n- `longContext`是用于处理长下上文的场景，该项目会对每次请求使用tiktoken实时计算上下文长度，如果上下文大于32K则使用该模型，旨在弥补`DeepSeek`在长上下文处理不佳的情况。\n\n以上就是该项目的发展历程以及我的一些思考，通过巧妙的使用环境变量覆盖的手段在不修改`Claude Code`源码的情况下完成请求的转发和修改，这就使得在可以得到 Anthropic 更新的同时使用自己的模型，自定义自己的提示词。该项目只是在 Anthropic 封禁中国区用户的情况下使用`Claude Code`并且达到成本和性能平衡的一种手段。如果可以的话，还是官方的Max Plan体验最好。\n"
  },
  {
    "path": "custom-router.example.js",
    "content": "module.exports = async function router(req, config) {\n  return \"deepseek,deepseek-chat\";\n};\n"
  },
  {
    "path": "docs/.gitignore",
    "content": "# Docusaurus build output\nbuild/\ndist/\n\n# Docusaurus generated files\n.docusaurus/\n\n# Node modules\nnode_modules/\n\n# Environment variables\n.env\n.env.local\n.env.*.local\n\n# IDE\n.vscode/\n.idea/\n\n# OS\n.DS_Store\nThumbs.db\n\n# Logs\n*.log\n\n# Misc\n*.swp\n*.swo\n"
  },
  {
    "path": "docs/README.md",
    "content": "# Claude Code Router Documentation\n\nThis directory contains the documentation website built with [Docusaurus](https://docusaurus.io/).\n\n## Development\n\n### Install Dependencies\n\n```bash\ncd docs\npnpm install\n```\n\n### Start Development Server\n\n```bash\n# From docs directory\npnpm start\n\n# Or from root directory\npnpm dev:docs\n```\n\nOpen [http://localhost:3000](http://localhost:3000) to view the documentation.\n\n## Build\n\n```bash\n# From docs directory\npnpm build\n\n# Or from root directory\npnpm build:docs\n```\n\nThe built files will be in the `build/` directory.\n\n## Serve Built Files\n\n```bash\n# From docs directory\npnpm serve\n\n# Or from root directory\npnpm serve:docs\n```\n\n## Structure\n\n```\ndocs/\n├── docs/              # Markdown documentation files\n│   ├── intro.md       # Introduction page\n│   ├── installation.md\n│   ├── config/        # Configuration docs\n│   ├── advanced/      # Advanced topics\n│   └── cli/           # CLI reference\n├── src/               # React components and pages\n│   ├── components/    # Custom React components\n│   ├── pages/         # Additional pages\n│   ├── css/           # Custom CSS\n│   └── theme/         # Docusaurus theme customization\n├── static/            # Static assets (images, etc.)\n├── i18n/              # Internationalization files\n├── docusaurus.config.ts  # Docusaurus configuration\n└── sidebars.ts        # Documentation sidebar structure\n```\n\n## Adding Documentation\n\n### Adding New Docs\n\nCreate a new Markdown file in the `docs/` directory and add it to `sidebars.ts`.\n\n### Adding New Pages\n\nAdd React components to `src/pages/`.\n\n### Customizing Styles\n\nEdit `src/css/custom.css`.\n\n## Internationalization\n\nDocumentation supports both English and Chinese.\n\n- English: `docs/` and `src/`\n- Chinese: `i18n/zh/docusaurus-plugin-content-docs/current/`\n\nTo add Chinese translations:\n\n1. Create corresponding files in `i18n/zh/docusaurus-plugin-content-docs/current/`\n2. Translate the content\n\n## Deployment\n\nThe documentation can be deployed to:\n\n- GitHub Pages\n- Netlify\n- Vercel\n- Any static hosting service\n\nSee [Docusaurus deployment docs](https://docusaurus.io/docs/deployment) for details.\n"
  },
  {
    "path": "docs/blog/2025-02-25-project-motivation.md",
    "content": "---\ntitle: Project Motivation and Principles\ndate: 2025-02-25\ntags: [claude-code, reverse-engineering, tutorial]\n---\n\n# Project Motivation and Principles\n\nAs early as the day after Claude Code was released (2025-02-25), I began and completed a reverse engineering attempt of the project. At that time, using Claude Code required registering for an Anthropic account, applying for a waitlist, and waiting for approval. However, due to well-known reasons, Anthropic blocks users from mainland China, making it impossible for me to use the service through normal means. Based on known information, I discovered the following:\n\n1. Claude Code is installed via npm, so it's very likely developed with Node.js.\n2. Node.js offers various debugging methods: simple `console.log` usage, launching with `--inspect` to hook into Chrome DevTools, or even debugging obfuscated code using `d8`.\n\nMy goal was to use Claude Code without an Anthropic account. I didn't need the full source code—just a way to intercept and reroute requests made by Claude Code to Anthropic's models to my own custom endpoint. So I started the reverse engineering process:\n\n1. First, install Claude Code:\n```bash\nnpm install -g @anthropic-ai/claude-code\n```\n\n2. After installation, the project is located at `~/.nvm/versions/node/v20.10.0/lib/node_modules/@anthropic-ai/claude-code`(this may vary depending on your Node version manager and version).\n\n3. Open the package.json to analyze the entry point:\n```package.json\n{\n  \"name\": \"@anthropic-ai/claude-code\",\n  \"version\": \"1.0.24\",\n  \"main\": \"sdk.mjs\",\n  \"types\": \"sdk.d.ts\",\n  \"bin\": {\n    \"claude\": \"cli.js\"\n  },\n  \"engines\": {\n    \"node\": \">=18.0.0\"\n  },\n  \"type\": \"module\",\n  \"author\": \"Boris Cherny <boris@anthropic.com>\",\n  \"license\": \"SEE LICENSE IN README.md\",\n  \"description\": \"Use Claude, Anthropic's AI assistant, right from your terminal. Claude can understand your codebase, edit files, run terminal commands, and handle entire workflows for you.\",\n  \"homepage\": \"https://github.com/anthropics/claude-code\",\n  \"bugs\": {\n    \"url\": \"https://github.com/anthropics/claude-code/issues\"\n  },\n  \"scripts\": {\n    \"prepare\": \"node -e \\\"if (!process.env.AUTHORIZED) { console.error('ERROR: Direct publishing is not allowed.\\\\nPlease use the publish-external.sh script to publish this package.'); process.exit(1); }\\\"\",\n    \"preinstall\": \"node scripts/preinstall.js\"\n  },\n  \"dependencies\": {},\n  \"optionalDependencies\": {\n    \"@img/sharp-darwin-arm64\": \"^0.33.5\",\n    \"@img/sharp-darwin-x64\": \"^0.33.5\",\n    \"@img/sharp-linux-arm\": \"^0.33.5\",\n    \"@img/sharp-linux-arm64\": \"^0.33.5\",\n    \"@img/sharp-linux-x64\": \"^0.33.5\",\n    \"@img/sharp-win32-x64\": \"^0.33.5\"\n  }\n}\n```\n\nThe key entry is `\"claude\": \"cli.js\"`. Opening cli.js, you'll see the code is minified and obfuscated. But using WebStorm's `Format File` feature, you can reformat it for better readability:\n![webstorm-formate-file](/blog-images/webstorm-formate-file.png)\n\nNow you can begin understanding Claude Code's internal logic and prompt structure by reading the code. To dig deeper, you can insert console.log statements or launch in debug mode with Chrome DevTools using:\n\n```bash\nNODE_OPTIONS=\"--inspect-brk=9229\" claude\n```\n\nThis command starts Claude Code in debug mode and opens port 9229. Visit chrome://inspect/ in Chrome and click inspect to begin debugging:\n![chrome-devtools](/blog-images/chrome-inspect.png)\n![chrome-devtools](/blog-images/chrome-devtools.png)\n\nBy searching for the keyword api.anthropic.com, you can easily locate where Claude Code makes its API calls. From the surrounding code, it's clear that baseURL can be overridden with the `ANTHROPIC_BASE_URL` environment variable, and `apiKey` and `authToken` can be configured similarly:\n![search](/blog-images/search.png)\n\nSo far, we've discovered some key information:\n\n1. Environment variables can override Claude Code's `baseURL` and `apiKey`.\n\n2. Claude Code adheres to the Anthropic API specification.\n\nTherefore, we need:\n1. A service to convert OpenAI API-compatible requests into Anthropic API format.\n\n2. Set the environment variables before launching Claude Code to redirect requests to this service.\n\nThus, `claude-code-router` was born. This project uses `Express.js` to implement the `/v1/messages` endpoint. It leverages middlewares to transform request/response formats and supports request rewriting (useful for prompt tuning per model).\n\nBack in February, the full DeepSeek model series had poor support for Function Calling, so I initially used `qwen-max`. It worked well—but without KV cache support, it consumed a large number of tokens and couldn't provide the native `Claude Code` experience.\n\nSo I experimented with a Router-based mode using a lightweight model to dispatch tasks. The architecture included four roles: `router`, `tool`, `think`, and `coder`. Each request passed through a free lightweight model that would decide whether the task involved reasoning, coding, or tool usage. Reasoning and coding tasks looped until a tool was invoked to apply changes. However, the lightweight model lacked the capability to route tasks accurately, and architectural issues prevented it from effectively driving Claude Code.\n\nEverything changed at the end of May when the official Claude Code was launched, and `DeepSeek-R1` model (released 2025-05-28) added Function Call support. I redesigned the system. With the help of AI pair programming, I fixed earlier request/response transformation issues—especially the handling of models that return JSON instead of Function Call outputs.\n\nThis time, I used the `DeepSeek-V3`  model. It performed better than expected: supporting most tool calls, handling task decomposition and stepwise planning, and—most importantly—costing less than one-tenth the price of Claude 3.5 Sonnet.\n\nThe official Claude Code organizes agents differently from the beta version, so I restructured my Router mode to include four roles: the default model, `background`, `think`, and `longContext`.\n\n- The default model handles general tasks and acts as a fallback.\n\n- The `background` model manages lightweight background tasks. According to Anthropic, Claude Haiku 3.5 is often used here, so I routed this to a local `ollama` service.\n\n- The `think` model is responsible for reasoning and planning mode tasks. I use `DeepSeek-R1` here, though it doesn't support cost control, so `Think` and `UltraThink` behave identically.\n\n- The `longContext` model handles long-context scenarios. The router uses `tiktoken` to calculate token lengths in real time, and if the context exceeds 32K, it switches to this model to compensate for DeepSeek's long-context limitations.\n\nThis describes the evolution and reasoning behind the project. By cleverly overriding environment variables, we can forward and modify requests without altering Claude Code's source—allowing us to benefit from official updates while using our own models and custom prompts.\n\nThis project offers a practical approach to running Claude Code under Anthropic's regional restrictions, balancing `cost`, `performance`, and `customizability`. That said, the official `Max Plan` still offers the best experience if available.\n"
  },
  {
    "path": "docs/blog/2025-11-18-glm-reasoning.md",
    "content": "---\ntitle: GLM-4.6 Supports Reasoning and Interleaved Thinking\ndate: 2025-11-18\ntags: [glm, reasoning, chain-of-thought]\n---\n\n# GLM-4.6 Supports Reasoning and Interleaved Thinking\n\n## Enabling Reasoning in Claude Code with GLM-4.6\n\nStarting from version 4.5, GLM has supported Claude Code. I've been following its progress closely, and many users have reported that reasoning could not be enabled within Claude Code. Recently, thanks to sponsorship from Zhipu, I decided to investigate this issue in depth. According to the [official documentation](https://docs.z.ai/api-reference/llm/chat-completion), the`/chat/completions` endpoint has reasoning enabled by default, but the model itself decides whether to think:\n\n```\nthinking.type enum<string> default:enabled\n\nWhether to enable the chain of thought(When enabled, GLM-4.6, GLM-4.5 and others will automatically determine whether to think, while GLM-4.5V will think compulsorily), default: enabled\n\nAvailable options: enabled, disabled\n```\n\nHowever, within Claude Code, its heavy system prompt interference disrupts GLM's internal reasoning judgment, causing the model to rarely think.\nTherefore, we need to explicitly guide the model to believe reasoning is required. Since claude-code-router functions as a proxy, the only feasible approach is modifying prompts or parameters.\n\nInitially, I tried completely removing Claude Code's system prompt — and indeed, the model started reasoning — but that broke Claude Code's workflow.\nSo instead, I used prompt injection to clearly instruct the model to think step by step.\n\n\n```javascript\n// transformer.ts\nimport { UnifiedChatRequest } from \"../types/llm\";\nimport { Transformer } from \"../types/transformer\";\n\nexport class ForceReasoningTransformer implements Transformer {\n  name = \"forcereasoning\";\n\n  async transformRequestIn(\n    request: UnifiedChatRequest\n  ): Promise<UnifiedChatRequest> {\n    const systemMessage = request.messages.find(\n      (item) => item.role === \"system\"\n    );\n    if (Array.isArray(systemMessage?.content)) {\n      systemMessage.content.push({\n        type: \"text\",\n        text: \"You are an expert reasoning model.\\nAlways think step by step before answering. Even if the problem seems simple, always write down your reasoning process explicitly.\\nNever skip your chain of thought.\\nUse the following output format:\\n<reasoning_content>(Write your full detailed thinking here.)</reasoning_content>\\n\\nWrite your final conclusion here.\",\n      });\n    }\n    const lastMessage = request.messages[request.messages.length - 1];\n    if (lastMessage.role === \"user\" && Array.isArray(lastMessage.content)) {\n      lastMessage.content.push({\n        type: \"text\",\n        text: \"You are an expert reasoning model.\\nAlways think step by step before answering. Even if the problem seems simple, always write down your reasoning process explicitly.\\nNever skip your chain of thought.\\nUse the following output format:\\n<reasoning_content>(Write your full detailed thinking here.)</reasoning_content>\\n\\nWrite your final conclusion here.\",\n      });\n    }\n    if (lastMessage.role === \"tool\") {\n      request.messages.push({\n        role: \"user\",\n        content: [\n          {\n            type: \"text\",\n            text: \"You are an expert reasoning model.\\nAlways think step by step before answering. Even if the problem seems simple, always write down your reasoning process explicitly.\\nNever skip your chain of thought.\\nUse the following output format:\\n<reasoning_content>(Write your full detailed thinking here.)</reasoning_content>\\n\\nWrite your final conclusion here.\",\n          },\n        ],\n      });\n    }\n    return request;\n  }\n}\n```\n\nWhy use `<reasoning_content>` instead of the `<think>` tag? Two reasons:\n\n1. Using the `<think>` tag doesn't effectively trigger reasoning — likely because the model was trained on data where `<think>` had special behavior.\n\n2. If we use `<think>`, the reasoning output is split into a separate field, which directly relates to the chain-of-thought feedback problem discussed below.\n\n## Chain-of-Thought Feedback\nRecently, Minimax released `Minimax-m2`, along with [an article](https://www.minimaxi.com/news/why-is-interleaved-thinking-important-for-m2) explaining interleaved thinking.\nWhile the idea isn't entirely new, it's a good opportunity to analyze it.\n\nWhy do we need to interleaved thinking?\nMinimax's article mentions that the Chat Completion API does not support passing reasoning content between requests.\nWe know ChatGPT was the first to support reasoning, but OpenAI initially didn't expose the chain of thought to users.\nTherefore, the Chat Completion API didn't need to support it. Even the CoT field was first introduced by DeepSeek.\n\nDo we really need explicit CoT fields? What happens if we don't have them? Will it affect reasoning?\nBy inspecting [sglang's source code](https://github.com/sgl-project/sglang/blob/main/python/sglang/srt/parser/reasoning_parser.py), we can see that reasoning content is naturally emitted in messages with specific markers.\nIf we don't split it out, the next-round conversation will naturally include it.\nThus, the only reason we need interleaved thinking is because we separated the reasoning content from the normal messages.\n\nWith fewer than 40 lines of code above, I implemented a simple exploration of enabling reasoning and chain-of-thought feedback for GLM-4.5/4.6.\n(It's only simple because I haven't implemented parsing logic yet — you could easily modify the transformer to split reasoning output on response and merge it back on request, improving Claude Code's frontend display compatibility.)\n\nIf you have better ideas, feel free to reach out — I'd love to discuss further.\n"
  },
  {
    "path": "docs/blog/2025-11-18-router-exploration.md",
    "content": "---\ntitle: Maybe We Can Do More with the Router\ndate: 2025-11-18\ntags: [router, transformer, deepseek]\n---\n\n# Maybe We Can Do More with the Router\n\nSince the release of `claude-code-router`, I've received a lot of user feedback, and quite a few issues are still open. Most of them are related to support for different providers and the lack of tool usage from the deepseek model.\n\nOriginally, I created this project for personal use, mainly to access claude code at a lower cost. So, multi-provider support wasn't part of the initial design. But during troubleshooting, I discovered that even though most providers claim to be compatible with the OpenAI-style `/chat/completions` interface, there are many subtle differences. For example:\n\n1. When Gemini's tool parameter type is string, the `format` field only supports `date` and `date-time`, and there's no tool call ID.\n\n2. OpenRouter requires `cache_control` for caching.\n\n3. The official DeepSeek API has a `max_output` of 8192, but Volcano Engine's limit is even higher.\n\nAside from these, smaller providers often have quirks in their parameter handling. So I decided to create a new project, [musistudio/llms](https://github.com/musistudio/llms), to deal with these compatibility issues. It uses the OpenAI format as a base and introduces a generic Transformer interface for transforming both requests and responses.\n\nOnce a `Transformer` is implemented for each provider, it becomes possible to mix-and-match requests between them. For example, I implemented bidirectional conversion between Anthropic and OpenAI formats in `AnthropicTransformer`, which listens to the `/v1/messages` endpoint. Similarly, `GeminiTransformer` handles Gemini &lt;-&gt; OpenAI format conversions and listens to `/v1beta/models/:modelAndAction`.\n\nWhen both requests and responses are transformed into a common format, they can interoperate seamlessly:\n\n```\nAnthropicRequest -> AnthropicTransformer -> OpenAIRequest -> GeminiTransformer -> GeminiRequest -> GeminiServer\n```\n\n```\nGeminiResponse -> GeminiTransformer -> OpenAIResponse -> AnthropicTransformer -> AnthropicResponse\n```\n\nUsing a middleware layer to smooth out differences may introduce some performance overhead, but the main goal here is to enable `claude-code-router` to support multiple providers.\n\nAs for the issue of DeepSeek's lackluster tool usage — I found that it stems from poor instruction adherence in long conversations. Initially, the model actively calls tools, but after several rounds, it starts responding with plain text instead. My first workaround was injecting a system prompt to remind the model to use tools proactively. But in long contexts, the model tends to forget this instruction.\n\nAfter reading the DeepSeek documentation, I noticed it supports the `tool_choice` parameter, which can be set to `\"required\"` to force the model to use at least one tool. I tested this by enabling the parameter, and it significantly improved the model's tool usage. We can remove the setting when it's no longer necessary. With the help of the `Transformer` interface in [musistudio/llms](https://github.com/musistudio/llms), we can modify the request before it's sent and adjust the response after it's received.\n\nInspired by the Plan Mode in `claude code`, I implemented a similar Tool Mode for DeepSeek:\n\n```typescript\nexport class TooluseTransformer implements Transformer {\n  name = \"tooluse\";\n\n  transformRequestIn(request: UnifiedChatRequest): UnifiedChatRequest {\n    if (request.tools?.length) {\n      request.messages.push({\n        role: \"system\",\n        content: `<system-reminder>Tool mode is active. The user expects you to proactively execute the most suitable tool to help complete the task.\nBefore invoking a tool, you must carefully evaluate whether it matches the current task. If no available tool is appropriate for the task, you MUST call the \\`ExitTool\\` to exit tool mode — this is the only valid way to terminate tool mode.\nAlways prioritize completing the user's task effectively and efficiently by using tools whenever appropriate.</system-reminder>`,\n      });\n      request.tool_choice = \"required\";\n      request.tools.unshift({\n        type: \"function\",\n        function: {\n          name: \"ExitTool\",\n          description: `Use this tool when you are in tool mode and have completed the task. This is the only valid way to exit tool mode.\nIMPORTANT: Before using this tool, ensure that none of the available tools are applicable to the current task. You must evaluate all available options — only if no suitable tool can help you complete the task should you use ExitTool to terminate tool mode.\nExamples:\n1. Task: \"Use a tool to summarize this document\" — Do not use ExitTool if a summarization tool is available.\n2. Task: \"What's the weather today?\" — If no tool is available to answer, use ExitTool after reasoning that none can fulfill the task.`,\n          parameters: {\n            type: \"object\",\n            properties: {\n              response: {\n                type: \"string\",\n                description:\n                  \"Your response will be forwarded to the user exactly as returned — the tool will not modify or post-process it in any way.\",\n              },\n            },\n            required: [\"response\"],\n          },\n        },\n      });\n    }\n    return request;\n  }\n\n  async transformResponseOut(response: Response): Promise<Response> {\n    if (response.headers.get(\"Content-Type\")?.includes(\"application/json\")) {\n      const jsonResponse = await response.json();\n      if (\n        jsonResponse?.choices[0]?.message.tool_calls?.length &&\n        jsonResponse?.choices[0]?.message.tool_calls[0]?.function?.name ===\n          \"ExitTool\"\n      ) {\n        const toolArguments = JSON.parse(toolCall.function.arguments || \"{}\");\n        jsonResponse.choices[0].message.content = toolArguments.response || \"\";\n        delete jsonResponse.choices[0].message.tool_calls;\n      }\n\n      // Handle non-streaming response if needed\n      return new Response(JSON.stringify(jsonResponse), {\n        status: response.status,\n        statusText: response.statusText,\n        headers: response.headers,\n      });\n    } else if (response.headers.get(\"Content-Type\")?.includes(\"stream\")) {\n      // ...\n    }\n    return response;\n  }\n}\n```\n\nThis transformer ensures the model calls at least one tool. If no tools are appropriate or the task is finished, it can exit using `ExitTool`. Since this relies on the `tool_choice` parameter, it only works with models that support it.\n\nIn practice, this approach noticeably improves tool usage for DeepSeek. The tradeoff is that sometimes the model may invoke irrelevant or unnecessary tools, which could increase latency and token usage.\n\nThis update is just a small experiment — adding an `\"agent\"` to the router. Maybe there are more interesting things we can explore from here.\n"
  },
  {
    "path": "docs/docs/cli/commands/model.md",
    "content": "---\nsidebar_position: 2\n---\n\n# ccr model\n\nInteractive model selection and configuration.\n\n## Usage\n\n```bash\nccr model [command]\n```\n\n## Commands\n\n### Select Model\n\nInteractively select a model:\n\n```bash\nccr model\n```\n\nThis will display an interactive menu with available providers and models.\n\n### Set Default Model\n\nSet the default model directly:\n\n```bash\nccr model set <provider>,<model>\n```\n\nExample:\n\n```bash\nccr model set deepseek,deepseek-chat\n```\n\n### List Models\n\nList all configured models:\n\n```bash\nccr model list\n```\n\n### Add Model\n\nAdd a new model to configuration:\n\n```bash\nccr model add <provider>,<model>\n```\n\nExample:\n\n```bash\nccr model add groq,llama-3.3-70b-versatile\n```\n\n### Remove Model\n\nRemove a model from configuration:\n\n```bash\nccr model remove <provider>,<model>\n```\n\n## Examples\n\n### Interactive selection\n\n```bash\n$ ccr model\n\n? Select a provider: deepseek\n? Select a model: deepseek-chat\n\nDefault model set to: deepseek,deepseek-chat\n```\n\n### Direct configuration\n\n```bash\nccr model set deepseek,deepseek-chat\n```\n\n### View current configuration\n\n```bash\nccr model list\n```\n\nOutput:\n\n```\nConfigured Models:\n  deepseek,deepseek-chat (default)\n  groq,llama-3.3-70b-versatile\n  gemini,gemini-1.5-pro\n```\n\n## Related Commands\n\n- [ccr start](/docs/cli/start) - Start the server\n- [ccr config](/docs/cli/other-commands#ccr-config) - Edit configuration\n"
  },
  {
    "path": "docs/docs/cli/commands/other.md",
    "content": "---\nsidebar_position: 4\n---\n\n# Other Commands\n\nAdditional CLI commands for managing Claude Code Router.\n\n## ccr stop\n\nStop the running server.\n\n```bash\nccr stop\n```\n\n## ccr restart\n\nRestart the server.\n\n```bash\nccr restart\n```\n\n## ccr code\n\nExecute a claude command through the router.\n\n```bash\nccr code [args...]\n```\n\n## ccr ui\n\nOpen the Web UI in your browser.\n\n```bash\nccr ui\n```\n\n## ccr activate\n\nOutput shell environment variables for integration with external tools.\n\n```bash\nccr activate\n```\n\n## Global Options\n\nThese options can be used with any command:\n\n| Option | Description |\n|--------|-------------|\n| `-h, --help` | Show help |\n| `-v, --version` | Show version number |\n| `--config <path>` | Path to configuration file |\n| `--verbose` | Enable verbose output |\n\n## Examples\n\n### Stop the server\n\n```bash\nccr stop\n```\n\n### Restart with custom config\n\n```bash\nccr restart --config /path/to/config.json\n```\n\n### Open Web UI\n\n```bash\nccr ui\n```\n\n## Related Documentation\n\n- [Getting Started](/docs/intro) - Introduction to Claude Code Router\n- [Configuration](/docs/config/basic) - Configuration guide\n"
  },
  {
    "path": "docs/docs/cli/commands/preset.md",
    "content": "---\nsidebar_position: 5\n---\n\n# ccr preset\n\nManage presets - configuration templates that can be shared and reused.\n\n## Overview\n\nPresets allow you to:\n- Save your current configuration as a reusable template\n- Share configurations with others\n- Install pre-configured setups from the community\n- Switch between different configurations easily\n\n## Commands\n\n### export\n\nExport your current configuration as a preset.\n\n```bash\nccr preset export <name> [options]\n```\n\n**Options:**\n- `--output <path>` - Custom output directory path\n- `--description <text>` - Preset description\n- `--author <name>` - Preset author\n- `--tags <tags>` - Comma-separated keywords\n- `--include-sensitive` - Include API keys and sensitive data (not recommended)\n\n**Example:**\n```bash\nccr preset export my-config --description \"My production setup\" --author \"Your Name\"\n```\n\n**What happens:**\n1. Reads current configuration from `~/.claude-code-router/config.json`\n2. Prompts for description, author, and keywords (if not provided)\n3. Sanitizes sensitive fields (API keys become placeholders)\n4. Creates preset directory at `~/.claude-code-router/presets/<name>/`\n5. Generates `manifest.json` with configuration and metadata\n\n### install\n\nInstall a preset from a local directory.\n\n```bash\nccr preset install <source>\n```\n\n**Sources:**\n- Local directory path: `/path/to/preset-directory`\n- Preset name (for reconfiguring an already installed preset): `preset-name`\n\n**Example:**\n```bash\n# Install from directory\nccr preset install ./my-preset\n\n# Reconfigure an installed preset\nccr preset install my-preset\n```\n\n**What happens:**\n1. Reads `manifest.json` from the preset directory\n2. Validates the preset structure\n3. If the preset has a `schema`, prompts for required values (API keys, etc.)\n4. Copies preset to `~/.claude-code-router/presets/<name>/`\n5. Saves user inputs in `manifest.json`\n\n**Note:** URL installation is not currently supported. Download the preset directory first.\n\n### list\n\nList all installed presets.\n\n```bash\nccr preset list\n```\n\n**Example output:**\n```\nAvailable presets:\n\n• my-config (v1.0.0)\n  My production setup\n  by Your Name\n\n• openai-setup\n  Basic OpenAI configuration\n```\n\n### info\n\nShow detailed information about a preset.\n\n```bash\nccr preset info <name>\n```\n\n**Shows:**\n- Version, description, author, keywords\n- Configuration summary (Providers, Router rules)\n- Required inputs (if any)\n\n**Example:**\n```bash\nccr preset info my-config\n```\n\n### delete / rm / remove\n\nDelete an installed preset.\n\n```bash\nccr preset delete <name>\nccr preset rm <name>\nccr preset remove <name>\n```\n\n**Example:**\n```bash\nccr preset delete my-config\n```\n\n## Preset Structure\n\nA preset is a directory containing a `manifest.json` file:\n\n```json\n{\n  \"name\": \"my-preset\",\n  \"version\": \"1.0.0\",\n  \"description\": \"My configuration\",\n  \"author\": \"Author Name\",\n  \"keywords\": [\"openai\", \"production\"],\n\n  \"Providers\": [\n    {\n      \"name\": \"openai\",\n      \"api_base_url\": \"https://api.openai.com/v1/chat/completions\",\n      \"api_key\": \"{{apiKey}}\",\n      \"models\": [\"gpt-4\", \"gpt-3.5-turbo\"]\n    }\n  ],\n\n  \"Router\": {\n    \"default\": \"openai,gpt-4\"\n  },\n\n  \"schema\": [\n    {\n      \"id\": \"apiKey\",\n      \"type\": \"password\",\n      \"label\": \"OpenAI API Key\",\n      \"prompt\": \"Enter your OpenAI API key\"\n    }\n  ]\n}\n```\n\n### Schema System\n\nThe `schema` field defines inputs that users must provide during installation:\n\n**Field types:**\n- `password` - Hidden input (for API keys)\n- `input` - Text input\n- `select` - Single selection from options\n- `multiselect` - Multiple selection\n- `confirm` - Yes/No confirmation\n- `editor` - Multi-line text\n- `number` - Numeric input\n\n**Dynamic options:**\n```json\n{\n  \"id\": \"provider\",\n  \"type\": \"select\",\n  \"label\": \"Select Provider\",\n  \"options\": {\n    \"type\": \"providers\"\n  }\n}\n```\n\n**Conditional fields:**\n```json\n{\n  \"id\": \"model\",\n  \"type\": \"select\",\n  \"label\": \"Select Model\",\n  \"when\": {\n    \"field\": \"provider\",\n    \"operator\": \"exists\"\n  },\n  \"options\": {\n    \"type\": \"models\",\n    \"providerField\": \"#{selectedProvider}\"\n  }\n}\n```\n\n## Sharing Presets\n\nTo share a preset:\n\n1. **Export your configuration:**\n   ```bash\n   ccr preset export my-preset\n   ```\n\n2. **Share the directory:**\n   ```bash\n   ~/.claude-code-router/presets/my-preset/\n   ```\n\n3. **Distribution methods:**\n   - Upload to GitHub repository\n   - Create a GitHub Gist\n   - Share as a zip file\n   - Publish on npm (future feature)\n\n4. **Users install with:**\n   ```bash\n   ccr preset install /path/to/my-preset\n   ```\n\n## Security\n\n### Automatic Sanitization\n\nBy default, `export` sanitizes sensitive fields:\n- Fields named `api_key`, `apikey`, `password`, `secret` are replaced with `{{fieldName}}` placeholders\n- These placeholders become required inputs in the schema\n- Users are prompted to provide their own values during installation\n\n### Include Sensitive Data\n\nTo include actual values (not recommended):\n```bash\nccr preset export my-preset --include-sensitive\n```\n\n**Warning:** Never share presets containing sensitive data!\n\n## Related Documentation\n\n- [Configuration Guide](/docs/cli/config/basic) - Basic configuration\n- [Project-Level Configuration](/docs/cli/config/project-level) - Project-specific settings\n- [Presets](/docs/presets/intro) - Advanced preset topics\n"
  },
  {
    "path": "docs/docs/cli/commands/start.md",
    "content": "---\nsidebar_position: 1\n---\n\n# ccr start\n\nStart the Claude Code Router server.\n\n## Usage\n\n```bash\nccr start [options]\n```\n\n## Options\n\n| Option | Alias | Description |\n|--------|-------|-------------|\n| `--port <number>` | `-p` | Port to listen on (default: 8080) |\n| `--config <path>` | `-c` | Path to configuration file |\n| `--daemon` | `-d` | Run as daemon (background process) |\n| `--log-level <level>` | `-l` | Log level (fatal/error/warn/info/debug/trace) |\n\n## Examples\n\n### Start with default settings\n\n```bash\nccr start\n```\n\n### Start on custom port\n\n```bash\nccr start --port 3000\n```\n\n### Start with custom config\n\n```bash\nccr start --config /path/to/config.json\n```\n\n### Start as daemon\n\n```bash\nccr start --daemon\n```\n\n### Start with debug logging\n\n```bash\nccr start --log-level debug\n```\n\n## Environment Variables\n\nYou can also configure the server using environment variables:\n\n| Variable | Description |\n|----------|-------------|\n| `PORT` | Port to listen on |\n| `CONFIG_PATH` | Path to configuration file |\n| `LOG_LEVEL` | Logging level |\n| `CUSTOM_ROUTER_PATH` | Path to custom router function |\n| `HOST` | Host to bind to (default: 0.0.0.0) |\n\n## Output\n\nWhen started successfully, you'll see:\n\n```\nClaude Code Router is running on http://localhost:8080\nAPI endpoint: http://localhost:8080/v1\n```\n\n## Related Commands\n\n- [ccr stop](/docs/cli/other-commands#ccr-stop) - Stop the server\n- [ccr restart](/docs/cli/other-commands#ccr-restart) - Restart the server\n- [ccr status](/docs/cli/other-commands#ccr-status) - Check server status\n"
  },
  {
    "path": "docs/docs/cli/commands/status.md",
    "content": "---\nsidebar_position: 3\n---\n\n# ccr status\n\nShow the current status of the Claude Code Router server.\n\n## Usage\n\n```bash\nccr status\n```\n\n## Output\n\n### Running Server\n\nWhen the server is running:\n\n```\nClaude Code Router Status: Running\nVersion: 2.0.0\nPID: 12345\nPort: 8080\nUptime: 2h 34m\nConfiguration: /home/user/.claude-code-router/config.json\n```\n\n### Stopped Server\n\nWhen the server is not running:\n\n```\nClaude Code Router Status: Stopped\n```\n\n## Exit Codes\n\n| Code | Description |\n|------|-------------|\n| 0 | Server is running |\n| 1 | Server is stopped |\n| 2 | Error checking status |\n\n## Examples\n\n```bash\n$ ccr status\n\nClaude Code Router Status: Running\nVersion: 2.0.0\nPID: 12345\nPort: 8080\nUptime: 2h 34m\n```\n\n## Related Commands\n\n- [ccr start](/docs/cli/start) - Start the server\n- [ccr stop](/docs/cli/other-commands#ccr-stop) - Stop the server\n- [ccr restart](/docs/cli/other-commands#ccr-restart) - Restart the server\n"
  },
  {
    "path": "docs/docs/cli/commands/statusline.md",
    "content": "---\nsidebar_position: 5\n---\n\n# ccr statusline\n\nDisplay a customizable status bar showing real-time information about your Claude Code session, including workspace, Git branch, model, token usage, and more.\n\n## Overview\n\nThe `ccr statusline` command reads JSON data from stdin and renders a beautifully formatted status bar in your terminal. It's designed to integrate with Claude Code's hook system to display real-time session information.\n\n## Usage\n\n### Basic Usage\n\n```bash\nccr statusline\n```\n\nThe command expects JSON data via stdin, typically piped from a Claude Code hook:\n\n```bash\necho '{\"hook_event_name\":\"...\",\"session_id\":\"...\",\"...\"}' | ccr statusline\n```\n\n### Hook Integration\n\nConfigure in your Claude Code settings:\n\n```json\n{\n  \"hooks\": {\n    \"postResponse\": {\n      \"command\": \"ccr statusline\",\n      \"input\": \"json\"\n    }\n  }\n}\n```\n\n## Available Themes\n\n### Default Theme\n\nA clean, minimal theme with Nerd Font icons and colored text:\n\n```\n 󰉋 my-project   main  󰚩 claude-3-5-sonnet-20241022  ↑ 12.3k  ↓ 5.2k\n```\n\n### Powerline Theme\n\nA vim-powerline inspired style with colored backgrounds and arrow separators:\n\n```\n 󰉋 my-project   main  󰚩 claude-3-5-sonnet-20241022  ↑ 12.3k  ↓ 5.2k\n```\n\nActivate by setting `currentStyle: \"powerline\"` in your config.\n\n### Simple Theme\n\nFallback theme without icons for terminals that don't support Nerd Fonts:\n\n```\nmy-project  main  claude-3-5-sonnet-20241022  ↑ 12.3k  ↓ 5.2k\n```\n\nAutomatically used when `USE_SIMPLE_ICONS=true` or on unsupported terminals.\n\n## Available Modules\n\nStatus line modules display different types of information:\n\n| Module | Description | Variables |\n|--------|-------------|-----------|\n| **workDir** | Current working directory name | `{{workDirName}}` |\n| **gitBranch** | Current Git branch | `{{gitBranch}}` |\n| **model** | Model being used | `{{model}}` |\n| **usage** | Token usage (input/output) | `{{inputTokens}}`, `{{outputTokens}}` |\n| **context** | Context window usage | `{{contextPercent}}`, `{{contextWindowSize}}` |\n| **speed** | Token processing speed | `{{tokenSpeed}}`, `{{isStreaming}}` |\n| **cost** | API cost | `{{cost}}` |\n| **duration** | Session duration | `{{duration}}` |\n| **lines** | Code changes | `{{linesAdded}}`, `{{linesRemoved}}` |\n| **script** | Custom script output | Dynamic |\n\n## Configuration\n\nConfigure statusline in `~/.claude-code-router/config.json`:\n\n### Default Style Example\n\n```json\n{\n  \"StatusLine\": {\n    \"currentStyle\": \"default\",\n    \"default\": {\n      \"modules\": [\n        {\n          \"type\": \"workDir\",\n          \"icon\": \"󰉋\",\n          \"text\": \"{{workDirName}}\",\n          \"color\": \"bright_blue\"\n        },\n        {\n          \"type\": \"gitBranch\",\n          \"icon\": \"\",\n          \"text\": \"{{gitBranch}}\",\n          \"color\": \"bright_magenta\"\n        },\n        {\n          \"type\": \"model\",\n          \"icon\": \"󰚩\",\n          \"text\": \"{{model}}\",\n          \"color\": \"bright_cyan\"\n        },\n        {\n          \"type\": \"usage\",\n          \"icon\": \"↑\",\n          \"text\": \"{{inputTokens}}\",\n          \"color\": \"bright_green\"\n        },\n        {\n          \"type\": \"usage\",\n          \"icon\": \"↓\",\n          \"text\": \"{{outputTokens}}\",\n          \"color\": \"bright_yellow\"\n        }\n      ]\n    }\n  }\n}\n```\n\n### Powerline Style Example\n\n```json\n{\n  \"StatusLine\": {\n    \"currentStyle\": \"powerline\",\n    \"powerline\": {\n      \"modules\": [\n        {\n          \"type\": \"workDir\",\n          \"icon\": \"󰉋\",\n          \"text\": \"{{workDirName}}\",\n          \"color\": \"white\",\n          \"background\": \"bg_bright_blue\"\n        },\n        {\n          \"type\": \"gitBranch\",\n          \"icon\": \"\",\n          \"text\": \"{{gitBranch}}\",\n          \"color\": \"white\",\n          \"background\": \"bg_bright_magenta\"\n        }\n      ]\n    }\n  }\n}\n```\n\n### Full Featured Example\n\n```json\n{\n  \"StatusLine\": {\n    \"currentStyle\": \"default\",\n    \"default\": {\n      \"modules\": [\n        {\n          \"type\": \"workDir\",\n          \"icon\": \"󰉋\",\n          \"text\": \"{{workDirName}}\",\n          \"color\": \"bright_blue\"\n        },\n        {\n          \"type\": \"gitBranch\",\n          \"icon\": \"\",\n          \"text\": \"{{gitBranch}}\",\n          \"color\": \"bright_magenta\"\n        },\n        {\n          \"type\": \"model\",\n          \"icon\": \"󰚩\",\n          \"text\": \"{{model}}\",\n          \"color\": \"bright_cyan\"\n        },\n        {\n          \"type\": \"context\",\n          \"icon\": \"🪟\",\n          \"text\": \"{{contextPercent}}% / {{contextWindowSize}}\",\n          \"color\": \"bright_green\"\n        },\n        {\n          \"type\": \"speed\",\n          \"icon\": \"⚡\",\n          \"text\": \"{{tokenSpeed}} t/s {{isStreaming}}\",\n          \"color\": \"bright_yellow\"\n        },\n        {\n          \"type\": \"cost\",\n          \"icon\": \"💰\",\n          \"text\": \"{{cost}}\",\n          \"color\": \"bright_magenta\"\n        },\n        {\n          \"type\": \"duration\",\n          \"icon\": \"⏱️\",\n          \"text\": \"{{duration}}\",\n          \"color\": \"bright_white\"\n        },\n        {\n          \"type\": \"lines\",\n          \"icon\": \"📝\",\n          \"text\": \"+{{linesAdded}}/-{{linesRemoved}}\",\n          \"color\": \"bright_cyan\"\n        }\n      ]\n    }\n  }\n}\n```\n\n## Custom Scripts\n\nYou can create custom modules by executing scripts:\n\n```json\n{\n  \"type\": \"script\",\n  \"icon\": \"🔧\",\n  \"scriptPath\": \"/path/to/script.js\",\n  \"options\": {\n    \"customOption\": \"value\"\n  }\n}\n```\n\nScript format (CommonJS):\n\n```javascript\n// my-status-module.js\nmodule.exports = function(variables, options) {\n  // Access variables like model, gitBranch, etc.\n  // Access options from configuration\n  return `Custom: ${variables.model}`;\n};\n\n// Or async\nmodule.exports = async function(variables, options) {\n  const data = await fetchSomeData();\n  return data;\n};\n```\n\n## Color Options\n\n### Standard Colors\n\n- `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`\n- `bright_black`, `bright_red`, `bright_green`, `bright_yellow`, `bright_blue`, `bright_magenta`, `bright_cyan`, `bright_white`\n\n### Background Colors\n\nPrefix with `bg_`: `bg_blue`, `bg_bright_red`, etc.\n\n### Hexadecimal Colors\n\nUse 24-bit TrueColor with hex codes:\n\n```json\n{\n  \"color\": \"#FF5733\",\n  \"background\": \"bg_#1E90FF\"\n}\n```\n\n## Available Variables\n\nAll variables are accessible in module text using `{{variableName}}`:\n\n| Variable | Description | Example |\n|----------|-------------|---------|\n| `{{workDirName}}` | Current directory name | `my-project` |\n| `{{gitBranch}}` | Git branch name | `main` |\n| `{{model}}` | Model name | `claude-3-5-sonnet-20241022` |\n| `{{inputTokens}}` | Input tokens (formatted) | `12.3k` |\n| `{{outputTokens}}` | Output tokens (formatted) | `5.2k` |\n| `{{tokenSpeed}}` | Tokens per second | `45` |\n| `{{isStreaming}}` | Streaming status | `streaming` or empty |\n| `{{contextPercent}}` | Context usage percentage | `45` |\n| `{{contextWindowSize}}` | Total context window | `200k` |\n| `{{cost}}` | Total cost | `$0.15` |\n| `{{duration}}` | Session duration | `2m34s` |\n| `{{linesAdded}}` | Lines added | `150` |\n| `{{linesRemoved}}` | Lines removed | `25` |\n| `{{sessionId}}` | Session ID (first 8 chars) | `a1b2c3d4` |\n\n## Environment Variables\n\nControl behavior with environment variables:\n\n| Variable | Values | Description |\n|----------|--------|-------------|\n| `USE_SIMPLE_ICONS` | `true`/`false` | Force simple theme without icons |\n| `NERD_FONT` | Any value | Auto-detect Nerd Font support |\n\n## Examples\n\n### Minimal Status Line\n\n```json\n{\n  \"StatusLine\": {\n    \"default\": {\n      \"modules\": [\n        {\n          \"type\": \"model\",\n          \"text\": \"{{model}}\"\n        },\n        {\n          \"type\": \"usage\",\n          \"text\": \"↑{{inputTokens}} ↓{{outputTokens}}\"\n        }\n      ]\n    }\n  }\n}\n```\n\nOutput: `claude-3-5-sonnet-20241022 ↑12.3k ↓5.2k`\n\n### Developer Productivity Focus\n\n```json\n{\n  \"StatusLine\": {\n    \"default\": {\n      \"modules\": [\n        {\n          \"type\": \"gitBranch\",\n          \"icon\": \"\",\n          \"text\": \"{{gitBranch}}\",\n          \"color\": \"bright_magenta\"\n        },\n        {\n          \"type\": \"lines\",\n          \"icon\": \"📝\",\n          \"text\": \"+{{linesAdded}}/-{{linesRemoved}}\",\n          \"color\": \"bright_cyan\"\n        },\n        {\n          \"type\": \"duration\",\n          \"icon\": \"⏱️\",\n          \"text\": \"{{duration}}\",\n          \"color\": \"bright_white\"\n        }\n      ]\n    }\n  }\n}\n```\n\nOutput: ` feature/auth  📝 +150/-25  ⏱️ 2m34s`\n\n## Preset Integration\n\nStatusline themes can be included in presets. When you install a preset with statusline configuration, it will automatically apply when you activate that preset.\n\nSee [Presets](/docs/presets/intro) for more information.\n\n## Troubleshooting\n\n### Icons Not Displaying\n\nSet `USE_SIMPLE_ICONS=true` in your environment:\n\n```bash\nexport USE_SIMPLE_ICONS=true\n```\n\n### Colors Not Working\n\nEnsure your terminal supports TrueColor (24-bit color):\n\n```bash\nexport COLORTERM=truecolor\n```\n\n### Git Branch Not Showing\n\nEnsure you're in a Git repository and have the `git` command installed.\n\n## Related Commands\n\n- [ccr status](/docs/cli/commands/status) - Check server status\n- [ccr preset](/docs/cli/commands/preset) - Manage presets with statusline themes\n"
  },
  {
    "path": "docs/docs/cli/config/basic.md",
    "content": "---\ntitle: Basic Configuration\n---\n\n# Basic Configuration\n\nCLI uses the same configuration file as Server: `~/.claude-code-router/config.json`\n\n## Configuration Methods\n\nYou can configure Claude Code Router in two ways:\n\n### Option 1: Edit Configuration File Directly\n\nEdit `~/.claude-code-router/config.json` with your favorite editor:\n\n```bash\nnano ~/.claude-code-router/config.json\n```\n\n### Option 2: Use Web UI\n\nOpen the web interface and configure visually:\n\n```bash\nccr ui\n```\n\n## Restart After Configuration Changes\n\nAfter modifying the configuration file or making changes through the Web UI, you must restart the service:\n\n```bash\nccr restart\n```\n\nOr restart directly through the Web UI.\n\n## Configuration File Location\n\n```bash\n~/.claude-code-router/config.json\n```\n\n## Minimal Configuration Example\n\n```json5\n{\n  // API key (optional, used to protect service)\n  \"APIKEY\": \"your-api-key-here\",\n\n  // LLM providers\n  \"Providers\": [\n    {\n      \"name\": \"openai\",\n      \"baseUrl\": \"https://api.openai.com/v1\",\n      \"apiKey\": \"$OPENAI_API_KEY\",\n      \"models\": [\"gpt-4\", \"gpt-3.5-turbo\"]\n    }\n  ],\n\n  // Default routing\n  \"Router\": {\n    \"default\": \"openai,gpt-4\"\n  }\n}\n```\n\n## Environment Variables\n\nConfiguration supports environment variable interpolation:\n\n```json5\n{\n  \"Providers\": [\n    {\n      \"apiKey\": \"$OPENAI_API_KEY\"  // Read from environment variable\n    }\n  ]\n}\n```\n\nSet in `.bashrc` or `.zshrc`:\n\n```bash\nexport OPENAI_API_KEY=\"sk-...\"\nexport ANTHROPIC_API_KEY=\"sk-ant-...\"\n```\n\n## Common Configuration Options\n\n### HOST and PORT\n\n```json5\n{\n  \"HOST\": \"127.0.0.1\",  // Listen address\n  \"PORT\": 3456          // Listen port\n}\n```\n\n### Logging Configuration\n\n```json5\n{\n  \"LOG\": true,          // Enable logging\n  \"LOG_LEVEL\": \"info\"   // Log level\n}\n```\n\n### Routing Configuration\n\n```json5\n{\n  \"Router\": {\n    \"default\": \"openai,gpt-4\",\n    \"background\": \"openai,gpt-3.5-turbo\",\n    \"think\": \"openai,gpt-4\",\n    \"longContext\": \"anthropic,claude-3-opus\"\n  }\n}\n```\n\n## Configuration Validation\n\nConfiguration file is automatically validated. Common errors:\n\n- **Missing Providers**: Must configure at least one provider\n- **Missing API Key**: If Providers are configured, must provide API Key\n- **Model doesn't exist**: Ensure model is in provider's models list\n\n## Configuration Backup\n\nConfiguration is automatically backed up on each update:\n\n```\n~/.claude-code-router/config.backup.{timestamp}.json\n```\n\n## Apply Configuration Changes\n\nAfter modifying the configuration file or making changes through the Web UI, restart the service:\n\n```bash\nccr restart\n```\n\nOr restart directly through the Web UI by clicking the \"Save and Restart\" button.\n\n## View Current Configuration\n\n```bash\n# View via API\ncurl http://localhost:3456/api/config\n\n# Or view configuration file\ncat ~/.claude-code-router/config.json\n```\n\n## Example Configurations\n\n### OpenAI\n\n```json5\n{\n  \"Providers\": [\n    {\n      \"name\": \"openai\",\n      \"baseUrl\": \"https://api.openai.com/v1\",\n      \"apiKey\": \"$OPENAI_API_KEY\",\n      \"models\": [\"gpt-4\", \"gpt-3.5-turbo\"]\n    }\n  ],\n  \"Router\": {\n    \"default\": \"openai,gpt-4\"\n  }\n}\n```\n\n### Anthropic\n\n```json5\n{\n  \"Providers\": [\n    {\n      \"name\": \"anthropic\",\n      \"baseUrl\": \"https://api.anthropic.com/v1\",\n      \"apiKey\": \"$ANTHROPIC_API_KEY\",\n      \"models\": [\"claude-3-5-sonnet-20241022\", \"claude-3-opus-20240229\"]\n    }\n  ],\n  \"Router\": {\n    \"default\": \"anthropic,claude-3-5-sonnet-20241022\"\n  }\n}\n```\n\n### Multiple Providers\n\n```json5\n{\n  \"Providers\": [\n    {\n      \"name\": \"openai\",\n      \"baseUrl\": \"https://api.openai.com/v1\",\n      \"apiKey\": \"$OPENAI_API_KEY\",\n      \"models\": [\"gpt-4\", \"gpt-3.5-turbo\"]\n    },\n    {\n      \"name\": \"anthropic\",\n      \"baseUrl\": \"https://api.anthropic.com/v1\",\n      \"apiKey\": \"$ANTHROPIC_API_KEY\",\n      \"models\": [\"claude-3-5-sonnet-20241022\", \"claude-3-opus-20240229\"]\n    }\n  ],\n  \"Router\": {\n    \"default\": \"openai,gpt-4\",\n    \"think\": \"anthropic,claude-3-5-sonnet-20241022\",\n    \"background\": \"openai,gpt-3.5-turbo\"\n  }\n}\n```\n"
  },
  {
    "path": "docs/docs/cli/config/project-level.md",
    "content": "---\ntitle: Project-Level Configuration\n---\n\n# Project-Level Configuration\n\nIn addition to global configuration, `ccr` also supports setting different routing rules for specific projects.\n\n## Project Configuration File\n\nProject configuration file is located at:\n\n```\n~/.claude/projects/<project-id>/claude-code-router.json\n```\n\nWhere `<project-id>` is the unique identifier of the Claude Code project.\n\n## Project Configuration Structure\n\n```json5\n{\n  \"Router\": {\n    \"default\": \"openai,gpt-4\",\n    \"background\": \"openai,gpt-3.5-turbo\"\n  }\n}\n```\n\n## Finding Project ID\n\n### Method 1: Using CLI\n\n```bash\n# Run in project directory\nccr status\n```\n\nOutput will show current project ID:\n\n```\nProject: my-project (abc123def456)\n```\n\n### Method 2: Check Claude Code Configuration\n\n```bash\ncat ~/.claude.json\n```\n\nFind your project ID:\n\n```json\n{\n  \"projects\": {\n    \"abc123def456\": {\n      \"path\": \"/path/to/your/project\",\n      \"name\": \"my-project\"\n    }\n  }\n}\n```\n\n## Creating Project Configuration\n\n### Manual Creation\n\n```bash\n# Create project configuration directory\nmkdir -p ~/.claude/projects/abc123def456\n\n# Create configuration file\ncat > ~/.claude/projects/abc123def456/claude-code-router.json << 'EOF'\n{\n  \"Router\": {\n    \"default\": \"anthropic,claude-3-5-sonnet-20241022\",\n    \"background\": \"openai,gpt-3.5-turbo\"\n  }\n}\nEOF\n```\n\n### Using ccr model Command\n\n```bash\n# Run in project directory\ncd /path/to/your/project\nccr model --project\n```\n\n## Configuration Priority\n\nRouting configuration priority (from high to low):\n\n1. **Custom routing function** (`CUSTOM_ROUTER_PATH`)\n2. **Project-level configuration** (`~/.claude/projects/<id>/claude-code-router.json`)\n3. **Global configuration** (`~/.claude-code-router/config.json`)\n4. **Built-in routing rules**\n\n## Use Cases\n\n### Scenario 1: Different Projects Use Different Models\n\n```json5\n// Web project uses GPT-4\n~/.claude/projects/web-project-id/claude-code-router.json:\n{\n  \"Router\": {\n    \"default\": \"openai,gpt-4\"\n  }\n}\n\n// AI project uses Claude\n~/.claude/projects/ai-project-id/claude-code-router.json:\n{\n  \"Router\": {\n    \"default\": \"anthropic,claude-3-5-sonnet-20241022\"\n  }\n}\n```\n\n### Scenario 2: Test Projects Use Low-Cost Models\n\n```json5\n~/.claude/projects/test-project-id/claude-code-router.json:\n{\n  \"Router\": {\n    \"default\": \"openai,gpt-3.5-turbo\",\n    \"background\": \"openai,gpt-3.5-turbo\"\n  }\n}\n```\n\n### Scenario 3: Long Context Projects\n\n```json5\n~/.claude/projects/long-context-project-id/claude-code-router.json:\n{\n  \"Router\": {\n    \"default\": \"anthropic,claude-3-opus-20240229\",\n    \"longContext\": \"anthropic,claude-3-opus-20240229\"\n  }\n}\n```\n\n## Verify Project Configuration\n\n```bash\n# View routing used by current project\nccr status\n\n# Check logs to confirm routing decisions\ntail -f ~/.claude-code-router/claude-code-router.log\n```\n\n## Delete Project Configuration\n\n```bash\nrm ~/.claude/projects/<project-id>/claude-code-router.json\n```\n\nAfter deletion, falls back to global configuration.\n\n## Complete Example\n\nAssume you have two projects:\n\n### Global Configuration (`~/.claude-code-router/config.json`)\n\n```json5\n{\n  \"Providers\": [\n    {\n      \"name\": \"openai\",\n      \"baseUrl\": \"https://api.openai.com/v1\",\n      \"apiKey\": \"$OPENAI_API_KEY\",\n      \"models\": [\"gpt-4\", \"gpt-3.5-turbo\"]\n    },\n    {\n      \"name\": \"anthropic\",\n      \"baseUrl\": \"https://api.anthropic.com/v1\",\n      \"apiKey\": \"$ANTHROPIC_API_KEY\",\n      \"models\": [\"claude-3-5-sonnet-20241022\"]\n    }\n  ],\n  \"Router\": {\n    \"default\": \"openai,gpt-4\",\n    \"background\": \"openai,gpt-3.5-turbo\"\n  }\n}\n```\n\n### Web Project Configuration\n\n```json5\n{\n  \"Router\": {\n    \"default\": \"openai,gpt-4\"\n  }\n}\n```\n\n### AI Project Configuration\n\n```json5\n{\n  \"Router\": {\n    \"default\": \"anthropic,claude-3-5-sonnet-20241022\",\n    \"think\": \"anthropic,claude-3-5-sonnet-20241022\"\n  }\n}\n```\n\nThis way:\n- Web project uses GPT-4\n- AI project uses Claude\n- All projects' background tasks use GPT-3.5-turbo (inherit global configuration)\n"
  },
  {
    "path": "docs/docs/cli/installation.md",
    "content": "---\nsidebar_position: 2\n---\n\n# Installation\n\nInstall Claude Code Router globally using your preferred package manager.\n\n## Prerequisites\n\n- **Node.js**: >= 18.0.0\n- **pnpm**: >= 8.0.0 (if using pnpm)\n- An API key from your preferred LLM provider\n\n## Install via npm\n\n```bash\nnpm install -g @musistudio/claude-code-router\n```\n\n## Install via pnpm\n\n```bash\npnpm add -g @musistudio/claude-code-router\n```\n\n## Install via Yarn\n\n```bash\nyarn global add @musistudio/claude-code-router\n```\n\n## Verify Installation\n\nAfter installation, verify that `ccr` is available:\n\n```bash\nccr --version\n```\n\nYou should see the version number displayed.\n\n## Next Steps\n\nOnce installed, proceed to [Quick Start](/docs/quick-start) to configure and start using the router.\n"
  },
  {
    "path": "docs/docs/cli/intro.md",
    "content": "---\ntitle: CLI Introduction\n---\n\n# CLI Introduction\n\nClaude Code Router CLI (`ccr`) is a command-line tool for managing and controlling the Claude Code Router service.\n\n## Feature Overview\n\n`ccr` provides the following functionality:\n\n- **Service Management**: Start, stop, restart service\n- **Configuration Management**: Interactive model selection configuration\n- **Status Viewing**: View service running status\n- **Code Execution**: Directly execute `claude` command\n- **Environment Integration**: Output environment variables for shell integration\n- **Web UI**: Open Web management interface\n- **Status Bar**: Display customizable session status with `ccr statusline`\n\n## Installation\n\n```bash\nnpm install -g @musistudio/claude-code-router\n```\n\n## Basic Usage\n\n### Configuration\n\nBefore using Claude Code Router, you need to configure your providers. You can either:\n\n1. **Edit configuration file directly**: Edit `~/.claude-code-router/config.json` manually\n2. **Use Web UI**: Run `ccr ui` to open the web interface and configure visually\n\nAfter making configuration changes, restart the service:\n\n```bash\nccr restart\n```\n\nOr restart directly through the Web UI.\n\n### Start Claude Code\n\nOnce configured, you can start Claude Code with:\n\n```bash\nccr code\n```\n\nThis will launch Claude Code and route your requests through the configured provider.\n\n### Service Management\n\n```bash\nccr start    # Start the router service\nccr status   # View service status\nccr stop     # Stop the router service\nccr restart  # Restart the router service\n```\n\n### Web UI\n\n```bash\nccr ui       # Open Web management interface\n```\n\n## Configuration File\n\n`ccr` uses the configuration file at `~/.claude-code-router/config.json`\n\nConfigure once, and both CLI and Server will use it.\n\n## Next Steps\n\n- [Installation Guide](/docs/cli/installation) - Detailed installation instructions\n- [Quick Start](/docs/cli/quick-start) - Get started in 5 minutes\n- [Command Reference](/docs/category/cli-commands) - Complete command list\n- [Status Line](/docs/cli/commands/statusline) - Customize your status bar\n- [Configuration Guide](/docs/category/cli-config) - Configuration file details\n"
  },
  {
    "path": "docs/docs/cli/quick-start.md",
    "content": "---\nsidebar_position: 3\n---\n\n# Quick Start\n\nGet up and running with Claude Code Router in 5 minutes.\n\n## 1. Configure the Router\n\nBefore using Claude Code Router, you need to configure your LLM providers. You can either:\n\n### Option A: Edit Configuration File Directly\n\nEdit `~/.claude-code-router/config.json`:\n\n```json\n{\n  \"HOST\": \"0.0.0.0\",\n  \"PORT\": 8080,\n  \"Providers\": [\n    {\n      \"name\": \"openai\",\n      \"api_base_url\": \"https://api.openai.com/v1/chat/completions\",\n      \"api_key\": \"your-api-key-here\",\n      \"models\": [\"gpt-4\", \"gpt-3.5-turbo\"]\n    }\n  ],\n  \"Router\": {\n    \"default\": \"openai,gpt-4\"\n  }\n}\n```\n\n### Option B: Use Web UI\n\n```bash\nccr ui\n```\n\nThis will open the web interface where you can configure providers visually.\n\n## 2. Start the Router\n\n```bash\nccr start\n```\n\nThe router will start on `http://localhost:8080` by default.\n\n## 3. Use Claude Code\n\nNow you can use Claude Code normally:\n\n```bash\nccr code\n```\n\nYour requests will be routed through Claude Code Router to your configured provider.\n\n## Restart After Configuration Changes\n\nIf you modify the configuration file or make changes through the Web UI, restart the service:\n\n```bash\nccr restart\n```\n\nOr restart directly through the Web UI.\n\n## What's Next?\n\n- [Basic Configuration](/docs/cli/config/basic) - Learn about configuration options\n- [Routing](/docs/cli/config/routing) - Configure smart routing rules\n- [CLI Commands](/docs/category/cli-commands) - Explore all CLI commands\n"
  },
  {
    "path": "docs/docs/presets/intro.md",
    "content": "---\nsidebar_position: 3\n---\n\n# Presets\n\nUse predefined configurations for quick setup.\n\n## What are Presets?\n\nPresets are pre-configured settings that include provider configurations, routing rules, and transformers optimized for specific use cases.\n\n## Using Presets\n\n### CLI Mode (Command Line)\n\nCLI mode is suitable for developers who prefer command-line operations.\n\n#### Installing Presets\n\n**Install from local directory:**\n\n```bash\nccr preset install /path/to/preset-directory\n```\n\n**Reconfigure an installed preset:**\n\n```bash\nccr preset install my-preset\n```\n\n#### Using Presets\n\nAfter installing a preset, you can use the preset name to start Claude Code:\n\n```bash\n# Start with a specific preset\nccr my-preset \"your prompt\"\n```\n\nThe preset will:\n- Automatically load pre-configured Providers\n- Apply preset routing rules\n- Use transformers configured in the preset\n\n#### List All Presets\n\n```bash\nccr preset list\n```\n\nThis will display all installed presets with their names, versions, and descriptions.\n\n#### View Preset Information\n\n```bash\nccr preset info my-preset\n```\n\n#### Delete Preset\n\n```bash\nccr preset delete my-preset\n```\n\n### Web UI Mode\n\nWeb UI provides a more friendly visual interface with additional installation methods.\n\n#### Access Web UI\n\n```bash\nccr ui\n```\n\nThen open `http://localhost:3000` in your browser.\n\n#### Install from GitHub Repository\n\n1. Click the \"Preset Market\" button\n2. Select the preset you want to install from the list\n3. Click the \"Install\" button\n\n#### Reconfigure Preset\n\n1. Click the \"View Details\" icon next to the preset\n2. Modify configuration items in the detail page\n3. Click \"Apply\" to save configuration\n\n#### Manage Presets\n\n- **View**: Click the info icon on the right side of the preset\n- **Delete**: Click the delete icon on the right side of the preset\n\n## Creating Custom Presets\n\n### Preset Directory Structure\n\nPresets are stored as directories with the following structure:\n\n```\n~/.claude-code-router/presets/<preset-name>/\n├── manifest.json           # Required: Preset configuration file\n├── transformers/           # Optional: Custom transformers\n│   └── custom-transformer.js\n├── scripts/               # Optional: Custom scripts\n│   └── status.js\n└── README.md              # Optional: Documentation\n```\n\n### Dynamic Configuration System\n\nCCR introduces a powerful dynamic configuration system that supports:\n\n- **Multiple Input Types**: Selectors, multi-select, confirm boxes, text input, number input, etc.\n- **Conditional Logic**: Dynamically show/hide configuration fields based on user input\n- **Variable References**: Configuration fields can reference each other\n- **Dynamic Options**: Option lists can be dynamically generated from preset configuration or user input\n\n#### Schema Field Types\n\n| Type | Description | Example |\n|------|-------------|---------|\n| `password` | Password input (hidden) | API Key |\n| `input` | Single-line text input | Base URL |\n| `number` | Number input | Max tokens |\n| `select` | Single-select dropdown | Choose Provider |\n| `multiselect` | Multi-select | Enable features |\n| `confirm` | Confirmation box | Use proxy |\n| `editor` | Multi-line text editor | Custom config |\n\n#### Condition Operators\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `eq` | Equals | `{\"field\": \"provider\", \"operator\": \"eq\", \"value\": \"openai\"}` |\n| `ne` | Not equals | `{\"field\": \"advanced\", \"operator\": \"ne\", \"value\": true}` |\n| `in` | In (array) | `{\"field\": \"feature\", \"operator\": \"in\", \"value\": [\"a\", \"b\"]}` |\n| `nin` | Not in (array) | `{\"field\": \"type\", \"operator\": \"nin\", \"value\": [\"x\", \"y\"]}` |\n| `exists` | Field exists | `{\"field\": \"apiKey\", \"operator\": \"exists\"}` |\n| `gt/lt/gte/lte` | Greater/less than (or equal) | For number comparisons |\n\n#### Dynamic Options Types\n\n##### static - Static Options\n```json\n\"options\": {\n  \"type\": \"static\",\n  \"options\": [\n    {\"label\": \"Option 1\", \"value\": \"value1\"},\n    {\"label\": \"Option 2\", \"value\": \"value2\"}\n  ]\n}\n```\n\n##### providers - Extract from Providers Configuration\n```json\n\"options\": {\n  \"type\": \"providers\"\n}\n```\nAutomatically extracts names from the `Providers` array as options.\n\n##### models - Extract from Specified Provider's Models\n```json\n\"options\": {\n  \"type\": \"models\",\n  \"providerField\": \"{{selectedProvider}}\"\n}\n```\nDynamically displays models based on the user-selected provider.\n\n#### Template Variables\n\nUse `{{variableName}}` syntax to reference user input in the template:\n\n```json\n\"template\": {\n  \"Providers\": [\n    {\n      \"name\": \"{{providerName}}\",\n      \"api_key\": \"{{apiKey}}\"\n    }\n  ]\n}\n```\n\n#### Configuration Mappings\n\nFor complex configuration needs, use `configMappings` to precisely control value placement:\n\n```json\n\"configMappings\": [\n  {\n    \"target\": \"Providers[0].api_key\",\n    \"value\": \"{{apiKey}}\"\n  },\n  {\n    \"target\": \"PROXY_URL\",\n    \"value\": \"{{proxyUrl}}\",\n    \"when\": {\n      \"field\": \"useProxy\",\n      \"operator\": \"eq\",\n      \"value\": true\n    }\n  }\n]\n```\n\n#### Complete Example\n\n```json\n{\n  \"name\": \"multi-provider-example\",\n  \"version\": \"1.0.0\",\n  \"description\": \"Multi-provider configuration example - Switch between OpenAI and DeepSeek\",\n  \"author\": \"CCR Team\",\n  \"keywords\": [\"openai\", \"deepseek\", \"multi-provider\"],\n  \"ccrVersion\": \"2.0.0\",\n  \"schema\": [\n    {\n      \"id\": \"primaryProvider\",\n      \"type\": \"select\",\n      \"label\": \"Primary Provider\",\n      \"prompt\": \"Select your primary LLM provider\",\n      \"options\": {\n        \"type\": \"static\",\n        \"options\": [\n          {\n            \"label\": \"OpenAI\",\n            \"value\": \"openai\",\n            \"description\": \"Use OpenAI's GPT models\"\n          },\n          {\n            \"label\": \"DeepSeek\",\n            \"value\": \"deepseek\",\n            \"description\": \"Use DeepSeek's cost-effective models\"\n          }\n        ]\n      },\n      \"required\": true,\n      \"defaultValue\": \"openai\"\n    },\n    {\n      \"id\": \"apiKey\",\n      \"type\": \"password\",\n      \"label\": \"API Key\",\n      \"prompt\": \"Enter your API Key\",\n      \"placeholder\": \"sk-...\",\n      \"required\": true\n    },\n    {\n      \"id\": \"defaultModel\",\n      \"type\": \"select\",\n      \"label\": \"Default Model\",\n      \"prompt\": \"Select the default model to use\",\n      \"options\": {\n        \"type\": \"static\",\n        \"options\": [\n          {\"label\": \"GPT-4o\", \"value\": \"gpt-4o\"},\n          {\"label\": \"GPT-4o-mini\", \"value\": \"gpt-4o-mini\"}\n        ]\n      },\n      \"required\": true,\n      \"defaultValue\": \"gpt-4o\",\n      \"when\": {\n        \"field\": \"primaryProvider\",\n        \"operator\": \"eq\",\n        \"value\": \"openai\"\n      }\n    },\n    {\n      \"id\": \"enableProxy\",\n      \"type\": \"confirm\",\n      \"label\": \"Enable Proxy\",\n      \"prompt\": \"Access API through a proxy?\",\n      \"defaultValue\": false\n    },\n    {\n      \"id\": \"proxyUrl\",\n      \"type\": \"input\",\n      \"label\": \"Proxy URL\",\n      \"prompt\": \"Enter proxy server address\",\n      \"placeholder\": \"http://127.0.0.1:7890\",\n      \"required\": true,\n      \"when\": {\n        \"field\": \"enableProxy\",\n        \"operator\": \"eq\",\n        \"value\": true\n      }\n    }\n  ],\n  \"template\": {\n    \"Providers\": [\n      {\n        \"name\": \"{{primaryProvider}}\",\n        \"api_base_url\": \"https://api.openai.com/v1/chat/completions\",\n        \"api_key\": \"{{apiKey}}\",\n        \"models\": [\"{{defaultModel}}\"]\n      }\n    ],\n    \"Router\": {\n      \"default\": \"{{primaryProvider}},{{defaultModel}}\"\n    },\n    \"PROXY_URL\": \"{{proxyUrl}}\"\n  },\n  \"configMappings\": [\n    {\n      \"target\": \"PROXY_URL\",\n      \"value\": \"{{proxyUrl}}\",\n      \"when\": {\n        \"field\": \"enableProxy\",\n        \"operator\": \"eq\",\n        \"value\": true\n      }\n    }\n  ]\n}\n```\n\n### manifest.json Complete Field Reference\n\n`manifest.json` is the core configuration file of a preset, using JSON5 format (comments supported).\n\n#### 1. Metadata Fields\n\nThese fields describe basic information about the preset:\n\n| Field | Type | Required | Description |\n|-------|------|----------|-------------|\n| `name` | string | ✓ | Preset name (unique identifier) |\n| `version` | string | ✓ | Version number (follows semver) |\n| `description` | string | - | Preset description |\n| `author` | string | - | Author information |\n| `homepage` | string | - | Project homepage URL |\n| `repository` | string | - | Source repository URL |\n| `license` | string | - | License type |\n| `keywords` | string[] | - | Keyword tags |\n| `ccrVersion` | string | - | Compatible CCR version |\n\nExample:\n\n```json\n{\n  \"name\": \"my-preset\",\n  \"version\": \"1.0.0\",\n  \"description\": \"My custom preset\",\n  \"author\": \"Your Name\",\n  \"homepage\": \"https://github.com/yourname/ccr-presets\",\n  \"repository\": \"https://github.com/yourname/ccr-presets.git\",\n  \"license\": \"MIT\",\n  \"keywords\": [\"openai\", \"production\"],\n  \"ccrVersion\": \"2.0.0\"\n}\n```\n\n#### 2. Configuration Fields\n\nThese fields are directly merged into CCR's configuration. All fields supported in `config.json` can be used here:\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `Providers` | array | Provider configuration array |\n| `Router` | object | Routing configuration |\n| `transformers` | array | Transformer configuration |\n| `StatusLine` | object | Status bar configuration |\n| `NON_INTERACTIVE_MODE` | boolean | Enable non-interactive mode (for CI/CD) |\n\n**CLI-Only Fields** (these fields only work in CLI mode and are not used by the server):\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `noServer` | boolean | Skip local server startup and use provider's API directly |\n| `claudeCodeSettings` | object | Claude Code specific settings (env, statusLine, etc.) |\n\nExample:\n\n```json\n{\n  \"Providers\": [\n    {\n      \"name\": \"openai\",\n      \"api_base_url\": \"https://api.openai.com/v1/chat/completions\",\n      \"api_key\": \"${OPENAI_API_KEY}\",\n      \"models\": [\"gpt-4o\", \"gpt-4o-mini\"]\n    }\n  ],\n  \"Router\": {\n    \"default\": \"openai,gpt-4o\",\n    \"background\": \"openai,gpt-4o-mini\"\n  },\n  \"PORT\": 8080\n}\n```\n\n#### 3. Dynamic Configuration System Fields\n\nThese fields are used to create interactive configuration templates:\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `schema` | array | Configuration input form definition |\n| `template` | object | Configuration template (with variable references) |\n| `configMappings` | array | Configuration mapping rules |\n| `userValues` | object | User-filled values (used at runtime) |\n\n**Schema Field Types:**\n\n| Type | Description | Use Case |\n|------|-------------|----------|\n| `password` | Password input (hidden) | API Key |\n| `input` | Single-line text input | URL |\n| `number` | Number input | Port number |\n| `select` | Single-select dropdown | Select Provider |\n| `multiselect` | Multi-select | Enable features |\n| `confirm` | Confirmation box | Enable/disable |\n| `editor` | Multi-line text editor | Custom config |\n\nDynamic configuration example:\n\n```json\n{\n  \"schema\": [\n    {\n      \"id\": \"apiKey\",\n      \"type\": \"password\",\n      \"label\": \"API Key\",\n      \"prompt\": \"Enter your API Key\",\n      \"required\": true\n    },\n    {\n      \"id\": \"provider\",\n      \"type\": \"select\",\n      \"label\": \"Provider\",\n      \"options\": {\n        \"type\": \"static\",\n        \"options\": [\n          {\"label\": \"OpenAI\", \"value\": \"openai\"},\n          {\"label\": \"DeepSeek\", \"value\": \"deepseek\"}\n        ]\n      },\n      \"defaultValue\": \"openai\"\n    }\n  ],\n  \"template\": {\n    \"Providers\": [\n      {\n        \"name\": \"#{provider}\",\n        \"api_key\": \"#{apiKey}\"\n      }\n    ]\n  }\n}\n```\n\n### Creating Preset Examples\n\n#### Example 1: Simple Preset (No Dynamic Configuration)\n\n```bash\n# Create preset directory\nmkdir -p ~/.claude-code-router/presets/simple-openai\n\n# Create manifest.json\ncat > ~/.claude-code-router/presets/simple-openai/manifest.json << 'EOF'\n{\n  \"name\": \"simple-openai\",\n  \"version\": \"1.0.0\",\n  \"description\": \"Simple OpenAI configuration\",\n  \"author\": \"Your Name\",\n\n  \"Providers\": [\n    {\n      \"name\": \"openai\",\n      \"api_base_url\": \"https://api.openai.com/v1/chat/completions\",\n      \"api_key\": \"${OPENAI_API_KEY}\",\n      \"models\": [\"gpt-4o\", \"gpt-4o-mini\"]\n    }\n  ],\n\n  \"Router\": {\n    \"default\": \"openai,gpt-4o\",\n    \"background\": \"openai,gpt-4o-mini\"\n  }\n}\nEOF\n\n# Configure preset (input API Key)\nccr preset install simple-openai\n\n# Use preset\nccr simple-openai \"your prompt\"\n```\n\n#### Example 2: Advanced Preset (Dynamic Configuration)\n\n```bash\n# Create preset directory\nmkdir -p ~/.claude-code-router/presets/advanced-config\n\n# Create manifest.json\ncat > ~/.claude-code-router/presets/advanced-config/manifest.json << 'EOF'\n{\n  \"name\": \"advanced-config\",\n  \"version\": \"1.0.0\",\n  \"description\": \"Advanced configuration with multi-provider support\",\n  \"author\": \"Your Name\",\n  \"keywords\": [\"openai\", \"deepseek\", \"multi-provider\"],\n\n  \"schema\": [\n    {\n      \"id\": \"provider\",\n      \"type\": \"select\",\n      \"label\": \"Select Provider\",\n      \"prompt\": \"Choose your primary LLM provider\",\n      \"options\": {\n        \"type\": \"static\",\n        \"options\": [\n          {\n            \"label\": \"OpenAI\",\n            \"value\": \"openai\",\n            \"description\": \"Use OpenAI's GPT models\"\n          },\n          {\n            \"label\": \"DeepSeek\",\n            \"value\": \"deepseek\",\n            \"description\": \"Use DeepSeek's cost-effective models\"\n          }\n        ]\n      },\n      \"defaultValue\": \"openai\",\n      \"required\": true\n    },\n    {\n      \"id\": \"apiKey\",\n      \"type\": \"password\",\n      \"label\": \"API Key\",\n      \"prompt\": \"Enter your API Key\",\n      \"placeholder\": \"sk-...\",\n      \"required\": true\n    },\n    {\n      \"id\": \"enableProxy\",\n      \"type\": \"confirm\",\n      \"label\": \"Enable Proxy\",\n      \"prompt\": \"Access API through a proxy?\",\n      \"defaultValue\": false\n    },\n    {\n      \"id\": \"proxyUrl\",\n      \"type\": \"input\",\n      \"label\": \"Proxy URL\",\n      \"prompt\": \"Enter proxy server address\",\n      \"placeholder\": \"http://127.0.0.1:7890\",\n      \"required\": true,\n      \"when\": {\n        \"field\": \"enableProxy\",\n        \"operator\": \"eq\",\n        \"value\": true\n      }\n    }\n  ],\n\n  \"template\": {\n    \"Providers\": [\n      {\n        \"name\": \"#{provider}\",\n        \"api_base_url\": \"#{provider === 'openai' ? 'https://api.openai.com/v1/chat/completions' : 'https://api.deepseek.com/v1/chat/completions'}\",\n        \"api_key\": \"#{apiKey}\",\n        \"models\": [\"gpt-4o\", \"gpt-4o-mini\"]\n      }\n    ],\n    \"Router\": {\n      \"default\": \"#{provider},gpt-4o\",\n      \"background\": \"#{provider},gpt-4o-mini\"\n    }\n  },\n\n  \"configMappings\": [\n    {\n      \"target\": \"PROXY_URL\",\n      \"value\": \"#{proxyUrl}\",\n      \"when\": {\n        \"field\": \"enableProxy\",\n        \"operator\": \"eq\",\n        \"value\": true\n      }\n    }\n  ]\n}\nEOF\n\n# Configure preset (will prompt for input)\nccr preset install advanced-config\n\n# Use preset\nccr advanced-config \"your prompt\"\n```\n\n### Export Current Configuration as Preset\n\nIf you have already configured CCR, you can export the current configuration:\n\n```bash\n# Export current configuration\nccr preset export my-exported-preset\n```\n\nExport will automatically:\n- Identify sensitive fields (like `api_key`) and replace with environment variable placeholders\n- Generate `schema` for collecting user input\n- Generate `template` and `configMappings`\n\nOptional flags:\n\n```bash\nccr preset export my-exported-preset \\\n  --description \"Exported configuration\" \\\n  --author \"Your Name\" \\\n  --tags \"production,openai\"\n```\n\n## Preset File Location\n\nPresets are stored in:\n\n```\n~/.claude-code-router/presets/\n```\n\nEach preset is a directory containing a `manifest.json` file.\n\n## Best Practices\n\n1. **Use Dynamic Configuration**: Use the schema system for configuration items that require user input\n2. **Provide Defaults**: Set reasonable defaults for optional fields\n3. **Conditional Display**: Use `when` conditions to avoid unnecessary inputs\n4. **Clear Labels**: Provide clear `label` and `prompt` for each field\n5. **Validate Input**: Use `validator` to ensure input validity\n6. **Version Control**: Keep commonly used presets in version control\n7. **Document**: Add descriptions and version info for custom presets\n\n## Next Steps\n\n- [CLI Reference](/docs/cli/start) - Complete CLI command reference\n- [Configuration](/docs/config/basic) - Detailed configuration guide\n"
  },
  {
    "path": "docs/docs/server/advanced/custom-router.md",
    "content": "---\nsidebar_position: 1\n---\n\n# Custom Router\n\nWrite your own routing logic in JavaScript.\n\n## Creating a Custom Router\n\nCreate a JavaScript file that exports a routing function:\n\n```javascript\n// custom-router.js\nmodule.exports = function(config, context) {\n  const { scenario, projectId, tokenCount, request } = context;\n\n  // Your custom logic here\n  if (scenario === 'background') {\n    return 'groq,llama-3.3-70b-versatile';\n  }\n\n  if (tokenCount > 100000) {\n    return 'gemini,gemini-1.5-pro';\n  }\n\n  // Check request content\n  if (request && request.system && request.system.includes('code')) {\n    return 'deepseek,deepseek-coder';\n  }\n\n  // Default\n  return 'deepseek,deepseek-chat';\n};\n```\n\n## Context Object\n\nThe router function receives a context object with:\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `scenario` | string | Detected scenario (background, think, webSearch, image, etc.) |\n| `projectId` | string | Project ID from Claude Code |\n| `tokenCount` | number | Estimated token count of the request |\n| `request` | object | Full request object |\n\n## Configuration\n\nSet the environment variable to use your custom router:\n\n```bash\nexport CUSTOM_ROUTER_PATH=\"/path/to/custom-router.js\"\n```\n\nOr set it in your shell configuration:\n\n```bash\n# ~/.bashrc or ~/.zshrc\nexport CUSTOM_ROUTER_PATH=\"/path/to/custom-router.js\"\n```\n\n## Return Format\n\nThe router function should return a string in the format:\n\n```\n{provider-name},{model-name}\n```\n\nExample:\n\n```\ndeepseek,deepseek-chat\n```\n\n## Error Handling\n\nIf your router function throws an error or returns an invalid format, the router will fall back to the default routing configuration.\n\n## Example: Time-Based Routing\n\n```javascript\nmodule.exports = function(config, context) {\n  const hour = new Date().getHours();\n\n  // Use faster models during work hours\n  if (hour >= 9 && hour <= 18) {\n    return 'groq,llama-3.3-70b-versatile';\n  }\n\n  // Use more capable models outside work hours\n  return 'deepseek,deepseek-chat';\n};\n```\n\n## Example: Cost Optimization\n\n```javascript\nmodule.exports = function(config, context) {\n  const { tokenCount } = context;\n\n  // Use cheaper models for large requests\n  if (tokenCount > 50000) {\n    return 'groq,llama-3.3-70b-versatile';\n  }\n\n  // Use default for smaller requests\n  return 'deepseek,deepseek-chat';\n};\n```\n\n## Testing Your Router\n\nTest your custom router by checking the logs:\n\n```bash\ntail -f ~/.claude-code-router/claude-code-router.log\n```\n\nLook for routing decisions to see which model is being selected.\n\n## Next Steps\n\n- [Agents](/docs/advanced/agents) - Extend functionality with agents\n- [Presets](/docs/advanced/presets) - Use predefined configurations\n"
  },
  {
    "path": "docs/docs/server/api/config-api.md",
    "content": "---\ntitle: Configuration API\n---\n\n# Configuration API\n\n## GET /api/config\n\nGet current server configuration.\n\n### Request Example\n\n```bash\ncurl http://localhost:3456/api/config \\\n  -H \"x-api-key: your-api-key\"\n```\n\n### Response Example\n\n```json\n{\n  \"HOST\": \"0.0.0.0\",\n  \"PORT\": 3456,\n  \"APIKEY\": \"sk-xxxxx\",\n  \"Providers\": [\n    {\n      \"name\": \"openai\",\n      \"baseUrl\": \"https://api.openai.com/v1\",\n      \"apiKey\": \"sk-...\",\n      \"models\": [\"gpt-4\", \"gpt-3.5-turbo\"]\n    }\n  ],\n  \"Router\": {\n    \"default\": \"openai,gpt-4\"\n  },\n  \"transformers\": [\n    \"anthropic\"\n  ]\n}\n```\n\n## POST /api/config\n\nUpdate server configuration. Old configuration is automatically backed up before updating.\n\n### Request Example\n\n```bash\ncurl -X POST http://localhost:3456/api/config \\\n  -H \"x-api-key: your-api-key\" \\\n  -H \"content-type: application/json\" \\\n  -d '{\n    \"HOST\": \"0.0.0.0\",\n    \"PORT\": 3456,\n    \"Providers\": [\n      {\n        \"name\": \"openai\",\n        \"baseUrl\": \"https://api.openai.com/v1\",\n        \"apiKey\": \"$OPENAI_API_KEY\",\n        \"models\": [\"gpt-4\"]\n      }\n    ],\n    \"Router\": {\n      \"default\": \"openai,gpt-4\"\n    }\n  }'\n```\n\n### Configuration Object Structure\n\n#### Basic Configuration\n\n| Field | Type | Required | Description |\n|-------|------|----------|-------------|\n| `HOST` | string | No | Listen address (default 127.0.0.1) |\n| `PORT` | integer | No | Listen port (default 3456) |\n| `APIKEY` | string | No | API key |\n| `LOG` | boolean | No | Enable logging (default true) |\n| `LOG_LEVEL` | string | No | Log level (debug/info/warn/error) |\n\n#### Providers Configuration\n\n```json\n{\n  \"Providers\": [\n    {\n      \"name\": \"provider-name\",\n      \"baseUrl\": \"https://api.example.com/v1\",\n      \"apiKey\": \"your-api-key\",\n      \"models\": [\"model-1\", \"model-2\"]\n    }\n  ]\n}\n```\n\n| Field | Type | Required | Description |\n|-------|------|----------|-------------|\n| `name` | string | Yes | Provider name |\n| `baseUrl` | string | Yes | API base URL |\n| `apiKey` | string | Yes | API key |\n| `models` | array | Yes | List of supported models |\n\n#### Router Configuration\n\n```json\n{\n  \"Router\": {\n    \"default\": \"provider,model\",\n    \"longContextThreshold\": 100000,\n    \"routes\": {\n      \"background\": \"lightweight-model\",\n      \"think\": \"powerful-model\",\n      \"longContext\": \"long-context-model\",\n      \"webSearch\": \"search-model\",\n      \"image\": \"vision-model\"\n    }\n  }\n}\n```\n\n#### Transformers Configuration\n\n```json\n{\n  \"transformers\": [\n    {\n      \"name\": \"anthropic\",\n      \"provider\": \"provider-name\",\n      \"models\": [\"model-1\"],\n      \"options\": {}\n    }\n  ]\n}\n```\n\n### Response Example\n\nSuccess:\n\n```json\n{\n  \"success\": true,\n  \"message\": \"Config saved successfully\"\n}\n```\n\n### Configuration Backup\n\nEvery time configuration is updated, old configuration is automatically backed up to:\n\n```\n~/.claude-code-router/config.backup.{timestamp}.json\n```\n\nKeeps the last 3 backups.\n\n## GET /api/transformers\n\nGet list of all transformers loaded by the server.\n\n### Request Example\n\n```bash\ncurl http://localhost:3456/api/transformers \\\n  -H \"x-api-key: your-api-key\"\n```\n\n### Response Example\n\n```json\n{\n  \"transformers\": [\n    {\n      \"name\": \"anthropic\",\n      \"endpoint\": null\n    },\n    {\n      \"name\": \"openai\",\n      \"endpoint\": null\n    },\n    {\n      \"name\": \"gemini\",\n      \"endpoint\": \"https://generativelanguage.googleapis.com\"\n    }\n  ]\n}\n```\n\n### Transformer List\n\nBuilt-in transformers:\n\n- `anthropic` - Anthropic Claude format\n- `openai` - OpenAI format\n- `deepseek` - DeepSeek format\n- `gemini` - Google Gemini format\n- `openrouter` - OpenRouter format\n- `groq` - Groq format\n- `maxtoken` - Adjust max_tokens parameter\n- `tooluse` - Tool use conversion\n- `reasoning` - Reasoning mode conversion\n- `enhancetool` - Enhance tool functionality\n\n## Environment Variable Interpolation\n\nConfiguration supports environment variable interpolation:\n\n```json\n{\n  \"Providers\": [\n    {\n      \"apiKey\": \"$OPENAI_API_KEY\"\n    }\n  ]\n}\n```\n\nOr use `${VAR_NAME}` format:\n\n```json\n{\n  \"baseUrl\": \"${API_BASE_URL}\"\n}\n```\n"
  },
  {
    "path": "docs/docs/server/api/logs-api.md",
    "content": "---\ntitle: Logs API\n---\n\n# Logs API\n\n## GET /api/logs/files\n\nGet list of all available log files.\n\n### Request Example\n\n```bash\ncurl http://localhost:3456/api/logs/files \\\n  -H \"x-api-key: your-api-key\"\n```\n\n### Response Example\n\n```json\n[\n  {\n    \"name\": \"ccr-20241226143022.log\",\n    \"path\": \"/home/user/.claude-code-router/logs/ccr-20241226143022.log\",\n    \"size\": 1024000,\n    \"lastModified\": \"2024-12-26T14:30:22.000Z\"\n  },\n  {\n    \"name\": \"ccr-20241226143021.log\",\n    \"path\": \"/home/user/.claude-code-router/logs/ccr-20241226143021.log\",\n    \"size\": 980000,\n    \"lastModified\": \"2024-12-26T14:30:21.000Z\"\n  }\n]\n```\n\n### Field Description\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `name` | string | File name |\n| `path` | string | Complete file path |\n| `size` | integer | File size (bytes) |\n| `lastModified` | string | Last modification time (ISO 8601) |\n\nFiles are sorted by modification time in descending order.\n\n## GET /api/logs\n\nGet content of specified log file.\n\n### Query Parameters\n\n| Parameter | Type | Required | Description |\n|-----------|------|----------|-------------|\n| `file` | string | No | Log file path (default uses app.log) |\n\n### Request Example (Get Default Log)\n\n```bash\ncurl \"http://localhost:3456/api/logs\" \\\n  -H \"x-api-key: your-api-key\"\n```\n\n### Request Example (Get Specific File)\n\n```bash\ncurl \"http://localhost:3456/api/logs?file=/home/user/.claude-code-router/logs/ccr-20241226143022.log\" \\\n  -H \"x-api-key: your-api-key\"\n```\n\n### Response Example\n\n```json\n[\n  \"{\\\"level\\\":30,\\\"time\\\":1703550622000,\\\"pid\\\":12345,\\\"hostname\\\":\\\"server\\\",\\\"msg\\\":\\\"Incoming request\\\",\\\"req\\\":{\\\"id\\\":1,\\\"method\\\":\\\"POST\\\",\\\"url\\\":\\\"/v1/messages\\\",\\\"remoteAddress\\\":\\\"127.0.0.1\\\"}}\",\n  \"{\\\"level\\\":30,\\\"time\\\":1703550622500,\\\"pid\\\":12345,\\\"hostname\\\":\\\"server\\\",\\\"msg\\\":\\\"Request completed\\\",\\\"res\\\":{\\\"statusCode\\\":200,\\\"responseTime\\\":500}}\",\n  \"...\"\n]\n```\n\nReturns an array of log lines, each line is a JSON string.\n\n### Log Format\n\nLogs use Pino format:\n\n```json\n{\n  \"level\": 30,\n  \"time\": 1703550622000,\n  \"pid\": 12345,\n  \"hostname\": \"server\",\n  \"msg\": \"Incoming request\",\n  \"req\": {\n    \"id\": 1,\n    \"method\": \"POST\",\n    \"url\": \"/v1/messages\",\n    \"remoteAddress\": \"127.0.0.1\"\n  }\n}\n```\n\n### Log Levels\n\n| Level | Value | Description |\n|-------|-------|-------------|\n| `trace` | 10 | Most verbose logs |\n| `debug` | 20 | Debug information |\n| `info` | 30 | General information |\n| `warn` | 40 | Warning information |\n| `error` | 50 | Error information |\n| `fatal` | 60 | Fatal error |\n\n## DELETE /api/logs\n\nClear content of specified log file.\n\n### Query Parameters\n\n| Parameter | Type | Required | Description |\n|-----------|------|----------|-------------|\n| `file` | string | No | Log file path (default uses app.log) |\n\n### Request Example (Clear Default Log)\n\n```bash\ncurl -X DELETE \"http://localhost:3456/api/logs\" \\\n  -H \"x-api-key: your-api-key\"\n```\n\n### Request Example (Clear Specific File)\n\n```bash\ncurl -X DELETE \"http://localhost:3456/api/logs?file=/home/user/.claude-code-router/logs/ccr-20241226143022.log\" \\\n  -H \"x-api-key: your-api-key\"\n```\n\n### Response Example\n\n```json\n{\n  \"success\": true,\n  \"message\": \"Logs cleared successfully\"\n}\n```\n\n## Log Locations\n\n### Server Logs\n\nLocation: `~/.claude-code-router/logs/`\n\nFile naming: `ccr-{YYYYMMDD}{HH}{MM}{SS}.log`\n\nContent: HTTP requests, API calls, server events\n\n### Application Logs\n\nLocation: `~/.claude-code-router/claude-code-router.log`\n\nContent: Routing decisions, business logic events\n\n## Log Rotation\n\nServer logs use rotating-file-stream for automatic rotation:\n\n- **maxFiles**: 3 - Keep last 3 log files\n- **interval**: 1d - Rotate daily\n- **maxSize**: 50M - Maximum 50MB per file\n"
  },
  {
    "path": "docs/docs/server/api/messages-api.md",
    "content": "---\ntitle: Messages API\n---\n\n# Messages API\n\n## POST /v1/messages\n\nSend messages to LLM, compatible with Anthropic Claude API format.\n\n### Request Format\n\n```bash\ncurl -X POST http://localhost:3456/v1/messages \\\n  -H \"x-api-key: your-api-key\" \\\n  -H \"content-type: application/json\" \\\n  -d '{\n    \"model\": \"claude-3-5-sonnet-20241022\",\n    \"max_tokens\": 1024,\n    \"messages\": [\n      {\n        \"role\": \"user\",\n        \"content\": \"Hello, Claude!\"\n      }\n    ]\n  }'\n```\n\n### Request Parameters\n\n| Parameter | Type | Required | Description |\n|-----------|------|----------|-------------|\n| `model` | string | Yes | Model name (will be routed to actual provider) |\n| `messages` | array | Yes | Array of messages |\n| `max_tokens` | integer | Yes | Maximum tokens to generate |\n| `system` | string | No | System prompt |\n| `tools` | array | No | List of available tools |\n| `stream` | boolean | No | Whether to use streaming response (default false) |\n| `temperature` | number | No | Temperature parameter (0-1) |\n\n### Message Object Format\n\n```json\n{\n  \"role\": \"user|assistant\",\n  \"content\": \"string | array\"\n}\n```\n\n### Response Format (Non-streaming)\n\n```json\n{\n  \"id\": \"msg_xxx\",\n  \"type\": \"message\",\n  \"role\": \"assistant\",\n  \"content\": [\n    {\n      \"type\": \"text\",\n      \"text\": \"Hello! How can I help you today?\"\n    }\n  ],\n  \"model\": \"claude-3-5-sonnet-20241022\",\n  \"stop_reason\": \"end_turn\",\n  \"usage\": {\n    \"input_tokens\": 10,\n    \"output_tokens\": 20\n  }\n}\n```\n\n### Streaming Response\n\nSet `stream: true` to enable streaming response:\n\n```json\n{\n  \"model\": \"claude-3-5-sonnet-20241022\",\n  \"max_tokens\": 1024,\n  \"messages\": [...],\n  \"stream\": true\n}\n```\n\nStreaming response event types:\n\n- `message_start` - Message start\n- `content_block_start` - Content block start\n- `content_block_delta` - Content increment\n- `content_block_stop` - Content block end\n- `message_delta` - Message metadata (usage)\n- `message_stop` - Message end\n\n### Tool Use\n\nSupports function calling (Tool Use):\n\n```json\n{\n  \"model\": \"claude-3-5-sonnet-20241022\",\n  \"max_tokens\": 1024,\n  \"messages\": [\n    {\n      \"role\": \"user\",\n      \"content\": \"What's the weather like?\"\n    }\n  ],\n  \"tools\": [\n    {\n      \"name\": \"get_weather\",\n      \"description\": \"Get the current weather\",\n      \"input_schema\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"location\": {\n            \"type\": \"string\",\n            \"description\": \"City name\"\n          }\n        },\n        \"required\": [\"location\"]\n      }\n    }\n  ]\n}\n```\n\n### Multimodal Support\n\nSupports image input:\n\n```json\n{\n  \"role\": \"user\",\n  \"content\": [\n    {\n      \"type\": \"image\",\n      \"source\": {\n        \"type\": \"base64\",\n        \"media_type\": \"image/png\",\n        \"data\": \"iVBORw0KGgo...\"\n      }\n    },\n    {\n      \"type\": \"text\",\n      \"text\": \"Describe this image\"\n    }\n  ]\n}\n```\n\n## POST /v1/messages/count_tokens\n\nCount tokens in messages.\n\n### Request Format\n\n```bash\ncurl -X POST http://localhost:3456/v1/messages/count_tokens \\\n  -H \"x-api-key: your-api-key\" \\\n  -H \"content-type: application/json\" \\\n  -d '{\n    \"model\": \"claude-3-5-sonnet-20241022\",\n    \"messages\": [\n      {\n        \"role\": \"user\",\n        \"content\": \"Hello!\"\n      }\n    ],\n    \"tools\": [],\n    \"system\": \"You are a helpful assistant.\"\n  }'\n```\n\n### Request Parameters\n\n| Parameter | Type | Required | Description |\n|-----------|------|----------|-------------|\n| `model` | string | Yes | Model name |\n| `messages` | array | Yes | Array of messages |\n| `tools` | array | No | List of tools |\n| `system` | string | No | System prompt |\n\n### Response Format\n\n```json\n{\n  \"input_tokens\": 42\n}\n```\n\n## Error Responses\n\n### 400 Bad Request\n\n```json\n{\n  \"error\": {\n    \"type\": \"invalid_request_error\",\n    \"message\": \"messages is required\"\n  }\n}\n```\n\n### 401 Unauthorized\n\n```json\n{\n  \"error\": {\n    \"type\": \"authentication_error\",\n    \"message\": \"Invalid API key\"\n  }\n}\n```\n\n### 500 Internal Server Error\n\n```json\n{\n  \"error\": {\n    \"type\": \"api_error\",\n    \"message\": \"Failed to connect to provider\"\n  }\n}\n```\n"
  },
  {
    "path": "docs/docs/server/api/overview.md",
    "content": "---\ntitle: API Overview\n---\n\n# API Overview\n\nClaude Code Router Server provides a complete HTTP API with support for:\n\n- **Messages API**: Message interface compatible with Anthropic Claude API\n- **Configuration API**: Read and update server configuration\n- **Logs API**: View and manage service logs\n- **Tools API**: Calculate token counts\n\n## Basic Information\n\n**Base URL**: `http://localhost:3456`\n\n**Authentication**: API Key (via `x-api-key` header)\n\n```bash\ncurl -H \"x-api-key: your-api-key\" http://localhost:3456/api/config\n```\n\n## API Endpoints\n\n### Messages\n\n| Endpoint | Method | Description |\n|----------|--------|-------------|\n| `/v1/messages` | POST | Send message (compatible with Anthropic API) |\n| `/v1/messages/count_tokens` | POST | Count tokens in messages |\n\n### Configuration Management\n\n| Endpoint | Method | Description |\n|----------|--------|-------------|\n| `/api/config` | GET | Get current configuration |\n| `/api/config` | POST | Update configuration |\n| `/api/transformers` | GET | Get list of available transformers |\n\n### Log Management\n\n| Endpoint | Method | Description |\n|----------|--------|-------------|\n| `/api/logs/files` | GET | Get list of log files |\n| `/api/logs` | GET | Get log content |\n| `/api/logs` | DELETE | Clear logs |\n\n### Service Management\n\n| Endpoint | Method | Description |\n|----------|--------|-------------|\n| `/api/restart` | POST | Restart service |\n| `/ui` | GET | Web management interface |\n| `/ui/` | GET | Web management interface (redirect) |\n\n## Authentication\n\n### API Key Authentication\n\nAdd API Key in request header:\n\n```bash\ncurl -X POST http://localhost:3456/v1/messages \\\n  -H \"x-api-key: your-api-key\" \\\n  -H \"content-type: application/json\" \\\n  -d '...'\n```\n\n## Streaming Responses\n\nThe Messages API supports streaming responses (Server-Sent Events):\n\n```bash\ncurl -X POST http://localhost:3456/v1/messages \\\n  -H \"x-api-key: your-api-key\" \\\n  -H \"content-type: application/json\" \\\n  -d '{\"stream\": true, ...}'\n```\n\nStreaming response format:\n\n```\nevent: message_start\ndata: {\"type\":\"message_start\",\"message\":{...}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"delta\":{\"type\":\"text_delta\",\"text\":\"Hello\"}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n```\n"
  },
  {
    "path": "docs/docs/server/config/basic.md",
    "content": "---\nsidebar_position: 1\n---\n\n# Basic Configuration\n\nLearn how to configure Claude Code Router to suit your needs.\n\n## Configuration File Location\n\nThe configuration file is located at:\n\n```\n~/.claude-code-router/config.json\n```\n\n## Configuration Structure\n\n### Providers\n\nConfigure LLM providers to route requests to:\n\n```json\n{\n  \"Providers\": [\n    {\n      \"NAME\": \"deepseek\",\n      \"HOST\": \"https://api.deepseek.com\",\n      \"APIKEY\": \"your-api-key\",\n      \"MODELS\": [\"deepseek-chat\", \"deepseek-coder\"]\n    },\n    {\n      \"NAME\": \"groq\",\n      \"HOST\": \"https://api.groq.com/openai/v1\",\n      \"APIKEY\": \"your-groq-api-key\",\n      \"MODELS\": [\"llama-3.3-70b-versatile\"]\n    }\n  ]\n}\n```\n\n### Router\n\nConfigure which model to use by default:\n\n```json\n{\n  \"Router\": {\n    \"default\": \"deepseek,deepseek-chat\"\n  }\n}\n```\n\nFormat: `{provider-name},{model-name}`\n\n### Transformers\n\nApply transformations to requests/responses:\n\n```json\n{\n  \"transformers\": [\n    {\n      \"name\": \"anthropic\",\n      \"providers\": [\"deepseek\", \"groq\"]\n    }\n  ]\n}\n```\n\n### Environment Variables\n\nUse environment variables in your configuration:\n\n```json\n{\n  \"Providers\": [\n    {\n      \"NAME\": \"deepseek\",\n      \"HOST\": \"https://api.deepseek.com\",\n      \"APIKEY\": \"$DEEPSEEK_API_KEY\"\n    }\n  ]\n}\n```\n\nBoth `$VAR_NAME` and `${VAR_NAME}` syntax are supported.\n\n## Complete Example\n\n```json\n{\n  \"port\": 8080,\n  \"Providers\": [\n    {\n      \"NAME\": \"deepseek\",\n      \"HOST\": \"https://api.deepseek.com\",\n      \"APIKEY\": \"$DEEPSEEK_API_KEY\",\n      \"MODELS\": [\"deepseek-chat\", \"deepseek-coder\"],\n      \"transformers\": [\"anthropic\"]\n    },\n    {\n      \"NAME\": \"groq\",\n      \"HOST\": \"https://api.groq.com/openai/v1\",\n      \"APIKEY\": \"$GROQ_API_KEY\",\n      \"MODELS\": [\"llama-3.3-70b-versatile\"],\n      \"transformers\": [\"anthropic\"]\n    }\n  ],\n  \"Router\": {\n    \"default\": \"deepseek,deepseek-chat\",\n    \"longContextThreshold\": 100000,\n    \"background\": \"groq,llama-3.3-70b-versatile\"\n  },\n  \"transformers\": [\n    {\n      \"name\": \"anthropic\",\n      \"providers\": [\"deepseek\", \"groq\"]\n    }\n  ]\n}\n```\n\n## Editing Configuration\n\nUse the CLI to edit the configuration:\n\n```bash\nccr config edit\n```\n\nThis will open the configuration file in your default editor.\n\n## Reloading Configuration\n\nAfter editing the configuration, restart the router:\n\n```bash\nccr restart\n```\n\n## Next Steps\n\n- [Providers Configuration](/docs/config/providers) - Detailed provider configuration\n- [Routing Configuration](/docs/config/routing) - Configure routing rules\n- [Transformers](/docs/config/transformers) - Apply transformations\n"
  },
  {
    "path": "docs/docs/server/config/providers.md",
    "content": "---\nsidebar_position: 2\n---\n\n# Providers Configuration\n\nDetailed guide for configuring LLM providers.\n\n## Supported Providers\n\n### DeepSeek\n\n```json\n{\n  \"NAME\": \"deepseek\",\n  \"HOST\": \"https://api.deepseek.com\",\n  \"APIKEY\": \"your-api-key\",\n  \"MODELS\": [\"deepseek-chat\", \"deepseek-coder\"],\n  \"transformers\": [\"anthropic\"]\n}\n```\n\n### Groq\n\n```json\n{\n  \"NAME\": \"groq\",\n  \"HOST\": \"https://api.groq.com/openai/v1\",\n  \"APIKEY\": \"your-api-key\",\n  \"MODELS\": [\"llama-3.3-70b-versatile\"],\n  \"transformers\": [\"anthropic\"]\n}\n```\n\n### Gemini\n\n```json\n{\n  \"NAME\": \"gemini\",\n  \"HOST\": \"https://generativelanguage.googleapis.com/v1beta\",\n  \"APIKEY\": \"your-api-key\",\n  \"MODELS\": [\"gemini-1.5-pro\"],\n  \"transformers\": [\"anthropic\"]\n}\n```\n\n### OpenRouter\n\n```json\n{\n  \"NAME\": \"openrouter\",\n  \"HOST\": \"https://openrouter.ai/api/v1\",\n  \"APIKEY\": \"your-api-key\",\n  \"MODELS\": [\"anthropic/claude-3.5-sonnet\"],\n  \"transformers\": [\"anthropic\"]\n}\n```\n\n## Provider Configuration Options\n\n| Field | Type | Required | Description |\n|-------|------|----------|-------------|\n| `NAME` | string | Yes | Unique provider identifier |\n| `HOST` | string | Yes | API base URL |\n| `APIKEY` | string | Yes | API authentication key |\n| `MODELS` | string[] | No | List of available models |\n| `transformers` | string[] | No | List of transformers to apply |\n\n## Model Selection\n\nWhen selecting a model in routing, use the format:\n\n```\n{provider-name},{model-name}\n```\n\nFor example:\n\n```\ndeepseek,deepseek-chat\n```\n\n## Next Steps\n\n- [Routing Configuration](/docs/config/routing) - Configure how requests are routed\n- [Transformers](/docs/config/transformers) - Apply transformations to requests\n"
  },
  {
    "path": "docs/docs/server/config/routing.md",
    "content": "---\nsidebar_position: 3\n---\n\n# Routing Configuration\n\nConfigure how requests are routed to different models.\n\n## Default Routing\n\nSet the default model for all requests:\n\n```json\n{\n  \"Router\": {\n    \"default\": \"deepseek,deepseek-chat\"\n  }\n}\n```\n\n## Built-in Scenarios\n\n### Background Tasks\n\nRoute background tasks to a lightweight model:\n\n```json\n{\n  \"Router\": {\n    \"background\": \"groq,llama-3.3-70b-versatile\"\n  }\n}\n```\n\n### Thinking Mode (Plan Mode)\n\nRoute thinking-intensive tasks to a more capable model:\n\n```json\n{\n  \"Router\": {\n    \"think\": \"deepseek,deepseek-chat\"\n  }\n}\n```\n\n### Long Context\n\nRoute requests with long context:\n\n```json\n{\n  \"Router\": {\n    \"longContextThreshold\": 100000,\n    \"longContext\": \"gemini,gemini-1.5-pro\"\n  }\n}\n```\n\n### Web Search\n\nRoute web search tasks:\n\n```json\n{\n  \"Router\": {\n    \"webSearch\": \"deepseek,deepseek-chat\"\n  }\n}\n```\n\n### Image Tasks\n\nRoute image-related tasks:\n\n```json\n{\n  \"Router\": {\n    \"image\": \"gemini,gemini-1.5-pro\"\n  }\n}\n```\n\n## Fallback\n\nWhen a request fails, you can configure a list of backup models. The system will try each model in sequence until one succeeds:\n\n### Basic Configuration\n\n```json\n{\n  \"Router\": {\n    \"default\": \"deepseek,deepseek-chat\",\n    \"background\": \"ollama,qwen2.5-coder:latest\",\n    \"think\": \"deepseek,deepseek-reasoner\",\n    \"longContext\": \"openrouter,google/gemini-2.5-pro-preview\",\n    \"longContextThreshold\": 60000,\n    \"webSearch\": \"gemini,gemini-2.5-flash\"\n  },\n  \"fallback\": {\n    \"default\": [\n      \"aihubmix,Z/glm-4.5\",\n      \"openrouter,anthropic/claude-sonnet-4\"\n    ],\n    \"background\": [\n      \"ollama,qwen2.5-coder:latest\"\n    ],\n    \"think\": [\n      \"openrouter,anthropic/claude-3.7-sonnet:thinking\"\n    ],\n    \"longContext\": [\n      \"modelscope,Qwen/Qwen3-Coder-480B-A35B-Instruct\"\n    ],\n    \"webSearch\": [\n      \"openrouter,anthropic/claude-sonnet-4\"\n    ]\n  }\n}\n```\n\n### How It Works\n\n1. **Trigger**: When a model request fails for a routing scenario (HTTP error response)\n2. **Auto-switch**: The system automatically checks the fallback configuration for that scenario\n3. **Sequential retry**: Tries each backup model in order\n4. **Success**: Once a model responds successfully, returns immediately\n5. **All failed**: If all backup models fail, returns the original error\n\n### Configuration Details\n\n- **Format**: Each backup model format is `provider,model`\n- **Validation**: Backup models must exist in the `Providers` configuration\n- **Flexibility**: Different scenarios can have different fallback lists\n- **Optional**: If a scenario doesn't need fallback, omit it or use an empty array\n\n### Use Cases\n\n#### Scenario 1: Primary Model Quota Exhausted\n\n```json\n{\n  \"Router\": {\n    \"default\": \"openrouter,anthropic/claude-sonnet-4\"\n  },\n  \"fallback\": {\n    \"default\": [\n      \"deepseek,deepseek-chat\",\n      \"aihubmix,Z/glm-4.5\"\n    ]\n  }\n}\n```\n\nAutomatically switches to backup models when the primary model quota is exhausted.\n\n#### Scenario 2: Service Reliability\n\n```json\n{\n  \"Router\": {\n    \"background\": \"volcengine,deepseek-v3-250324\"\n  },\n  \"fallback\": {\n    \"background\": [\n      \"modelscope,Qwen/Qwen3-Coder-480B-A35B-Instruct\",\n      \"dashscope,qwen3-coder-plus\"\n    ]\n  }\n}\n```\n\nAutomatically switches to other providers when the primary service fails.\n\n### Log Monitoring\n\nThe system logs detailed fallback process:\n\n```\n[warn] Request failed for default, trying 2 fallback models\n[info] Trying fallback model: aihubmix,Z/glm-4.5\n[warn] Fallback model aihubmix,Z/glm-4.5 failed: API rate limit exceeded\n[info] Trying fallback model: openrouter,anthropic/claude-sonnet-4\n[info] Fallback model openrouter,anthropic/claude-sonnet-4 succeeded\n```\n\n### Important Notes\n\n1. **Cost consideration**: Backup models may incur different costs, configure appropriately\n2. **Performance differences**: Different models may have varying response speeds and quality\n3. **Quota management**: Ensure backup models have sufficient quotas\n4. **Testing**: Regularly test the availability of backup models\n\n## Project-Level Routing\n\nConfigure routing per project in `~/.claude/projects/<project-id>/claude-code-router.json`:\n\n```json\n{\n  \"Router\": {\n    \"default\": \"groq,llama-3.3-70b-versatile\"\n  }\n}\n```\n\nProject-level configuration takes precedence over global configuration.\n\n## Custom Router\n\nCreate a custom JavaScript router function:\n\n1. Create a router file (e.g., `custom-router.js`):\n\n```javascript\nmodule.exports = function(config, context) {\n  // Analyze the request context\n  const { scenario, projectId, tokenCount } = context;\n\n  // Custom routing logic\n  if (scenario === 'background') {\n    return 'groq,llama-3.3-70b-versatile';\n  }\n\n  if (tokenCount > 100000) {\n    return 'gemini,gemini-1.5-pro';\n  }\n\n  // Default\n  return 'deepseek,deepseek-chat';\n};\n```\n\n2. Set the `CUSTOM_ROUTER_PATH` environment variable:\n\n```bash\nexport CUSTOM_ROUTER_PATH=\"/path/to/custom-router.js\"\n```\n\n## Token Counting\n\nThe router uses `tiktoken` (cl100k_base) to estimate request token count. This is used for:\n\n- Determining if a request exceeds `longContextThreshold`\n- Custom routing logic based on token count\n\n## Subagent Routing\n\nSpecify models for subagents using special tags:\n\n```\n<CCR-SUBAGENT-MODEL>provider,model</CCR-SUBAGENT-MODEL>\nPlease help me analyze this code...\n```\n\n## Next Steps\n\n- [Transformers](/docs/config/transformers) - Apply transformations to requests\n- [Custom Router](/docs/advanced/custom-router) - Advanced custom routing\n"
  },
  {
    "path": "docs/docs/server/config/transformers.md",
    "content": "---\nsidebar_position: 4\n---\n\n# Transformers\n\nTransformers are the core mechanism for adapting API differences between LLM providers. They convert requests and responses between different formats, handle authentication, and manage provider-specific features.\n\n## Understanding Transformers\n\n### What is a Transformer?\n\nA transformer is a plugin that:\n- **Transforms requests** from the unified format to provider-specific format\n- **Transforms responses** from provider format back to unified format\n- **Handles authentication** for provider APIs\n- **Modifies requests** to add or adjust parameters\n\n### Data Flow\n\n```\n┌─────────────────┐\n│ Incoming Request│ (Anthropic format from Claude Code)\n└────────┬────────┘\n         │\n         ▼\n┌─────────────────────────────────┐\n│  transformRequestOut            │ ← Parse incoming request to unified format\n└────────┬────────────────────────┘\n         │\n         ▼\n┌─────────────────────────────────┐\n│  UnifiedChatRequest             │\n└────────┬────────────────────────┘\n         │\n         ▼\n┌─────────────────────────────────┐\n│  transformRequestIn (optional)  │ ← Modify unified request before sending\n└────────┬────────────────────────┘\n         │\n         ▼\n┌─────────────────────────────────┐\n│  Provider API Call              │\n└────────┬────────────────────────┘\n         │\n         ▼\n┌─────────────────────────────────┐\n│  transformResponseIn (optional) │ ← Convert provider response to unified format\n└────────┬────────────────────────┘\n         │\n         ▼\n┌─────────────────────────────────┐\n│  transformResponseOut (optional)│ ← Convert unified response to Anthropic format\n└────────┬────────────────────────┘\n         │\n         ▼\n┌─────────────────┐\n│ Outgoing Response│ (Anthropic format to Claude Code)\n└─────────────────┘\n```\n\n### Transformer Interface\n\nAll transformers implement the following interface:\n\n```typescript\ninterface Transformer {\n  // Convert unified request to provider-specific format\n  transformRequestIn?: (\n    request: UnifiedChatRequest,\n    provider: LLMProvider,\n    context: TransformerContext\n  ) => Promise<Record<string, any>>;\n\n  // Convert provider request to unified format\n  transformRequestOut?: (\n    request: any,\n    context: TransformerContext\n  ) => Promise<UnifiedChatRequest>;\n\n  // Convert provider response to unified format\n  transformResponseIn?: (\n    response: Response,\n    context?: TransformerContext\n  ) => Promise<Response>;\n\n  // Convert unified response to provider format\n  transformResponseOut?: (\n    response: Response,\n    context: TransformerContext\n  ) => Promise<Response>;\n\n  // Custom endpoint path (optional)\n  endPoint?: string;\n\n  // Transformer name (for custom transformers)\n  name?: string;\n\n  // Custom authentication handler (optional)\n  auth?: (\n    request: any,\n    provider: LLMProvider,\n    context: TransformerContext\n  ) => Promise<any>;\n\n  // Logger instance (auto-injected)\n  logger?: any;\n}\n```\n\n### Key Types\n\n#### UnifiedChatRequest\n\n```typescript\ninterface UnifiedChatRequest {\n  messages: UnifiedMessage[];\n  model: string;\n  max_tokens?: number;\n  temperature?: number;\n  stream?: boolean;\n  tools?: UnifiedTool[];\n  tool_choice?: any;\n  reasoning?: {\n    effort?: ThinkLevel;  // \"none\" | \"low\" | \"medium\" | \"high\"\n    max_tokens?: number;\n    enabled?: boolean;\n  };\n}\n```\n\n#### UnifiedMessage\n\n```typescript\ninterface UnifiedMessage {\n  role: \"user\" | \"assistant\" | \"system\" | \"tool\";\n  content: string | null | MessageContent[];\n  tool_calls?: Array<{\n    id: string;\n    type: \"function\";\n    function: {\n      name: string;\n      arguments: string;\n    };\n  }>;\n  tool_call_id?: string;\n  thinking?: {\n    content: string;\n    signature?: string;\n  };\n}\n```\n\n## Built-in Transformers\n\n### anthropic\n\nTransforms requests to be compatible with Anthropic-style APIs:\n\n```json\n{\n  \"transformers\": [\n    {\n      \"name\": \"anthropic\",\n      \"providers\": [\"deepseek\", \"groq\"]\n    }\n  ]\n}\n```\n\n**Features:**\n- Converts Anthropic message format to/from OpenAI format\n- Handles tool calls and tool results\n- Supports thinking/reasoning content blocks\n- Manages streaming responses\n\n### deepseek\n\nSpecialized transformer for DeepSeek API:\n\n```json\n{\n  \"transformers\": [\n    {\n      \"name\": \"deepseek\",\n      \"providers\": [\"deepseek\"]\n    }\n  ]\n}\n```\n\n**Features:**\n- DeepSeek-specific reasoning format\n- Handles `reasoning_content` in responses\n- Supports thinking budget tokens\n\n### gemini\n\nTransformer for Google Gemini API:\n\n```json\n{\n  \"transformers\": [\n    {\n      \"name\": \"gemini\",\n      \"providers\": [\"gemini\"]\n    }\n  ]\n}\n```\n\n### maxtoken\n\nLimits max_tokens in requests:\n\n```json\n{\n  \"transformers\": [\n    {\n      \"name\": \"maxtoken\",\n      \"options\": {\n        \"max_tokens\": 8192\n      },\n      \"models\": [\"deepseek,deepseek-chat\"]\n    }\n  ]\n}\n```\n\n### customparams\n\nInjects custom parameters into requests:\n\n```json\n{\n  \"transformers\": [\n    {\n      \"name\": \"customparams\",\n      \"options\": {\n        \"include_reasoning\": true,\n        \"custom_header\": \"value\"\n      }\n    }\n  ]\n}\n```\n\n## Creating Custom Transformers\n\n### Simple Transformer: Modifying Requests\n\nThe simplest transformers just modify the request before it's sent to the provider.\n\n**Example: Add a custom header to all requests**\n\n```javascript\n// custom-header-transformer.js\nmodule.exports = class CustomHeaderTransformer {\n  name = 'custom-header';\n\n  constructor(options) {\n    this.headerName = options?.headerName || 'X-Custom-Header';\n    this.headerValue = options?.headerValue || 'default-value';\n  }\n\n  async transformRequestIn(request, provider, context) {\n    // Add custom header (will be used by auth method)\n    request._customHeaders = {\n      [this.headerName]: this.headerValue\n    };\n    return request;\n  }\n\n  async auth(request, provider) {\n    const headers = {\n      'authorization': `Bearer ${provider.apiKey}`,\n      ...request._customHeaders\n    };\n    return {\n      body: request,\n      config: { headers }\n    };\n  }\n};\n```\n\n**Usage in config:**\n\n```json\n{\n  \"transformers\": [\n    {\n      \"name\": \"custom-header\",\n      \"path\": \"/path/to/custom-header-transformer.js\",\n      \"options\": {\n        \"headerName\": \"X-My-Header\",\n        \"headerValue\": \"my-value\"\n      }\n    }\n  ]\n}\n```\n\n### Intermediate Transformer: Request/Response Conversion\n\nThis example shows how to convert between different API formats.\n\n**Example: Mock API format transformer**\n\n```javascript\n// mockapi-transformer.js\nmodule.exports = class MockAPITransformer {\n  name = 'mockapi';\n  endPoint = '/v1/chat';  // Custom endpoint\n\n  // Convert from MockAPI format to unified format\n  async transformRequestOut(request, context) {\n    const messages = request.conversation.map(msg => ({\n      role: msg.sender,\n      content: msg.text\n    }));\n\n    return {\n      messages,\n      model: request.model_id,\n      max_tokens: request.max_tokens,\n      temperature: request.temp\n    };\n  }\n\n  // Convert from unified format to MockAPI format\n  async transformRequestIn(request, provider, context) {\n    return {\n      model_id: request.model,\n      conversation: request.messages.map(msg => ({\n        sender: msg.role,\n        text: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content)\n      })),\n      max_tokens: request.max_tokens || 4096,\n      temp: request.temperature || 0.7\n    };\n  }\n\n  // Convert MockAPI response to unified format\n  async transformResponseIn(response, context) {\n    const data = await response.json();\n\n    const unifiedResponse = {\n      id: data.request_id,\n      object: 'chat.completion',\n      created: data.timestamp,\n      model: data.model,\n      choices: [{\n        index: 0,\n        message: {\n          role: 'assistant',\n          content: data.reply.text\n        },\n        finish_reason: data.stop_reason\n      }],\n      usage: {\n        prompt_tokens: data.tokens.input,\n        completion_tokens: data.tokens.output,\n        total_tokens: data.tokens.input + data.tokens.output\n      }\n    };\n\n    return new Response(JSON.stringify(unifiedResponse), {\n      status: response.status,\n      statusText: response.statusText,\n      headers: { 'Content-Type': 'application/json' }\n    });\n  }\n};\n```\n\n### Advanced Transformer: Streaming Response Processing\n\nThis example shows how to handle streaming responses.\n\n**Example: Add custom metadata to streaming responses**\n\n```javascript\n// streaming-metadata-transformer.js\nmodule.exports = class StreamingMetadataTransformer {\n  name = 'streaming-metadata';\n\n  constructor(options) {\n    this.metadata = options?.metadata || {};\n    this.logger = null;  // Will be injected by the system\n  }\n\n  async transformResponseOut(response, context) {\n    const contentType = response.headers.get('Content-Type');\n\n    // Handle streaming response\n    if (contentType?.includes('text/event-stream')) {\n      return this.transformStream(response, context);\n    }\n\n    // Handle non-streaming response\n    return response;\n  }\n\n  async transformStream(response, context) {\n    const decoder = new TextDecoder();\n    const encoder = new TextEncoder();\n\n    const transformedStream = new ReadableStream({\n      start: async (controller) => {\n        const reader = response.body.getReader();\n        let buffer = '';\n\n        try {\n          while (true) {\n            const { done, value } = await reader.read();\n            if (done) break;\n\n            buffer += decoder.decode(value, { stream: true });\n            const lines = buffer.split('\\n');\n            buffer = lines.pop() || '';\n\n            for (const line of lines) {\n              if (!line.trim() || !line.startsWith('data: ')) {\n                controller.enqueue(encoder.encode(line + '\\n'));\n                continue;\n              }\n\n              const data = line.slice(6).trim();\n              if (data === '[DONE]') {\n                controller.enqueue(encoder.encode(line + '\\n'));\n                continue;\n              }\n\n              try {\n                const chunk = JSON.parse(data);\n\n                // Add custom metadata\n                if (chunk.choices && chunk.choices[0]) {\n                  chunk.choices[0].metadata = this.metadata;\n                }\n\n                // Log for debugging\n                this.logger?.debug({\n                  chunk,\n                  context: context.req.id\n                }, 'Transformed streaming chunk');\n\n                const modifiedLine = `data: ${JSON.stringify(chunk)}\\n\\n`;\n                controller.enqueue(encoder.encode(modifiedLine));\n              } catch (parseError) {\n                // If parsing fails, pass through original line\n                controller.enqueue(encoder.encode(line + '\\n'));\n              }\n            }\n          }\n        } catch (error) {\n          this.logger?.error({ error }, 'Stream transformation error');\n          controller.error(error);\n        } finally {\n          controller.close();\n          reader.releaseLock();\n        }\n      }\n    });\n\n    return new Response(transformedStream, {\n      status: response.status,\n      statusText: response.statusText,\n      headers: {\n        'Content-Type': 'text/event-stream',\n        'Cache-Control': 'no-cache',\n        'Connection': 'keep-alive'\n      }\n    });\n  }\n};\n```\n\n### Real-World Example: Reasoning Content Transformer\n\nThis is based on the actual `reasoning.transformer.ts` from the codebase.\n\n```typescript\n// reasoning-transformer.ts\nimport { Transformer, TransformerOptions } from \"@musistudio/llms\";\n\nexport class ReasoningTransformer implements Transformer {\n  static TransformerName = \"reasoning\";\n  enable: boolean;\n\n  constructor(private readonly options?: TransformerOptions) {\n    this.enable = this.options?.enable ?? true;\n  }\n\n  // Transform request to add reasoning parameters\n  async transformRequestIn(request: UnifiedChatRequest): Promise<UnifiedChatRequest> {\n    if (!this.enable) {\n      request.thinking = {\n        type: \"disabled\",\n        budget_tokens: -1,\n      };\n      request.enable_thinking = false;\n      return request;\n    }\n\n    if (request.reasoning) {\n      request.thinking = {\n        type: \"enabled\",\n        budget_tokens: request.reasoning.max_tokens,\n      };\n      request.enable_thinking = true;\n    }\n    return request;\n  }\n\n  // Transform response to convert reasoning_content to thinking format\n  async transformResponseOut(response: Response): Promise<Response> {\n    if (!this.enable) return response;\n\n    const contentType = response.headers.get(\"Content-Type\");\n\n    // Handle non-streaming response\n    if (contentType?.includes(\"application/json\")) {\n      const jsonResponse = await response.json();\n      if (jsonResponse.choices[0]?.message.reasoning_content) {\n        jsonResponse.thinking = {\n          content: jsonResponse.choices[0].message.reasoning_content\n        };\n      }\n      return new Response(JSON.stringify(jsonResponse), {\n        status: response.status,\n        statusText: response.statusText,\n        headers: response.headers,\n      });\n    }\n\n    // Handle streaming response\n    if (contentType?.includes(\"stream\")) {\n      // [Streaming transformation code here]\n      // See the full implementation in the codebase\n    }\n\n    return response;\n  }\n}\n```\n\n## Transformer Registration\n\n### Method 1: Static Name (Class-based)\n\nUse this when creating a transformer in TypeScript/ES6:\n\n```typescript\nexport class MyTransformer implements Transformer {\n  static TransformerName = \"my-transformer\";\n\n  async transformRequestIn(request: UnifiedChatRequest): Promise<any> {\n    // Transformation logic\n    return request;\n  }\n}\n```\n\n### Method 2: Instance Name (Instance-based)\n\nUse this for JavaScript transformers:\n\n```javascript\nmodule.exports = class MyTransformer {\n  constructor(options) {\n    this.name = 'my-transformer';\n    this.options = options;\n  }\n\n  async transformRequestIn(request, provider, context) {\n    // Transformation logic\n    return request;\n  }\n};\n```\n\n## Applying Transformers\n\n### Global Application (Provider Level)\n\nApply to all requests for a provider:\n\n```json\n{\n  \"Providers\": [\n    {\n      \"NAME\": \"deepseek\",\n      \"HOST\": \"https://api.deepseek.com\",\n      \"APIKEY\": \"your-api-key\",\n      \"transformers\": [\"anthropic\"]\n    }\n  ]\n}\n```\n\n### Model-Specific Application\n\nApply to specific models only:\n\n```json\n{\n  \"transformers\": [\n    {\n      \"name\": \"maxtoken\",\n      \"options\": {\n        \"max_tokens\": 8192\n      },\n      \"models\": [\"deepseek,deepseek-chat\"]\n    }\n  ]\n}\n```\n\nNote: The model format is `provider,model` (e.g., `deepseek,deepseek-chat`).\n\n### Global Transformers (All Providers)\n\nApply transformers to all providers:\n\n```json\n{\n  \"transformers\": [\n    {\n      \"name\": \"custom-logger\",\n      \"path\": \"/path/to/custom-logger.js\"\n    }\n  ]\n}\n```\n\n### Passing Options\n\nSome transformers accept configuration options:\n\n```json\n{\n  \"transformers\": [\n    {\n      \"name\": \"maxtoken\",\n      \"options\": {\n        \"max_tokens\": 8192\n      }\n    },\n    {\n      \"name\": \"customparams\",\n      \"options\": {\n        \"custom_param_1\": \"value1\",\n        \"custom_param_2\": 42\n      }\n    }\n  ]\n}\n```\n\n## Best Practices\n\n### 1. Immutability\n\nAlways create new objects rather than mutating existing ones:\n\n```javascript\n// Bad\nasync transformRequestIn(request) {\n  request.max_tokens = 4096;\n  return request;\n}\n\n// Good\nasync transformRequestIn(request) {\n  return {\n    ...request,\n    max_tokens: request.max_tokens || 4096\n  };\n}\n```\n\n### 2. Error Handling\n\nAlways handle errors gracefully:\n\n```javascript\nasync transformResponseIn(response) {\n  try {\n    const data = await response.json();\n    // Process data\n    return new Response(JSON.stringify(processedData), {\n      status: response.status,\n      headers: response.headers\n    });\n  } catch (error) {\n    this.logger?.error({ error }, 'Transformation failed');\n    // Return original response if transformation fails\n    return response;\n  }\n}\n```\n\n### 3. Logging\n\nUse the injected logger for debugging:\n\n```javascript\nasync transformRequestIn(request, provider, context) {\n  this.logger?.debug({\n    model: request.model,\n    provider: provider.name\n  }, 'Transforming request');\n\n  // Your transformation logic\n\n  return modifiedRequest;\n}\n```\n\n### 4. Stream Handling\n\nWhen handling streams, always:\n- Use a buffer to handle incomplete chunks\n- Properly release the reader lock\n- Handle errors in the stream\n- Close the controller when done\n\n```javascript\nconst transformedStream = new ReadableStream({\n  start: async (controller) => {\n    const reader = response.body.getReader();\n    let buffer = '';\n\n    try {\n      while (true) {\n        const { done, value } = await reader.read();\n        if (done) break;\n\n        // Process stream...\n      }\n    } catch (error) {\n      controller.error(error);\n    } finally {\n      controller.close();\n      reader.releaseLock();\n    }\n  }\n});\n```\n\n### 5. Context Usage\n\nThe `context` parameter contains useful information:\n\n```javascript\nasync transformRequestIn(request, provider, context) {\n  // Access request ID\n  const requestId = context.req.id;\n\n  // Access original request\n  const originalRequest = context.req.original;\n\n  // Your transformation logic\n}\n```\n\n## Testing Your Transformer\n\n### Manual Testing\n\n1. Add your transformer to the config\n2. Start the server: `ccr restart`\n3. Check logs: `tail -f ~/.claude-code-router/logs/ccr-*.log`\n4. Make a test request\n5. Verify the output\n\n### Debug Tips\n\n- Add logging to track transformation steps\n- Test with both streaming and non-streaming requests\n- Verify error handling with invalid inputs\n- Check that original responses are returned on error\n\n## Next Steps\n\n- [Advanced Topics](/docs/server/advanced/custom-router) - Advanced routing customization\n- [Agents](/docs/server/advanced/agents) - Extending with agents\n- [Core Package](/docs/server/intro) - Learn about @musistudio/llms\n"
  },
  {
    "path": "docs/docs/server/deployment.md",
    "content": "---\ntitle: Server Deployment\n---\n\n# Server Deployment\n\nClaude Code Router Server supports multiple deployment methods, from local development to production environments.\n\n## Docker Deployment (Recommended)\n\n### Using Docker Hub Image\n\n```bash\ndocker run -d \\\n  --name claude-code-router \\\n  -p 3456:3456 \\\n  -v ~/.claude-code-router:/app/.claude-code-router \\\n  musistudio/claude-code-router:latest\n```\n\n### Using Docker Compose\n\nCreate `docker-compose.yml`:\n\n```yaml\nversion: '3.8'\nservices:\n  claude-code-router:\n    image: musistudio/claude-code-router:latest\n    container_name: claude-code-router\n    ports:\n      - \"3456:3456\"\n    volumes:\n      - ./config:/app/.claude-code-router\n    environment:\n      - LOG_LEVEL=info\n      - HOST=0.0.0.0\n      - PORT=3456\n    restart: unless-stopped\n```\n\nStart the service:\n\n```bash\ndocker-compose up -d\n```\n\n### Custom Build\n\nBuild Docker image from source:\n\n```bash\ngit clone https://github.com/musistudio/claude-code-router.git\ncd claude-code-router\ndocker build -t claude-code-router:latest .\n```\n\n## Configuration File Mounting\n\nMount configuration file into container:\n\n```bash\ndocker run -d \\\n  --name claude-code-router \\\n  -p 3456:3456 \\\n  -v $(pwd)/config.json:/app/.claude-code-router/config.json \\\n  musistudio/claude-code-router:latest\n```\n\nConfiguration file example:\n\n```json5\n{\n  // Server configuration\n  \"HOST\": \"0.0.0.0\",\n  \"PORT\": 3456,\n  \"APIKEY\": \"your-api-key-here\",\n\n  // Logging configuration\n  \"LOG\": true,\n  \"LOG_LEVEL\": \"info\",\n\n  // LLM provider configuration\n  \"Providers\": [\n    {\n      \"name\": \"openai\",\n      \"baseUrl\": \"https://api.openai.com/v1\",\n      \"apiKey\": \"$OPENAI_API_KEY\",\n      \"models\": [\"gpt-4\", \"gpt-3.5-turbo\"]\n    }\n  ],\n\n  // Routing configuration\n  \"Router\": {\n    \"default\": \"openai,gpt-4\"\n  }\n}\n```\n\n## Environment Variables\n\nOverride configuration through environment variables:\n\n| Variable | Description | Default |\n|----------|-------------|---------|\n| `HOST` | Listen address | `127.0.0.1` |\n| `PORT` | Listen port | `3456` |\n| `APIKEY` | API key | - |\n| `LOG_LEVEL` | Log level | `debug` |\n| `LOG` | Enable logging | `true` |\n\n## Production Recommendations\n\n### 1. Use Reverse Proxy\n\nUse Nginx as reverse proxy:\n\n```nginx\nserver {\n    listen 80;\n    server_name your-domain.com;\n\n    location / {\n        proxy_pass http://localhost:3456;\n        proxy_http_version 1.1;\n        proxy_set_header Upgrade $http_upgrade;\n        proxy_set_header Connection 'upgrade';\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_cache_bypass $http_upgrade;\n    }\n}\n```\n\n### 2. Configure HTTPS\n\nUse Let's Encrypt to obtain free certificate:\n\n```bash\nsudo certbot --nginx -d your-domain.com\n```\n\n### 3. Log Management\n\nConfigure log rotation and persistence:\n\n```yaml\nversion: '3.8'\nservices:\n  claude-code-router:\n    image: musistudio/claude-code-router:latest\n    volumes:\n      - ./logs:/app/.claude-code-router/logs\n    environment:\n      - LOG_LEVEL=warn\n```\n\n### 4. Health Check\n\nConfigure Docker health check:\n\n```yaml\nhealthcheck:\n  test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:3456/api/config\"]\n  interval: 30s\n  timeout: 10s\n  retries: 3\n```\n\n## Access Web UI\n\nAfter deployment is complete, access the Web UI:\n\n```\nhttp://localhost:3456/ui/\n```\n\nThrough the Web UI you can:\n- View and manage configuration\n- Monitor logs\n- Check service status\n\n## Secondary Development\n\nIf you need to develop based on CCR Server, please see [API Reference](/docs/category/api).\n"
  },
  {
    "path": "docs/docs/server/intro.md",
    "content": "---\ntitle: Server Introduction\n---\n\n# Server Introduction\n\nClaude Code Router Server is a core service component responsible for routing Claude Code API requests to different LLM providers. It provides a complete HTTP API with support for:\n\n- **API Request Routing**: Convert Anthropic-format requests to various provider API formats\n- **Authentication & Authorization**: Support API Key authentication\n- **Configuration Management**: Dynamic configuration of providers, routing rules, and transformers\n- **Web UI**: Built-in management interface\n- **Logging System**: Complete request logging\n\n## Architecture Overview\n\n```\n┌─────────────┐     ┌─────────────────────────────┐     ┌──────────────┐\n│ Claude Code │────▶│ CCR Server                  │────▶│ LLM Provider │\n│   Client    │     │  ┌─────────────────────┐    │     │  (OpenAI/    │\n└─────────────┘     │  │ @musistudio/llms    │    │     │   Gemini/etc)│\n                    │  │ (Core Package)       │    │     └──────────────┘\n                    │  │ - Request Transform  │    │\n                    │  │ - Response Transform │    │\n                    │  │ - Auth Handling      │    │\n                    │  └─────────────────────┘    │\n                    │                             │\n                    │  - Routing Logic            │\n                    │  - Agent System             │\n                    │  - Configuration            │\n                    └─────────────────────────────┘\n                           │\n                           ├─ Web UI\n                           ├─ Config API\n                           └─ Logs API\n```\n\n## Core Package: @musistudio/llms\n\nThe server is built on top of **@musistudio/llms**, a universal LLM API transformation library that provides the core request/response transformation capabilities.\n\n### What is @musistudio/llms?\n\n`@musistudio/llms` is a standalone npm package (`@musistudio/llms`) that handles:\n\n- **API Format Conversion**: Transforms between different LLM provider APIs (Anthropic, OpenAI, Gemini, etc.)\n- **Request/Response Transformation**: Converts requests and responses to a unified format\n- **Authentication Handling**: Manages different authentication methods across providers\n- **Streaming Support**: Handles streaming responses from different providers\n- **Transformer System**: Provides an extensible architecture for adding new providers\n\n### Key Concepts\n\n#### 1. Unified Request/Response Format\n\nThe core package defines a unified format (`UnifiedChatRequest`, `UnifiedChatResponse`) that abstracts away provider-specific differences:\n\n```typescript\ninterface UnifiedChatRequest {\n  messages: UnifiedMessage[];\n  model: string;\n  max_tokens?: number;\n  temperature?: number;\n  stream?: boolean;\n  tools?: UnifiedTool[];\n  tool_choice?: any;\n  reasoning?: {\n    effort?: ThinkLevel;\n    max_tokens?: number;\n    enabled?: boolean;\n  };\n}\n```\n\n#### 2. Transformer Interface\n\nAll transformers implement a common interface:\n\n```typescript\ninterface Transformer {\n  transformRequestIn?: (request: UnifiedChatRequest, provider: LLMProvider, context: TransformerContext) => Promise<any>;\n  transformRequestOut?: (request: any, context: TransformerContext) => Promise<UnifiedChatRequest>;\n  transformResponseIn?: (response: Response, context?: TransformerContext) => Promise<Response>;\n  transformResponseOut?: (response: Response, context: TransformerContext) => Promise<Response>;\n  endPoint?: string;\n  name?: string;\n  auth?: (request: any, provider: LLMProvider, context: TransformerContext) => Promise<any>;\n}\n```\n\n#### 3. Built-in Transformers\n\nThe core package includes transformers for:\n- **anthropic**: Anthropic API format\n- **openai**: OpenAI API format\n- **gemini**: Google Gemini API format\n- **deepseek**: DeepSeek API format\n- **groq**: Groq API format\n- **openrouter**: OpenRouter API format\n- And more...\n\n### Integration with CCR Server\n\nThe CCR server integrates `@musistudio/llms` through:\n\n1. **Transformer Service** (`packages/core/src/services/transformer.ts`): Manages transformer registration and instantiation\n2. **Provider Configuration**: Maps provider configs to core package's LLMProvider interface\n3. **Request Pipeline**: Applies transformers in sequence during request processing\n4. **Custom Transformers**: Supports loading external transformer plugins\n\n### Version and Updates\n\nThe current version of `@musistudio/llms` is `1.0.51`. It's published as an independent npm package and can be used standalone or as part of CCR Server.\n\n## Core Features\n\n### 1. Request Routing\n- Token-count-based intelligent routing\n- Project-level routing configuration\n- Custom routing functions\n- Scenario-based routing (background, think, longContext, etc.)\n\n### 2. Request Transformation\n- Supports API format conversion for multiple LLM providers\n- Built-in transformers: Anthropic, DeepSeek, Gemini, OpenRouter, Groq, etc.\n- Extensible transformer system\n\n### 3. Agent System\n- Plugin-based Agent architecture\n- Built-in image processing Agent\n- Custom Agent support\n\n### 4. Configuration Management\n- JSON5 format configuration file\n- Environment variable interpolation\n- Hot configuration reload (requires service restart)\n\n## Use Cases\n\n### Scenario 1: Personal Local Service\nRun the service locally for personal Claude Code use:\n\n```bash\nccr start\n```\n\n### Scenario 2: Team Shared Service\nDeploy using Docker to provide shared service for team members:\n\n```bash\ndocker run -d -p 3456:3456 musistudio/claude-code-router\n```\n\n### Scenario 3: Secondary Development\nBuild custom applications based on exposed APIs:\n\n```bash\nGET /api/config\nPOST /v1/messages\nGET /api/logs\n```\n\n## Next Steps\n\n- [Docker Deployment Guide](/docs/server/deployment) - Learn how to deploy the service\n- [API Reference](/docs/category/api) - View complete API documentation\n- [Configuration Guide](/docs/category/server-config) - Understand server configuration options\n"
  },
  {
    "path": "docs/docusaurus.config.ts",
    "content": "import type { Config } from '@docusaurus/types';\nimport type * as Preset from '@docusaurus/preset-classic';\nimport { themes as prismThemes } from 'prism-react-renderer';\n\nconst config: Config = {\n  title: 'Claude Code Router',\n  tagline: 'Use Claude Code without an Anthropics account and route it to another LLM provider',\n  favicon: 'img/favicon.ico',\n\n  url: 'https://musistudio.github.io',\n  baseUrl: '/claude-code-router/',\n\n  organizationName: 'musistudio',\n  projectName: 'claude-code-router',\n\n  onBrokenLinks: 'warn',\n  onBrokenMarkdownLinks: 'warn',\n  onBrokenAnchors: 'warn',\n\n  i18n: {\n    defaultLocale: 'en',\n    locales: ['en', 'zh-CN'],\n  },\n\n  presets: [\n    [\n      'classic',\n      {\n        docs: {\n          sidebarPath: './sidebars.ts',\n          editUrl:\n            'https://github.com/musistudio/claude-code-router/tree/main/docs',\n        },\n        blog: {\n          showReadingTime: true,\n          editUrl:\n            'https://github.com/musistudio/claude-code-router/tree/main/docs',\n        },\n        theme: {\n          customCss: './src/css/custom.css',\n        },\n      } satisfies Preset.Options,\n    ],\n  ],\n\n  themeConfig: {\n    // Disable dark mode\n    colorMode: {\n      defaultMode: 'light',\n      disableSwitch: true,\n      respectPrefersColorScheme: false,\n    },\n\n    image: 'img/docusaurus-social-card.jpg',\n    navbar: {\n      title: 'Claude Code Router',\n      logo: {\n        alt: 'Claude Code Router Logo',\n        src: 'img/ccr.svg',\n        width: 32,\n        height: 32,\n      },\n      items: [\n        {\n          type: 'docSidebar',\n          sidebarId: 'tutorialSidebar',\n          position: 'left',\n          label: 'Documentation',\n        },\n        { to: '/blog', label: 'Blog', position: 'left' },\n        {\n          type: 'localeDropdown',\n          position: 'right',\n        },\n        {\n          href: 'https://github.com/musistudio/claude-code-router',\n          label: 'GitHub',\n          position: 'right',\n        },\n      ],\n    },\n    footer: {\n      style: 'light',\n      links: [],\n      copyright: `Copyright © ${new Date().getFullYear()} Claude Code Router. Built with Docusaurus.`,\n    },\n    prism: {\n      theme: prismThemes.github,\n      darkTheme: prismThemes.dracula,\n      additionalLanguages: ['bash', 'typescript', 'javascript', 'json'],\n    },\n  } satisfies Preset.ThemeConfig,\n};\n\nexport default config;\n"
  },
  {
    "path": "docs/i18n/en/code.json",
    "content": "{\n  \"theme.ErrorPageContent.title\": {\n    \"message\": \"This page crashed.\",\n    \"description\": \"The title of the fallback page when the page crashed\"\n  },\n  \"theme.BackToTopButton.buttonAriaLabel\": {\n    \"message\": \"Scroll back to top\",\n    \"description\": \"The ARIA label for the back to top button\"\n  },\n  \"theme.blog.archive.title\": {\n    \"message\": \"Archive\",\n    \"description\": \"The page & hero title of the blog archive page\"\n  },\n  \"theme.blog.archive.description\": {\n    \"message\": \"Archive\",\n    \"description\": \"The page & hero description of the blog archive page\"\n  },\n  \"theme.blog.paginator.navAriaLabel\": {\n    \"message\": \"Blog list page navigation\",\n    \"description\": \"The ARIA label for the blog pagination\"\n  },\n  \"theme.blog.paginator.newerEntries\": {\n    \"message\": \"Newer entries\",\n    \"description\": \"The label used to navigate to the newer blog posts page (previous page)\"\n  },\n  \"theme.blog.paginator.olderEntries\": {\n    \"message\": \"Older entries\",\n    \"description\": \"The label used to navigate to the older blog posts page (next page)\"\n  },\n  \"theme.blog.post.paginator.navAriaLabel\": {\n    \"message\": \"Blog post page navigation\",\n    \"description\": \"The ARIA label for the blog posts pagination\"\n  },\n  \"theme.blog.post.paginator.newerPost\": {\n    \"message\": \"Newer post\",\n    \"description\": \"The blog post button label to navigate to the newer/previous post\"\n  },\n  \"theme.blog.post.paginator.olderPost\": {\n    \"message\": \"Older post\",\n    \"description\": \"The blog post button label to navigate to the older/next post\"\n  },\n  \"theme.tags.tagsPageLink\": {\n    \"message\": \"View all tags\",\n    \"description\": \"The label of the link targeting the tag list page\"\n  },\n  \"theme.colorToggle.ariaLabel.mode.system\": {\n    \"message\": \"system mode\",\n    \"description\": \"The name for the system color mode\"\n  },\n  \"theme.colorToggle.ariaLabel.mode.light\": {\n    \"message\": \"light mode\",\n    \"description\": \"The name for the light color mode\"\n  },\n  \"theme.colorToggle.ariaLabel.mode.dark\": {\n    \"message\": \"dark mode\",\n    \"description\": \"The name for the dark color mode\"\n  },\n  \"theme.colorToggle.ariaLabel\": {\n    \"message\": \"Switch between dark and light mode (currently {mode})\",\n    \"description\": \"The ARIA label for the color mode toggle\"\n  },\n  \"theme.docs.breadcrumbs.navAriaLabel\": {\n    \"message\": \"Breadcrumbs\",\n    \"description\": \"The ARIA label for the breadcrumbs\"\n  },\n  \"theme.docs.DocCard.categoryDescription.plurals\": {\n    \"message\": \"1 item|{count} items\",\n    \"description\": \"The default description for a category card in the generated index about how many items this category includes\"\n  },\n  \"theme.docs.paginator.navAriaLabel\": {\n    \"message\": \"Docs pages\",\n    \"description\": \"The ARIA label for the docs pagination\"\n  },\n  \"theme.docs.paginator.previous\": {\n    \"message\": \"Previous\",\n    \"description\": \"The label used to navigate to the previous doc\"\n  },\n  \"theme.docs.paginator.next\": {\n    \"message\": \"Next\",\n    \"description\": \"The label used to navigate to the next doc\"\n  },\n  \"theme.docs.tagDocListPageTitle.nDocsTagged\": {\n    \"message\": \"One doc tagged|{count} docs tagged\",\n    \"description\": \"Pluralized label for \\\"{count} docs tagged\\\". Use as much plural forms (separated by \\\"|\\\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)\"\n  },\n  \"theme.docs.tagDocListPageTitle\": {\n    \"message\": \"{nDocsTagged} with \\\"{tagName}\\\"\",\n    \"description\": \"The title of the page for a docs tag\"\n  },\n  \"theme.docs.versionBadge.label\": {\n    \"message\": \"Version: {versionLabel}\"\n  },\n  \"theme.docs.versions.unreleasedVersionLabel\": {\n    \"message\": \"This is unreleased documentation for {siteTitle} {versionLabel} version.\",\n    \"description\": \"The label used to tell the user that he's browsing an unreleased doc version\"\n  },\n  \"theme.docs.versions.unmaintainedVersionLabel\": {\n    \"message\": \"This is documentation for {siteTitle} {versionLabel}, which is no longer actively maintained.\",\n    \"description\": \"The label used to tell the user that he's browsing an unmaintained doc version\"\n  },\n  \"theme.docs.versions.latestVersionSuggestionLabel\": {\n    \"message\": \"For up-to-date documentation, see the {latestVersionLink} ({versionLabel}).\",\n    \"description\": \"The label used to tell the user to check the latest version\"\n  },\n  \"theme.docs.versions.latestVersionLinkLabel\": {\n    \"message\": \"latest version\",\n    \"description\": \"The label used for the latest version suggestion link label\"\n  },\n  \"theme.common.editThisPage\": {\n    \"message\": \"Edit this page\",\n    \"description\": \"The link label to edit the current page\"\n  },\n  \"theme.common.headingLinkTitle\": {\n    \"message\": \"Direct link to {heading}\",\n    \"description\": \"Title for link to heading\"\n  },\n  \"theme.lastUpdated.atDate\": {\n    \"message\": \" on {date}\",\n    \"description\": \"The words used to describe on which date a page has been last updated\"\n  },\n  \"theme.lastUpdated.byUser\": {\n    \"message\": \" by {user}\",\n    \"description\": \"The words used to describe by who the page has been last updated\"\n  },\n  \"theme.lastUpdated.lastUpdatedAtBy\": {\n    \"message\": \"Last updated{atDate}{byUser}\",\n    \"description\": \"The sentence used to display when a page has been last updated, and by who\"\n  },\n  \"theme.NotFound.title\": {\n    \"message\": \"Page Not Found\",\n    \"description\": \"The title of the 404 page\"\n  },\n  \"theme.navbar.mobileVersionsDropdown.label\": {\n    \"message\": \"Versions\",\n    \"description\": \"The label for the navbar versions dropdown on mobile view\"\n  },\n  \"theme.tags.tagsListLabel\": {\n    \"message\": \"Tags:\",\n    \"description\": \"The label alongside a tag list\"\n  },\n  \"theme.AnnouncementBar.closeButtonAriaLabel\": {\n    \"message\": \"Close\",\n    \"description\": \"The ARIA label for close button of announcement bar\"\n  },\n  \"theme.admonition.caution\": {\n    \"message\": \"caution\",\n    \"description\": \"The default label used for the Caution admonition (:::caution)\"\n  },\n  \"theme.admonition.danger\": {\n    \"message\": \"danger\",\n    \"description\": \"The default label used for the Danger admonition (:::danger)\"\n  },\n  \"theme.admonition.info\": {\n    \"message\": \"info\",\n    \"description\": \"The default label used for the Info admonition (:::info)\"\n  },\n  \"theme.admonition.note\": {\n    \"message\": \"note\",\n    \"description\": \"The default label used for the Note admonition (:::note)\"\n  },\n  \"theme.admonition.tip\": {\n    \"message\": \"tip\",\n    \"description\": \"The default label used for the Tip admonition (:::tip)\"\n  },\n  \"theme.admonition.warning\": {\n    \"message\": \"warning\",\n    \"description\": \"The default label used for the Warning admonition (:::warning)\"\n  },\n  \"theme.blog.sidebar.navAriaLabel\": {\n    \"message\": \"Blog recent posts navigation\",\n    \"description\": \"The ARIA label for recent posts in the blog sidebar\"\n  },\n  \"theme.DocSidebarItem.expandCategoryAriaLabel\": {\n    \"message\": \"Expand sidebar category '{label}'\",\n    \"description\": \"The ARIA label to expand the sidebar category\"\n  },\n  \"theme.DocSidebarItem.collapseCategoryAriaLabel\": {\n    \"message\": \"Collapse sidebar category '{label}'\",\n    \"description\": \"The ARIA label to collapse the sidebar category\"\n  },\n  \"theme.IconExternalLink.ariaLabel\": {\n    \"message\": \"(opens in new tab)\",\n    \"description\": \"The ARIA label for the external link icon\"\n  },\n  \"theme.NavBar.navAriaLabel\": {\n    \"message\": \"Main\",\n    \"description\": \"The ARIA label for the main navigation\"\n  },\n  \"theme.NotFound.p1\": {\n    \"message\": \"We could not find what you were looking for.\",\n    \"description\": \"The first paragraph of the 404 page\"\n  },\n  \"theme.NotFound.p2\": {\n    \"message\": \"Please contact the owner of the site that linked you to the original URL and let them know their link is broken.\",\n    \"description\": \"The 2nd paragraph of the 404 page\"\n  },\n  \"theme.navbar.mobileLanguageDropdown.label\": {\n    \"message\": \"Languages\",\n    \"description\": \"The label for the mobile language switcher dropdown\"\n  },\n  \"theme.TOCCollapsible.toggleButtonLabel\": {\n    \"message\": \"On this page\",\n    \"description\": \"The label used by the button on the collapsible TOC component\"\n  },\n  \"theme.blog.post.readMore\": {\n    \"message\": \"Read more\",\n    \"description\": \"The label used in blog post item excerpts to link to full blog posts\"\n  },\n  \"theme.blog.post.readMoreLabel\": {\n    \"message\": \"Read more about {title}\",\n    \"description\": \"The ARIA label for the link to full blog posts from excerpts\"\n  },\n  \"theme.blog.post.readingTime.plurals\": {\n    \"message\": \"One min read|{readingTime} min read\",\n    \"description\": \"Pluralized label for \\\"{readingTime} min read\\\". Use as much plural forms (separated by \\\"|\\\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)\"\n  },\n  \"theme.CodeBlock.copy\": {\n    \"message\": \"Copy\",\n    \"description\": \"The copy button label on code blocks\"\n  },\n  \"theme.CodeBlock.copied\": {\n    \"message\": \"Copied\",\n    \"description\": \"The copied button label on code blocks\"\n  },\n  \"theme.CodeBlock.copyButtonAriaLabel\": {\n    \"message\": \"Copy code to clipboard\",\n    \"description\": \"The ARIA label for copy code blocks button\"\n  },\n  \"theme.CodeBlock.wordWrapToggle\": {\n    \"message\": \"Toggle word wrap\",\n    \"description\": \"The title attribute for toggle word wrapping button of code block lines\"\n  },\n  \"theme.docs.breadcrumbs.home\": {\n    \"message\": \"Home page\",\n    \"description\": \"The ARIA label for the home page in the breadcrumbs\"\n  },\n  \"theme.docs.sidebar.navAriaLabel\": {\n    \"message\": \"Docs sidebar\",\n    \"description\": \"The ARIA label for the sidebar navigation\"\n  },\n  \"theme.docs.sidebar.collapseButtonTitle\": {\n    \"message\": \"Collapse sidebar\",\n    \"description\": \"The title attribute for collapse button of doc sidebar\"\n  },\n  \"theme.docs.sidebar.collapseButtonAriaLabel\": {\n    \"message\": \"Collapse sidebar\",\n    \"description\": \"The title attribute for collapse button of doc sidebar\"\n  },\n  \"theme.docs.sidebar.closeSidebarButtonAriaLabel\": {\n    \"message\": \"Close navigation bar\",\n    \"description\": \"The ARIA label for close button of mobile sidebar\"\n  },\n  \"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel\": {\n    \"message\": \"← Back to main menu\",\n    \"description\": \"The label of the back button to return to main menu, inside the mobile navbar sidebar secondary menu (notably used to display the docs sidebar)\"\n  },\n  \"theme.docs.sidebar.toggleSidebarButtonAriaLabel\": {\n    \"message\": \"Toggle navigation bar\",\n    \"description\": \"The ARIA label for hamburger menu button of mobile navigation\"\n  },\n  \"theme.navbar.mobileDropdown.collapseButton.expandAriaLabel\": {\n    \"message\": \"Expand the dropdown\",\n    \"description\": \"The ARIA label of the button to expand the mobile dropdown navbar item\"\n  },\n  \"theme.navbar.mobileDropdown.collapseButton.collapseAriaLabel\": {\n    \"message\": \"Collapse the dropdown\",\n    \"description\": \"The ARIA label of the button to collapse the mobile dropdown navbar item\"\n  },\n  \"theme.docs.sidebar.expandButtonTitle\": {\n    \"message\": \"Expand sidebar\",\n    \"description\": \"The ARIA label and title attribute for expand button of doc sidebar\"\n  },\n  \"theme.docs.sidebar.expandButtonAriaLabel\": {\n    \"message\": \"Expand sidebar\",\n    \"description\": \"The ARIA label and title attribute for expand button of doc sidebar\"\n  },\n  \"theme.blog.post.plurals\": {\n    \"message\": \"One post|{count} posts\",\n    \"description\": \"Pluralized label for \\\"{count} posts\\\". Use as much plural forms (separated by \\\"|\\\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)\"\n  },\n  \"theme.blog.tagTitle\": {\n    \"message\": \"{nPosts} tagged with \\\"{tagName}\\\"\",\n    \"description\": \"The title of the page for a blog tag\"\n  },\n  \"theme.blog.author.pageTitle\": {\n    \"message\": \"{authorName} - {nPosts}\",\n    \"description\": \"The title of the page for a blog author\"\n  },\n  \"theme.blog.authorsList.pageTitle\": {\n    \"message\": \"Authors\",\n    \"description\": \"The title of the authors page\"\n  },\n  \"theme.blog.authorsList.viewAll\": {\n    \"message\": \"View all authors\",\n    \"description\": \"The label of the link targeting the blog authors page\"\n  },\n  \"theme.blog.author.noPosts\": {\n    \"message\": \"This author has not written any posts yet.\",\n    \"description\": \"The text for authors with 0 blog post\"\n  },\n  \"theme.contentVisibility.unlistedBanner.title\": {\n    \"message\": \"Unlisted page\",\n    \"description\": \"The unlisted content banner title\"\n  },\n  \"theme.contentVisibility.unlistedBanner.message\": {\n    \"message\": \"This page is unlisted. Search engines will not index it, and only users having a direct link can access it.\",\n    \"description\": \"The unlisted content banner message\"\n  },\n  \"theme.contentVisibility.draftBanner.title\": {\n    \"message\": \"Draft page\",\n    \"description\": \"The draft content banner title\"\n  },\n  \"theme.contentVisibility.draftBanner.message\": {\n    \"message\": \"This page is a draft. It will only be visible in dev and be excluded from the production build.\",\n    \"description\": \"The draft content banner message\"\n  },\n  \"theme.ErrorPageContent.tryAgain\": {\n    \"message\": \"Try again\",\n    \"description\": \"The label of the button to try again rendering when the React error boundary captures an error\"\n  },\n  \"theme.common.skipToMainContent\": {\n    \"message\": \"Skip to main content\",\n    \"description\": \"The skip to content label used for accessibility, allowing to rapidly navigate to main content with keyboard tab/enter navigation\"\n  },\n  \"theme.tags.tagsPageTitle\": {\n    \"message\": \"Tags\",\n    \"description\": \"The title of the tag list page\"\n  }\n}\n"
  },
  {
    "path": "docs/i18n/en/docusaurus-plugin-content-blog/options.json",
    "content": "{\n  \"title\": {\n    \"message\": \"Blog\",\n    \"description\": \"The title for the blog used in SEO\"\n  },\n  \"description\": {\n    \"message\": \"Blog\",\n    \"description\": \"The description for the blog used in SEO\"\n  },\n  \"sidebar.title\": {\n    \"message\": \"Recent posts\",\n    \"description\": \"The label for the left sidebar\"\n  }\n}\n"
  },
  {
    "path": "docs/i18n/en/docusaurus-plugin-content-docs/current.json",
    "content": "{\n  \"version.label\": {\n    \"message\": \"Next\",\n    \"description\": \"The label for version current\"\n  },\n  \"sidebar.tutorialSidebar.category.Server\": {\n    \"message\": \"Server\",\n    \"description\": \"The label for category 'Server' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.Server.link.generated-index.title\": {\n    \"message\": \"Claude Code Router Server\",\n    \"description\": \"The generated-index page title for category 'Server' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.Server.link.generated-index.description\": {\n    \"message\": \"Deploy and manage Claude Code Router server\",\n    \"description\": \"The generated-index page description for category 'Server' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.API Reference\": {\n    \"message\": \"API Reference\",\n    \"description\": \"The label for category 'API Reference' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.API Reference.link.generated-index.title\": {\n    \"message\": \"API Reference\",\n    \"description\": \"The generated-index page title for category 'API Reference' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.API Reference.link.generated-index.description\": {\n    \"message\": \"Server API documentation\",\n    \"description\": \"The generated-index page description for category 'API Reference' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.server-configuration-category\": {\n    \"message\": \"Configuration\",\n    \"description\": \"The label for category 'Configuration' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.server-configuration-category.link.generated-index.title\": {\n    \"message\": \"Server Configuration\",\n    \"description\": \"The generated-index page title for category 'Configuration' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.server-configuration-category.link.generated-index.description\": {\n    \"message\": \"Server configuration guide\",\n    \"description\": \"The generated-index page description for category 'Configuration' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.Advanced\": {\n    \"message\": \"Advanced\",\n    \"description\": \"The label for category 'Advanced' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.Advanced.link.generated-index.title\": {\n    \"message\": \"Advanced Topics\",\n    \"description\": \"The generated-index page title for category 'Advanced' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.Advanced.link.generated-index.description\": {\n    \"message\": \"Advanced features and customization\",\n    \"description\": \"The generated-index page description for category 'Advanced' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.CLI\": {\n    \"message\": \"CLI\",\n    \"description\": \"The label for category 'CLI' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.CLI.link.generated-index.title\": {\n    \"message\": \"Claude Code Router CLI\",\n    \"description\": \"The generated-index page title for category 'CLI' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.CLI.link.generated-index.description\": {\n    \"message\": \"Command-line tool usage guide\",\n    \"description\": \"The generated-index page description for category 'CLI' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.Commands\": {\n    \"message\": \"Commands\",\n    \"description\": \"The label for category 'Commands' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.Commands.link.generated-index.title\": {\n    \"message\": \"CLI Commands\",\n    \"description\": \"The generated-index page title for category 'Commands' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.Commands.link.generated-index.description\": {\n    \"message\": \"Complete command reference\",\n    \"description\": \"The generated-index page description for category 'Commands' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.cli-configuration-category\": {\n    \"message\": \"Configuration\",\n    \"description\": \"The label for category 'Configuration' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.cli-configuration-category.link.generated-index.title\": {\n    \"message\": \"CLI Configuration\",\n    \"description\": \"The generated-index page title for category 'Configuration' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.cli-configuration-category.link.generated-index.description\": {\n    \"message\": \"CLI configuration guide\",\n    \"description\": \"The generated-index page description for category 'Configuration' in sidebar 'tutorialSidebar'\"\n  }\n}\n"
  },
  {
    "path": "docs/i18n/en/docusaurus-theme-classic/footer.json",
    "content": "{\n  \"link.title.Docs\": {\n    \"message\": \"Docs\",\n    \"description\": \"The title of the footer links column with title=Docs in the footer\"\n  },\n  \"link.title.Community\": {\n    \"message\": \"Community\",\n    \"description\": \"The title of the footer links column with title=Community in the footer\"\n  },\n  \"link.title.More\": {\n    \"message\": \"More\",\n    \"description\": \"The title of the footer links column with title=More in the footer\"\n  },\n  \"link.item.label.Tutorial\": {\n    \"message\": \"Tutorial\",\n    \"description\": \"The label of footer link with label=Tutorial linking to /docs/intro\"\n  },\n  \"link.item.label.GitHub\": {\n    \"message\": \"GitHub\",\n    \"description\": \"The label of footer link with label=GitHub linking to https://github.com/musistudio/claude-code-router\"\n  },\n  \"link.item.label.Blog\": {\n    \"message\": \"Blog\",\n    \"description\": \"The label of footer link with label=Blog linking to /blog\"\n  },\n  \"copyright\": {\n    \"message\": \"Copyright © 2026 Claude Code Router. Built with Docusaurus.\",\n    \"description\": \"The footer copyright\"\n  }\n}\n"
  },
  {
    "path": "docs/i18n/en/docusaurus-theme-classic/navbar.json",
    "content": "{\n  \"title\": {\n    \"message\": \"Claude Code Router\",\n    \"description\": \"The title in the navbar\"\n  },\n  \"logo.alt\": {\n    \"message\": \"Claude Code Router Logo\",\n    \"description\": \"The alt text of navbar logo\"\n  },\n  \"item.label.Documentation\": {\n    \"message\": \"Documentation\",\n    \"description\": \"Navbar item with label Documentation\"\n  },\n  \"item.label.Blog\": {\n    \"message\": \"Blog\",\n    \"description\": \"Navbar item with label Blog\"\n  },\n  \"item.label.GitHub\": {\n    \"message\": \"GitHub\",\n    \"description\": \"Navbar item with label GitHub\"\n  }\n}\n"
  },
  {
    "path": "docs/i18n/zh-CN/code.json",
    "content": "{\n  \"theme.ErrorPageContent.title\": {\n    \"message\": \"页面已崩溃。\",\n    \"description\": \"The title of the fallback page when the page crashed\"\n  },\n  \"theme.BackToTopButton.buttonAriaLabel\": {\n    \"message\": \"回到顶部\",\n    \"description\": \"The ARIA label for the back to top button\"\n  },\n  \"theme.blog.archive.title\": {\n    \"message\": \"历史博文\",\n    \"description\": \"The page & hero title of the blog archive page\"\n  },\n  \"theme.blog.archive.description\": {\n    \"message\": \"历史博文\",\n    \"description\": \"The page & hero description of the blog archive page\"\n  },\n  \"theme.blog.paginator.navAriaLabel\": {\n    \"message\": \"博文列表分页导航\",\n    \"description\": \"The ARIA label for the blog pagination\"\n  },\n  \"theme.blog.paginator.newerEntries\": {\n    \"message\": \"较新的博文\",\n    \"description\": \"The label used to navigate to the newer blog posts page (previous page)\"\n  },\n  \"theme.blog.paginator.olderEntries\": {\n    \"message\": \"较旧的博文\",\n    \"description\": \"The label used to navigate to the older blog posts page (next page)\"\n  },\n  \"theme.blog.post.paginator.navAriaLabel\": {\n    \"message\": \"博文分页导航\",\n    \"description\": \"The ARIA label for the blog posts pagination\"\n  },\n  \"theme.blog.post.paginator.newerPost\": {\n    \"message\": \"较新一篇\",\n    \"description\": \"The blog post button label to navigate to the newer/previous post\"\n  },\n  \"theme.blog.post.paginator.olderPost\": {\n    \"message\": \"较旧一篇\",\n    \"description\": \"The blog post button label to navigate to the older/next post\"\n  },\n  \"theme.tags.tagsPageLink\": {\n    \"message\": \"查看所有标签\",\n    \"description\": \"The label of the link targeting the tag list page\"\n  },\n  \"theme.colorToggle.ariaLabel.mode.system\": {\n    \"message\": \"system mode\",\n    \"description\": \"The name for the system color mode\"\n  },\n  \"theme.colorToggle.ariaLabel.mode.light\": {\n    \"message\": \"浅色模式\",\n    \"description\": \"The name for the light color mode\"\n  },\n  \"theme.colorToggle.ariaLabel.mode.dark\": {\n    \"message\": \"暗黑模式\",\n    \"description\": \"The name for the dark color mode\"\n  },\n  \"theme.colorToggle.ariaLabel\": {\n    \"message\": \"切换浅色/暗黑模式（当前为{mode}）\",\n    \"description\": \"The ARIA label for the color mode toggle\"\n  },\n  \"theme.docs.breadcrumbs.navAriaLabel\": {\n    \"message\": \"页面路径\",\n    \"description\": \"The ARIA label for the breadcrumbs\"\n  },\n  \"theme.docs.DocCard.categoryDescription.plurals\": {\n    \"message\": \"{count} 个项目\",\n    \"description\": \"The default description for a category card in the generated index about how many items this category includes\"\n  },\n  \"theme.docs.paginator.navAriaLabel\": {\n    \"message\": \"文件选项卡\",\n    \"description\": \"The ARIA label for the docs pagination\"\n  },\n  \"theme.docs.paginator.previous\": {\n    \"message\": \"上一页\",\n    \"description\": \"The label used to navigate to the previous doc\"\n  },\n  \"theme.docs.paginator.next\": {\n    \"message\": \"下一页\",\n    \"description\": \"The label used to navigate to the next doc\"\n  },\n  \"theme.docs.tagDocListPageTitle.nDocsTagged\": {\n    \"message\": \"{count} 篇文档带有标签\",\n    \"description\": \"Pluralized label for \\\"{count} docs tagged\\\". Use as much plural forms (separated by \\\"|\\\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)\"\n  },\n  \"theme.docs.tagDocListPageTitle\": {\n    \"message\": \"{nDocsTagged}「{tagName}」\",\n    \"description\": \"The title of the page for a docs tag\"\n  },\n  \"theme.docs.versionBadge.label\": {\n    \"message\": \"版本：{versionLabel}\"\n  },\n  \"theme.docs.versions.unreleasedVersionLabel\": {\n    \"message\": \"此为 {siteTitle} {versionLabel} 版尚未发行的文档。\",\n    \"description\": \"The label used to tell the user that he's browsing an unreleased doc version\"\n  },\n  \"theme.docs.versions.unmaintainedVersionLabel\": {\n    \"message\": \"此为 {siteTitle} {versionLabel} 版的文档，现已不再积极维护。\",\n    \"description\": \"The label used to tell the user that he's browsing an unmaintained doc version\"\n  },\n  \"theme.docs.versions.latestVersionSuggestionLabel\": {\n    \"message\": \"最新的文档请参阅 {latestVersionLink} ({versionLabel})。\",\n    \"description\": \"The label used to tell the user to check the latest version\"\n  },\n  \"theme.docs.versions.latestVersionLinkLabel\": {\n    \"message\": \"最新版本\",\n    \"description\": \"The label used for the latest version suggestion link label\"\n  },\n  \"theme.common.headingLinkTitle\": {\n    \"message\": \"{heading}的直接链接\",\n    \"description\": \"Title for link to heading\"\n  },\n  \"theme.common.editThisPage\": {\n    \"message\": \"编辑此页\",\n    \"description\": \"The link label to edit the current page\"\n  },\n  \"theme.lastUpdated.atDate\": {\n    \"message\": \"于 {date} \",\n    \"description\": \"The words used to describe on which date a page has been last updated\"\n  },\n  \"theme.lastUpdated.byUser\": {\n    \"message\": \"由 {user} \",\n    \"description\": \"The words used to describe by who the page has been last updated\"\n  },\n  \"theme.lastUpdated.lastUpdatedAtBy\": {\n    \"message\": \"最后{byUser}{atDate}更新\",\n    \"description\": \"The sentence used to display when a page has been last updated, and by who\"\n  },\n  \"theme.NotFound.title\": {\n    \"message\": \"找不到页面\",\n    \"description\": \"The title of the 404 page\"\n  },\n  \"theme.navbar.mobileVersionsDropdown.label\": {\n    \"message\": \"选择版本\",\n    \"description\": \"The label for the navbar versions dropdown on mobile view\"\n  },\n  \"theme.tags.tagsListLabel\": {\n    \"message\": \"标签：\",\n    \"description\": \"The label alongside a tag list\"\n  },\n  \"theme.AnnouncementBar.closeButtonAriaLabel\": {\n    \"message\": \"关闭\",\n    \"description\": \"The ARIA label for close button of announcement bar\"\n  },\n  \"theme.admonition.caution\": {\n    \"message\": \"警告\",\n    \"description\": \"The default label used for the Caution admonition (:::caution)\"\n  },\n  \"theme.admonition.danger\": {\n    \"message\": \"危险\",\n    \"description\": \"The default label used for the Danger admonition (:::danger)\"\n  },\n  \"theme.admonition.info\": {\n    \"message\": \"信息\",\n    \"description\": \"The default label used for the Info admonition (:::info)\"\n  },\n  \"theme.admonition.note\": {\n    \"message\": \"备注\",\n    \"description\": \"The default label used for the Note admonition (:::note)\"\n  },\n  \"theme.admonition.tip\": {\n    \"message\": \"提示\",\n    \"description\": \"The default label used for the Tip admonition (:::tip)\"\n  },\n  \"theme.admonition.warning\": {\n    \"message\": \"注意\",\n    \"description\": \"The default label used for the Warning admonition (:::warning)\"\n  },\n  \"theme.blog.sidebar.navAriaLabel\": {\n    \"message\": \"最近博文导航\",\n    \"description\": \"The ARIA label for recent posts in the blog sidebar\"\n  },\n  \"theme.DocSidebarItem.expandCategoryAriaLabel\": {\n    \"message\": \"展开侧边栏分类 '{label}'\",\n    \"description\": \"The ARIA label to expand the sidebar category\"\n  },\n  \"theme.DocSidebarItem.collapseCategoryAriaLabel\": {\n    \"message\": \"折叠侧边栏分类 '{label}'\",\n    \"description\": \"The ARIA label to collapse the sidebar category\"\n  },\n  \"theme.IconExternalLink.ariaLabel\": {\n    \"message\": \"(opens in new tab)\",\n    \"description\": \"The ARIA label for the external link icon\"\n  },\n  \"theme.NavBar.navAriaLabel\": {\n    \"message\": \"主导航\",\n    \"description\": \"The ARIA label for the main navigation\"\n  },\n  \"theme.NotFound.p1\": {\n    \"message\": \"我们找不到您要找的页面。\",\n    \"description\": \"The first paragraph of the 404 page\"\n  },\n  \"theme.NotFound.p2\": {\n    \"message\": \"请联系原始链接来源网站的所有者，并告知他们链接已损坏。\",\n    \"description\": \"The 2nd paragraph of the 404 page\"\n  },\n  \"theme.navbar.mobileLanguageDropdown.label\": {\n    \"message\": \"选择语言\",\n    \"description\": \"The label for the mobile language switcher dropdown\"\n  },\n  \"theme.TOCCollapsible.toggleButtonLabel\": {\n    \"message\": \"本页总览\",\n    \"description\": \"The label used by the button on the collapsible TOC component\"\n  },\n  \"theme.blog.post.readMore\": {\n    \"message\": \"阅读更多\",\n    \"description\": \"The label used in blog post item excerpts to link to full blog posts\"\n  },\n  \"theme.blog.post.readMoreLabel\": {\n    \"message\": \"阅读 {title} 的全文\",\n    \"description\": \"The ARIA label for the link to full blog posts from excerpts\"\n  },\n  \"theme.CodeBlock.copy\": {\n    \"message\": \"复制\",\n    \"description\": \"The copy button label on code blocks\"\n  },\n  \"theme.CodeBlock.copied\": {\n    \"message\": \"复制成功\",\n    \"description\": \"The copied button label on code blocks\"\n  },\n  \"theme.CodeBlock.copyButtonAriaLabel\": {\n    \"message\": \"复制代码到剪贴板\",\n    \"description\": \"The ARIA label for copy code blocks button\"\n  },\n  \"theme.CodeBlock.wordWrapToggle\": {\n    \"message\": \"切换自动换行\",\n    \"description\": \"The title attribute for toggle word wrapping button of code block lines\"\n  },\n  \"theme.blog.post.readingTime.plurals\": {\n    \"message\": \"阅读需 {readingTime} 分钟\",\n    \"description\": \"Pluralized label for \\\"{readingTime} min read\\\". Use as much plural forms (separated by \\\"|\\\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)\"\n  },\n  \"theme.docs.breadcrumbs.home\": {\n    \"message\": \"主页面\",\n    \"description\": \"The ARIA label for the home page in the breadcrumbs\"\n  },\n  \"theme.docs.sidebar.navAriaLabel\": {\n    \"message\": \"文档侧边栏\",\n    \"description\": \"The ARIA label for the sidebar navigation\"\n  },\n  \"theme.docs.sidebar.collapseButtonTitle\": {\n    \"message\": \"收起侧边栏\",\n    \"description\": \"The title attribute for collapse button of doc sidebar\"\n  },\n  \"theme.docs.sidebar.collapseButtonAriaLabel\": {\n    \"message\": \"收起侧边栏\",\n    \"description\": \"The title attribute for collapse button of doc sidebar\"\n  },\n  \"theme.docs.sidebar.closeSidebarButtonAriaLabel\": {\n    \"message\": \"关闭导航栏\",\n    \"description\": \"The ARIA label for close button of mobile sidebar\"\n  },\n  \"theme.navbar.mobileDropdown.collapseButton.expandAriaLabel\": {\n    \"message\": \"Expand the dropdown\",\n    \"description\": \"The ARIA label of the button to expand the mobile dropdown navbar item\"\n  },\n  \"theme.navbar.mobileDropdown.collapseButton.collapseAriaLabel\": {\n    \"message\": \"Collapse the dropdown\",\n    \"description\": \"The ARIA label of the button to collapse the mobile dropdown navbar item\"\n  },\n  \"theme.docs.sidebar.toggleSidebarButtonAriaLabel\": {\n    \"message\": \"切换导航栏\",\n    \"description\": \"The ARIA label for hamburger menu button of mobile navigation\"\n  },\n  \"theme.docs.sidebar.expandButtonTitle\": {\n    \"message\": \"展开侧边栏\",\n    \"description\": \"The ARIA label and title attribute for expand button of doc sidebar\"\n  },\n  \"theme.docs.sidebar.expandButtonAriaLabel\": {\n    \"message\": \"展开侧边栏\",\n    \"description\": \"The ARIA label and title attribute for expand button of doc sidebar\"\n  },\n  \"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel\": {\n    \"message\": \"← 回到主菜单\",\n    \"description\": \"The label of the back button to return to main menu, inside the mobile navbar sidebar secondary menu (notably used to display the docs sidebar)\"\n  },\n  \"theme.blog.post.plurals\": {\n    \"message\": \"{count} 篇博文\",\n    \"description\": \"Pluralized label for \\\"{count} posts\\\". Use as much plural forms (separated by \\\"|\\\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)\"\n  },\n  \"theme.blog.tagTitle\": {\n    \"message\": \"{nPosts} 含有标签「{tagName}」\",\n    \"description\": \"The title of the page for a blog tag\"\n  },\n  \"theme.blog.author.pageTitle\": {\n    \"message\": \"{authorName} - {nPosts}\",\n    \"description\": \"The title of the page for a blog author\"\n  },\n  \"theme.blog.authorsList.pageTitle\": {\n    \"message\": \"作者\",\n    \"description\": \"The title of the authors page\"\n  },\n  \"theme.blog.authorsList.viewAll\": {\n    \"message\": \"查看所有作者\",\n    \"description\": \"The label of the link targeting the blog authors page\"\n  },\n  \"theme.blog.author.noPosts\": {\n    \"message\": \"该作者尚未撰写任何文章。\",\n    \"description\": \"The text for authors with 0 blog post\"\n  },\n  \"theme.contentVisibility.unlistedBanner.title\": {\n    \"message\": \"未列出页\",\n    \"description\": \"The unlisted content banner title\"\n  },\n  \"theme.contentVisibility.unlistedBanner.message\": {\n    \"message\": \"此页面未列出。搜索引擎不会对其索引，只有拥有直接链接的用户才能访问。\",\n    \"description\": \"The unlisted content banner message\"\n  },\n  \"theme.contentVisibility.draftBanner.title\": {\n    \"message\": \"草稿页\",\n    \"description\": \"The draft content banner title\"\n  },\n  \"theme.contentVisibility.draftBanner.message\": {\n    \"message\": \"此页面是草稿，仅在开发环境中可见，不会包含在正式版本中。\",\n    \"description\": \"The draft content banner message\"\n  },\n  \"theme.ErrorPageContent.tryAgain\": {\n    \"message\": \"重试\",\n    \"description\": \"The label of the button to try again rendering when the React error boundary captures an error\"\n  },\n  \"theme.common.skipToMainContent\": {\n    \"message\": \"跳到主要内容\",\n    \"description\": \"The skip to content label used for accessibility, allowing to rapidly navigate to main content with keyboard tab/enter navigation\"\n  },\n  \"theme.tags.tagsPageTitle\": {\n    \"message\": \"标签\",\n    \"description\": \"The title of the tag list page\"\n  }\n}\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-blog/2025-02-25-project-motivation.md",
    "content": "---\ntitle: 项目初衷及原理\ndate: 2025-02-25\ntags: [claude-code, 逆向工程, 教程]\n---\n\n# 项目初衷及原理\n\n早在 Claude Code 发布的第二天(2025-02-25)，我就尝试并完成了对该项目的逆向。当时要使用 Claude Code 你需要注册一个 Anthropic 账号，然后申请 waitlist，等待通过后才能使用。但是因为众所周知的原因，Anthropic 屏蔽了中国区的用户，所以通过正常手段我无法使用，通过已知的信息，我发现：\n\n1. Claude Code 使用 npm 进行安装，所以很大可能其使用 Node.js 进行开发。\n2. Node.js 调试手段众多，可以简单使用`console.log`获取想要的信息，也可以使用`--inspect`将其接入`Chrome Devtools`，甚至你可以使用`d8`去调试某些加密混淆的代码。\n\n由于我的目标是让我在没有 Anthropic 账号的情况下使用`Claude Code`，我并不需要获得完整的源代码，只需要将`Claude Code`请求 Anthropic 模型时将其转发到我自定义的接口即可。接下来我就开启了我的逆向过程：\n\n1. 首先安装`Claude Code`\n\n```bash\nnpm install -g @anthropic-ai/claude-code\n```\n\n2. 安装后该项目被放在了`~/.nvm/versions/node/v20.10.0/lib/node_modules/@anthropic-ai/claude-code`中，因为我使用了`nvm`作为我的 node 版本控制器，当前使用`node-v20.10.0`，所以该路径会因人而异。\n3. 找到项目路径之后可通过 package.json 分析包入口,内容如下：\n\n```package.json\n{\n  \"name\": \"@anthropic-ai/claude-code\",\n  \"version\": \"1.0.24\",\n  \"main\": \"sdk.mjs\",\n  \"types\": \"sdk.d.ts\",\n  \"bin\": {\n    \"claude\": \"cli.js\"\n  },\n  \"engines\": {\n    \"node\": \">=18.0.0\"\n  },\n  \"type\": \"module\",\n  \"author\": \"Boris Cherny <boris@anthropic.com>\",\n  \"license\": \"SEE LICENSE IN README.md\",\n  \"description\": \"Use Claude, Anthropic's AI assistant, right from your terminal. Claude can understand your codebase, edit files, run terminal commands, and handle entire workflows for you.\",\n  \"homepage\": \"https://github.com/anthropics/claude-code\",\n  \"bugs\": {\n    \"url\": \"https://github.com/anthropics/claude-code/issues\"\n  },\n  \"scripts\": {\n    \"prepare\": \"node -e \\\"if (!process.env.AUTHORIZED) { console.error('ERROR: Direct publishing is not allowed.\\\\nPlease use the publish-external.sh script to publish this package.'); process.exit(1); }\\\"\",\n    \"preinstall\": \"node scripts/preinstall.js\"\n  },\n  \"dependencies\": {},\n  \"optionalDependencies\": {\n    \"@img/sharp-darwin-arm64\": \"^0.33.5\",\n    \"@img/sharp-darwin-x64\": \"^0.33.5\",\n    \"@img/sharp-linux-arm\": \"^0.33.5\",\n    \"@img/sharp-linux-arm64\": \"^0.33.5\",\n    \"@img/sharp-linux-x64\": \"^0.33.5\",\n    \"@img/sharp-win32-x64\": \"^0.33.5\"\n  }\n}\n```\n\n其中`\"claude\": \"cli.js\"`就是我们要找的入口，打开 cli.js，发现代码被压缩混淆过了。没关系，借助`webstorm`的`Formate File`功能可以重新格式化，让代码变得稍微好看一点。就像这样：\n![webstorm-formate-file](/blog-images/webstorm-formate-file.png)\n\n现在，你可以通过阅读部分代码来了解`Claude Code`的内容工具原理与提示词。你也可以在关键地方使用`console.log`来获得更多信息，当然，也可以使用`Chrome Devtools`来进行断点调试，使用以下命令启动`Claude Code`:\n\n```bash\nNODE_OPTIONS=\"--inspect-brk=9229\" claude\n```\n\n该命令会以调试模式启动`Claude Code`，并将调试的端口设置为`9229`。这时候通过 Chrome 访问`chrome://inspect/`即可看到当前的`Claude Code`进程，点击`inspect`即可进行调试。\n![chrome-devtools](/blog-images/chrome-inspect.png)\n![chrome-devtools](/blog-images/chrome-devtools.png)\n\n通过搜索关键字符`api.anthropic.com`很容易能找到`Claude Code`用来发请求的地方，根据上下文的查看，很容易发现这里的`baseURL`可以通过环境变量`ANTHROPIC_BASE_URL`进行覆盖，`apiKey`和`authToken`也同理。\n![search](/blog-images/search.png)\n\n到目前为止，我们获得关键信息：\n\n1. 可以使用环境变量覆盖`Claude Code`的`BaseURL`和`apiKey`的配置\n\n2. `Claude Code`使用[Anthropic API](https://docs.anthropic.com/en/api/overview)的规范\n\n所以我们需要：\n\n1. 实现一个服务用来将`OpenAI API`的规范转换成`Anthropic API`格式。\n\n2. 启动`Claude Code`之前写入环境变量将`baseURL`指向到该服务。\n\n于是，`claude-code-router`就诞生了，该项目使用`Express.js`作为 HTTP 服务，实现`/v1/messages`端点，使用`middlewares`处理请求/响应的格式转换以及请求重写功能(可以用来重写 Claude Code 的提示词以针对单个模型进行调优)。\n在 2 月份由于`DeepSeek`全系列模型对`Function Call`的支持不佳导致无法直接使用`DeepSeek`模型，所以在当时我选择了`qwen-max`模型，一切表现的都很好，但是`qwen-max`不支持`KV Cache`，意味着我要消耗大量的 token，但是却无法获取`Claude Code`原生的体验。\n所以我又尝试了`Router`模式，即使用一个小模型对任务进行分发，一共分为四个模型:`router`、`tool`、`think`和`coder`，所有的请求先经过一个免费的小模型，由小模型去判断应该是进行思考还是编码还是调用工具，再进行任务的分发，如果是思考和编码任务将会进行循环调用，直到最终使用工具写入或修改文件。但是实践下来发现免费的小模型不足以很好的完成任务的分发，再加上整个 Agnet 的设计存在缺陷，导致并不能很好的驱动`Claude Code`。\n直到 5 月底，`Claude Code`被正式推出，这时`DeepSeek`全系列模型(R1 于 05-28)均支持`Function Call`，我开始重新设计该项目。在与 AI 的结对编程中我修复了之前的请求和响应转换问题，在某些场景下模型输出 JSON 响应而不是`Function Call`。这次直接使用`DeepSeek-v3`模型，它工作的比我想象中要好：能完成绝大多数工具调用，还支持用步骤规划解决任务，最关键的是`DeepSeek`的价格不到`claude Sonnet 3.5`的十分之一。正式发布的`Claude Code`对 Agent 的组织也不同于测试版，于是在分析了`Claude Code`的请求调用之后，我重新组织了`Router`模式：现在它还是四个模型：默认模型、`background`、`think`和`longContext`。\n\n- 默认模型作为最终的兜底和日常处理\n\n- `background`是用来处理一些后台任务，据 Anthropic 官方说主要用`Claude Haiku 3.5`模型去处理一些小任务，如俳句生成和对话摘要，于是我将其路由到了本地的`ollama`服务。\n\n- `think`模型用于让`Claude Code`进行思考或者在`Plan Mode`下使用，这里我使用的是`DeepSeek-R1`，由于其不支持推理成本控制，所以`Think`和`UltraThink`是一样的逻辑。\n\n- `longContext`是用于处理长下上文的场景，该项目会对每次请求使用tiktoken实时计算上下文长度，如果上下文大于32K则使用该模型，旨在弥补`DeepSeek`在长上下文处理不佳的情况。\n\n以上就是该项目的发展历程以及我的一些思考，通过巧妙的使用环境变量覆盖的手段在不修改`Claude Code`源码的情况下完成请求的转发和修改，这就使得在可以得到 Anthropic 更新的同时使用自己的模型，自定义自己的提示词。该项目只是在 Anthropic 封禁中国区用户的情况下使用`Claude Code`并且达到成本和性能平衡的一种手段。如果可以的话，还是官方的Max Plan体验最好。\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-blog/2025-11-18-glm-reasoning.md",
    "content": "---\ntitle: GLM-4.6支持思考及思维链回传\ndate: 2025-11-18\ntags: [glm, 思考, 思维链]\n---\n\n# GLM-4.6支持思考及思维链回传\n\n## GLM-4.6在cluade code中启用思考\nGLM从4.5开始就对claude code进行了支持，我之前也一直在关注，很多用户反映在claude code中无法启用思考，刚好最近收到了来自智谱的赞助，就着手进行研究。\n\n首先根据[官方文档](https://docs.bigmodel.cn/api-reference/%E6%A8%A1%E5%9E%8B-api/%E5%AF%B9%E8%AF%9D%E8%A1%A5%E5%85%A8)，我们发现`/chat/completions`端点是默认启用思考的，但是是由模型判断是否需要进行思考\n\n```\nthinking object\n仅 GLM-4.5 及以上模型支持此参数配置. 控制大模型是否开启思维链。\n\nthinking.type enum<string> default:enabled\n是否开启思维链(当开启后 GLM-4.6 GLM-4.5 为模型自动判断是否思考，GLM-4.5V 为强制思考), 默认: enabled.\n\nAvailable options: enabled, disabled\n```\n\n在claude code本身大量的提示词干扰下，会严重阻碍GLM模型本身的判断机制，导致模型很少进行思考。所以我们需要对模型进行引导，让模型认为需要进行思考。但是`claude-code-router`作为proxy，能做的只能是修改提示词/参数。\n\n在最开始，我尝试直接删除claude code的系统提示词，模型确实进行了思考，但是这样就无法驱动claude code。所以我们需要进行提示词注入，明确告知模型需要进行思考。\n\n```javascript\n// transformer.ts\nimport { UnifiedChatRequest } from \"../types/llm\";\nimport { Transformer } from \"../types/transformer\";\n\nexport class ForceReasoningTransformer implements Transformer {\n  name = \"forcereasoning\";\n\n  async transformRequestIn(\n    request: UnifiedChatRequest\n  ): Promise<UnifiedChatRequest> {\n    const systemMessage = request.messages.find(\n      (item) => item.role === \"system\"\n    );\n    if (Array.isArray(systemMessage?.content)) {\n      systemMessage.content.push({\n        type: \"text\",\n        text: \"You are an expert reasoning model. \\nAlways think step by step before answering. Even if the problem seems simple, always write down your reasoning process explicitly. \\nNever skip your chain of thought. \\nUse the following output format:\\n<reasoning_content>(Write your full detailed thinking here.)</reasoning_content>\\n\\nWrite your final conclusion here.\",\n      });\n    }\n    const lastMessage = request.messages[request.messages.length - 1];\n    if (lastMessage.role === \"user\" && Array.isArray(lastMessage.content)) {\n      lastMessage.content.push({\n        type: \"text\",\n        text: \"You are an expert reasoning model. \\nAlways think step by step before answering. Even if the problem seems simple, always write down your reasoning process explicitly. \\nNever skip your chain of thought. \\nUse the following output format:\\n<reasoning_content>(Write your full detailed thinking here.)</reasoning_content>\\n\\nWrite your final conclusion here.\",\n      });\n    }\n    if (lastMessage.role === \"tool\") {\n      request.messages.push({\n        role: \"user\",\n        content: [\n          {\n            type: \"text\",\n            text: \"You are an expert reasoning model. \\nAlways think step by step before answering. Even if the problem seems simple, always write down your reasoning process explicitly. \\nNever skip your chain of thought. \\nUse the following output format:\\n<reasoning_content>(Write your full detailed thinking here.)</reasoning_content>\\n\\nWrite your final conclusion here.\",\n          },\n        ],\n      });\n    }\n    return request;\n  }\n}\n```\n\n至于为什么让模型将思考内容放入reasoning_content标签而不是think标签有两个原因：\n1. 直接使用think标签不能很好的激活思考，猜测是训练模型时以think标签作为数据集进行训练。\n2. 如果使用think标签，模型的推理内容会被拆分到单独的字段，这就涉及到我们接下来要说的思维链回传问题。\n\n\n## 思维链回传\n\n近期Minimax发布了Minimax-m2，与此同时，他们还发布了一篇[文章](https://www.minimaxi.com/news/why-is-interleaved-thinking-important-for-m2)介绍思维链回传。但是太阳底下无新鲜事，刚好借此来剖析一下。\n1. 我们首先来看一下为什么需要回传思维链？\nMinimax在文章中说的是Chat Completion API不支持在后续请求中传递推理内容。我们知道ChatGPT是最先支持推理的，但是OpenAI最初没有开放思维链给用户，所以对于Chat Completion API来讲并不需要支持思维链相关的东西。就连CoT的字段也是DeepSeek率先在Chat Completion API中加入的。\n\n2. 我们真的需要这些字段吗？\n如果没有这些字段会怎么样？会影响到模型的思考吗？可以查看一下[sglang的源码](https://github.com/sgl-project/sglang/blob/main/python/sglang/srt/parser/reasoning_parser.py)发现思维链的信息原本就会在消息中按照特定的标记进行输出，假如我们不对其进行拆分，正常情况下在下轮对话中会自然包含这些信息。所以需要思维链回传的原因就是我们对模型的思维链内容进行拆分。\n\n我用上面不到40行的代码完成了对GLM-4.5/6支持思考以及思维链回传的简单探索(单纯是因为没时间做拆分，完全可以在transformer中响应时先做拆分，请求时再进行合并，这样对cc前端的展示适配会更好)，如果你有什么更好的想法也欢迎与我联系。\n\n\n\n\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-blog/2025-11-18-router-exploration.md",
    "content": "---\ntitle: 或许我们能在 Router 中做更多事情\ndate: 2025-11-18\ntags: [router, transformer, deepseek]\n---\n\n# 或许我们能在 Router 中做更多事情\n\n自从`claude-code-router`发布以来，我收到了很多用户的反馈，至今还有不少的 issues 未处理。其中大多都是关于不同的供应商的支持和`deepseek`模型调用工具不积极的问题。\n之前开发这个项目主要是为了我自己能以较低成本使用上`claude code`，所以一开始的设计并没有考虑到多供应商的情况。在实际的排查问题中，我发现尽管市面上所有的供应商几乎都宣称兼容`OpenAI`格式调用，即通过`/chat/compeletions`接口调用，但是其中的细节差异非常多。例如:\n\n1. Gemini 的工具参数类型是 string 时，`format`参数只支持`date`和`date-time`，并且没有工具调用 ID。\n\n2. OpenRouter 需要使用`cache_control`进行缓存。\n\n3. DeepSeek 官方 API 的 `max_output` 为 8192，而火山引擎的会更大。\n\n除了这些问题之外，还有一些其他的小的供应商，他们或多或少参数都有点问题。于是，我打算开发一个新的项目[musistudio/llms](https://github.com/musistudio/llms)来处理这种不同服务商的兼容问题。该项目使用 OpenAI 格式为基础的通用格式，提供了一个`Transformer`接口，该接口用于处理转换请求和响应。当我们给不同的服务商都实现了`Transformer`后，我们可以实现不同服务商的混合调用。比如我在`AnthropicTransformer`中实现了`Anthropic` &lt;-&gt; `OpenAI`格式的互相转换，并监听了`/v1/messages`端点，在`GeminiTransformer`中实现了`Gemini` &lt;-&gt; `OpenAI`格式的互相转换，并监听了`/v1beta/models/:modelAndAction`端点，当他们的请求和响应都被转换成一个通用格式的时候，就可以实现他们的互相调用。\n\n```\nAnthropicRequest -> AnthropicTransformer -> OpenAIRequest -> GeminiTransformer -> GeminiRequest -> GeminiServer\n```\n\n```\nGeminiReseponse -> GeminiTransformer -> OpenAIResponse -> AnthropicTransformer -> AnthropicResponse\n```\n\n虽然使用中间层抹平差异可能会带来一些性能问题，但是该项目最初的目的是为了让`claude-code-router`支持不同的供应商。\n\n至于`deepseek`模型调用工具不积极的问题，我发现这是由于`deepseek`在长上下文中的指令遵循不佳导致的。现象就是刚开始模型会主动调用工具，但是在经过几轮对话后模型只会返回文本。一开始的解决方案是通过注入一个系统提示词告知模型需要积极去使用工具以解决用户的问题，但是后面测试发现在长上下文中模型会遗忘该指令。\n查看`deepseek`文档后发现模型支持`tool_choice`参数，可以强制让模型最少调用 1 个工具，我尝试将该值设置为`required`，发现模型调用工具的积极性大大增加，现在我们只需要在合适的时候取消这个参数即可。借助[musistudio/llms](https://github.com/musistudio/llms)的`Transformer`可以让我们在发送请求前和收到响应后做点什么，所以我参考`claude code`的`Plan Mode`，实现了一个使用与`deepseek`的`Tool Mode`\n\n```typescript\nexport class TooluseTransformer implements Transformer {\n  name = \"tooluse\";\n\n  transformRequestIn(request: UnifiedChatRequest): UnifiedChatRequest {\n    if (request.tools?.length) {\n      request.messages.push({\n        role: \"system\",\n        content: `<system-reminder>Tool mode is active. The user expects you to proactively execute the most suitable tool to help complete the task.\nBefore invoking a tool, you must carefully evaluate whether it matches the current task. If no available tool is appropriate for the task, you MUST call the \\`ExitTool\\` to exit tool mode — this is the only valid way to terminate tool mode.\nAlways prioritize completing the user's task effectively and efficiently by using tools whenever appropriate.</system-reminder>`,\n      });\n      request.tool_choice = \"required\";\n      request.tools.unshift({\n        type: \"function\",\n        function: {\n          name: \"ExitTool\",\n          description: `Use this tool when you are in tool mode and have completed the task. This is the only valid way to exit tool mode.\nIMPORTANT: Before using this tool, ensure that none of the available tools are applicable to the current task. You must evaluate all available options — only if no suitable tool can help you complete the task should you use ExitTool to terminate tool mode.\nExamples:\n1. Task: \"Use a tool to summarize this document\" — Do not use ExitTool if a summarization tool is available.\n2. Task: \"What's the weather today?\" — If no tool is available to answer, use ExitTool after reasoning that none can fulfill the task.`,\n          parameters: {\n            type: \"object\",\n            properties: {\n              response: {\n                type: \"string\",\n                description:\n                  \"Your response will be forwarded to the user exactly as returned — the tool will not modify or post-process it in any way.\",\n              },\n            },\n            required: [\"response\"],\n          },\n        },\n      });\n    }\n    return request;\n  }\n\n  async transformResponseOut(response: Response): Promise<Response> {\n    if (response.headers.get(\"Content-Type\")?.includes(\"application/json\")) {\n      const jsonResponse = await response.json();\n      if (\n        jsonResponse?.choices[0]?.message.tool_calls?.length &&\n        jsonResponse?.choices[0]?.message.tool_calls[0]?.function?.name ===\n          \"ExitTool\"\n      ) {\n        const toolArguments = JSON.parse(toolCall.function.arguments || \"{}\");\n        jsonResponse.choices[0].message.content = toolArguments.response || \"\";\n        delete jsonResponse.choices[0].message.tool_calls;\n      }\n\n      // Handle non-streaming response if needed\n      return new Response(JSON.stringify(jsonResponse), {\n        status: response.status,\n        statusText: response.statusText,\n        headers: response.headers,\n      });\n    } else if (response.headers.get(\"Content-Type\")?.includes(\"stream\")) {\n      // ...\n    }\n    return response;\n  }\n}\n```\n\n该工具将始终让模型至少调用一个工具，如果没有合适的工具或者任务已完成可以调用`ExitTool`来退出工具模式，因为是依靠`tool_choice`参数实现的，所以仅适用于支持该参数的模型。经过测试，该工具能显著增加`deepseek`的工具调用次数，弊端是可能会有跟任务无关或者没有必要的工具调用导致增加任务执行事件和消耗的 `token` 数。\n\n这次更新仅仅是在 Router 中实现一个`agent`的一次小探索，或许还能做更多其他有趣的事也说不定...\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-blog/options.json",
    "content": "{\n  \"title\": {\n    \"message\": \"Blog\",\n    \"description\": \"The title for the blog used in SEO\"\n  },\n  \"description\": {\n    \"message\": \"Blog\",\n    \"description\": \"The description for the blog used in SEO\"\n  },\n  \"sidebar.title\": {\n    \"message\": \"Recent posts\",\n    \"description\": \"The label for the left sidebar\"\n  }\n}\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/cli/commands/model.md",
    "content": "---\ntitle: ccr model\nsidebar_position: 2\n---\n\n# ccr model\n\n交互式模型选择和配置。\n\n## 用法\n\n```bash\nccr model [命令]\n```\n\n## 命令\n\n### 选择模型\n\n交互式选择模型：\n\n```bash\nccr model\n```\n\n这将显示一个包含可用提供商和模型的交互式菜单。\n\n### 设置默认模型\n\n直接设置默认模型：\n\n```bash\nccr model set <provider>,<model>\n```\n\n示例：\n\n```bash\nccr model set deepseek,deepseek-chat\n```\n\n### 列出模型\n\n列出所有配置的模型：\n\n```bash\nccr model list\n```\n\n### 添加模型\n\n添加新模型到配置：\n\n```bash\nccr model add <provider>,<model>\n```\n\n示例：\n\n```bash\nccr model add groq,llama-3.3-70b-versatile\n```\n\n### 删除模型\n\n从配置中删除模型：\n\n```bash\nccr model remove <provider>,<model>\n```\n\n## 示例\n\n### 交互式选择\n\n```bash\n$ ccr model\n\n? 选择一个提供商: deepseek\n? 选择一个模型: deepseek-chat\n\n默认模型设置为: deepseek,deepseek-chat\n```\n\n### 直接配置\n\n```bash\nccr model set deepseek,deepseek-chat\n```\n\n### 查看当前配置\n\n```bash\nccr model list\n```\n\n输出：\n\n```\n已配置的模型:\n  deepseek,deepseek-chat (默认)\n  groq,llama-3.3-70b-versatile\n  gemini,gemini-2.5-pro\n```\n\n## 交互式功能\n\n`ccr model` 命令提供以下功能：\n\n1. **查看当前配置**：查看所有已配置的模型和路由器设置\n2. **切换模型**：快速更改每个路由器类型使用的模型\n3. **添加新模型**：向现有提供商添加模型\n4. **创建新提供商**：设置完整的提供商配置，包括：\n   - 提供商名称和 API 端点\n   - API 密钥\n   - 可用模型\n   - 转换器配置，支持：\n     - 多个转换器（openrouter、deepseek、gemini 等）\n     - 转换器选项（例如，带自定义限制的 maxtoken）\n     - 提供商特定路由（例如，OpenRouter 提供商偏好）\n\nCLI 工具会验证所有输入并提供有用的提示来引导您完成配置过程，使管理复杂设置变得容易，无需手动编辑 JSON 文件。\n\n## 相关命令\n\n- [ccr start](/zh/docs/cli/start) - 启动服务器\n- [ccr config](/zh/docs/cli/other-commands#ccr-config) - 编辑配置\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/cli/commands/other.md",
    "content": "---\ntitle: 其他命令\nsidebar_position: 4\n---\n\n# 其他命令\n\n管理 Claude Code Router 的其他 CLI 命令。\n\n## ccr stop\n\n停止运行中的服务器。\n\n```bash\nccr stop\n```\n\n## ccr restart\n\n重启服务器。\n\n```bash\nccr restart\n```\n\n## ccr code\n\n通过路由器执行 claude 命令。\n\n```bash\nccr code [参数...]\n```\n\n## ccr ui\n\n在浏览器中打开 Web UI。\n\n```bash\nccr ui\n```\n\n## ccr activate\n\n输出用于与外部工具集成的 shell 环境变量。\n\n```bash\nccr activate\n```\n\n## 全局选项\n\n这些选项可用于任何命令：\n\n| 选项 | 说明 |\n|------|------|\n| `-h, --help` | 显示帮助 |\n| `-v, --version` | 显示版本号 |\n| `--config <路径>` | 配置文件路径 |\n| `--verbose` | 启用详细输出 |\n\n## 示例\n\n### 停止服务器\n\n```bash\nccr stop\n```\n\n### 使用自定义配置重启\n\n```bash\nccr restart --config /path/to/config.json\n```\n\n### 打开 Web UI\n\n```bash\nccr ui\n```\n\n## 相关文档\n\n- [入门](/zh/docs/intro) - Claude Code Router 简介\n- [配置](/zh/docs/config/basic) - 配置指南\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/cli/commands/preset.md",
    "content": "---\nsidebar_position: 5\n---\n\n# ccr preset\n\n管理预设（Presets）——可共享和重用的配置模板。\n\n## 概述\n\n预设功能让您可以：\n- 将当前配置保存为可重用的模板\n- 与他人分享配置\n- 安装社区提供的预配置方案\n- 在不同配置之间轻松切换\n\n## 命令\n\n### export\n\n将当前配置导出为预设。\n\n```bash\nccr preset export <名称> [选项]\n```\n\n**选项：**\n- `--output <路径>` - 自定义输出目录路径\n- `--description <文本>` - 预设描述\n- `--author <名称>` - 预设作者\n- `--tags <标签>` - 逗号分隔的关键字\n- `--include-sensitive` - 包含 API 密钥等敏感数据（不推荐）\n\n**示例：**\n```bash\nccr preset export my-config --description \"我的生产环境配置\" --author \"您的名字\"\n```\n\n**执行过程：**\n1. 读取 `~/.claude-code-router/config.json` 中的当前配置\n2. 提示输入描述、作者和关键字（如未通过命令行提供）\n3. 自动清理敏感字段（API 密钥变为占位符）\n4. 在 `~/.claude-code-router/presets/<名称>/` 创建预设目录\n5. 生成包含配置和元数据的 `manifest.json`\n\n### install\n\n从本地目录安装预设。\n\n```bash\nccr preset install <来源>\n```\n\n**来源：**\n- 本地目录路径：`/path/to/preset-directory`\n- 预设名称（用于重新配置已安装的预设）：`preset-name`\n\n**示例：**\n```bash\n# 从目录安装\nccr preset install ./my-preset\n\n# 重新配置已安装的预设\nccr preset install my-preset\n```\n\n**执行过程：**\n1. 从预设目录读取 `manifest.json`\n2. 验证预设结构\n3. 如果预设包含 `schema`，提示输入必需的值（API 密钥等）\n4. 将预设复制到 `~/.claude-code-router/presets/<名称>/`\n5. 在 `manifest.json` 中保存用户输入\n\n**注意：** 目前不支持从 URL 安装。请先下载预设目录。\n\n### list\n\n列出所有已安装的预设。\n\n```bash\nccr preset list\n```\n\n**示例输出：**\n```\nAvailable presets:\n\n• my-config (v1.0.0)\n  My production setup\n  by Your Name\n\n• openai-setup\n  Basic OpenAI configuration\n```\n\n### info\n\n显示预设的详细信息。\n\n```bash\nccr preset info <名称>\n```\n\n**显示内容：**\n- 版本、描述、作者、关键字\n- 配置摘要（Providers、Router 规则）\n- 必需输入（如果有）\n\n**示例：**\n```bash\nccr preset info my-config\n```\n\n### delete / rm / remove\n\n删除已安装的预设。\n\n```bash\nccr preset delete <名称>\nccr preset rm <名称>\nccr preset remove <名称>\n```\n\n**示例：**\n```bash\nccr preset delete my-config\n```\n\n## 预设结构\n\n预设是一个包含 `manifest.json` 文件的目录：\n\n```json\n{\n  \"name\": \"my-preset\",\n  \"version\": \"1.0.0\",\n  \"description\": \"我的配置\",\n  \"author\": \"作者姓名\",\n  \"keywords\": [\"openai\", \"production\"],\n\n  \"Providers\": [\n    {\n      \"name\": \"openai\",\n      \"api_base_url\": \"https://api.openai.com/v1/chat/completions\",\n      \"api_key\": \"{{apiKey}}\",\n      \"models\": [\"gpt-4\", \"gpt-3.5-turbo\"]\n    }\n  ],\n\n  \"Router\": {\n    \"default\": \"openai,gpt-4\"\n  },\n\n  \"schema\": [\n    {\n      \"id\": \"apiKey\",\n      \"type\": \"password\",\n      \"label\": \"OpenAI API 密钥\",\n      \"prompt\": \"请输入您的 OpenAI API 密钥\"\n    }\n  ]\n}\n```\n\n### Schema 系统\n\n`schema` 字段定义用户在安装时必须提供的输入：\n\n**字段类型：**\n- `password` - 隐藏输入（用于 API 密钥）\n- `input` - 文本输入\n- `select` - 单选下拉框\n- `multiselect` - 多选下拉框\n- `confirm` - 是/否确认\n- `editor` - 多行文本编辑器\n- `number` - 数字输入\n\n**动态选项：**\n```json\n{\n  \"id\": \"provider\",\n  \"type\": \"select\",\n  \"label\": \"选择提供商\",\n  \"options\": {\n    \"type\": \"providers\"\n  }\n}\n```\n\n**条件显示：**\n```json\n{\n  \"id\": \"model\",\n  \"type\": \"select\",\n  \"label\": \"选择模型\",\n  \"when\": {\n    \"field\": \"provider\",\n    \"operator\": \"exists\"\n  },\n  \"options\": {\n    \"type\": \"models\",\n    \"providerField\": \"#{selectedProvider}\"\n  }\n}\n```\n\n## 分享预设\n\n分享预设的步骤：\n\n1. **导出配置：**\n   ```bash\n   ccr preset export my-preset\n   ```\n\n2. **分享目录：**\n   ```bash\n   ~/.claude-code-router/presets/my-preset/\n   ```\n\n3. **分发方式：**\n   - 上传到 GitHub 仓库\n   - 创建 GitHub Gist\n   - 打包为 zip 文件分享\n   - 发布到 npm（未来功能）\n\n4. **用户安装：**\n   ```bash\n   ccr preset install /path/to/my-preset\n   ```\n\n## 安全性\n\n### 自动清理\n\n默认情况下，`export` 会清理敏感字段：\n- 名为 `api_key`、`apikey`、`password`、`secret` 的字段会被替换为 `{{字段名}}` 占位符\n- 这些占位符会成为 schema 中的必需输入\n- 用户在安装时会被提示提供自己的值\n\n### 包含敏感数据\n\n要包含实际值（不推荐）：\n```bash\nccr preset export my-preset --include-sensitive\n```\n\n**警告：** 永远不要分享包含敏感数据的预设！\n\n## 相关文档\n\n- [配置指南](/zh/docs/cli/config/basic) - 基础配置\n- [项目级配置](/zh/docs/cli/config/project-level) - 项目特定设置\n- [预设](/zh/docs/presets/intro) - 高级预设主题\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/cli/commands/start.md",
    "content": "---\ntitle: ccr start\nsidebar_position: 1\n---\n\n# ccr start\n\n启动 Claude Code Router 服务器。\n\n## 用法\n\n```bash\nccr start [选项]\n```\n\n## 选项\n\n| 选项 | 别名 | 说明 |\n|------|------|------|\n| `--port <number>` | `-p` | 监听端口号（默认：3456） |\n| `--config <path>` | `-c` | 配置文件路径 |\n| `--daemon` | `-d` | 作为守护进程运行（后台进程） |\n| `--log-level <level>` | `-l` | 日志级别（fatal/error/warn/info/debug/trace） |\n\n## 示例\n\n### 使用默认设置启动\n\n```bash\nccr start\n```\n\n### 在自定义端口启动\n\n```bash\nccr start --port 3000\n```\n\n### 使用自定义配置启动\n\n```bash\nccr start --config /path/to/config.json\n```\n\n### 作为守护进程启动\n\n```bash\nccr start --daemon\n```\n\n### 启用调试日志\n\n```bash\nccr start --log-level debug\n```\n\n## 环境变量\n\n您也可以使用环境变量配置服务器：\n\n| 变量 | 说明 |\n|------|------|\n| `PORT` | 监听端口号 |\n| `CONFIG_PATH` | 配置文件路径 |\n| `LOG_LEVEL` | 日志级别 |\n| `CUSTOM_ROUTER_PATH` | 自定义路由器函数路径 |\n| `HOST` | 绑定主机地址（默认：0.0.0.0） |\n\n## 输出\n\n启动成功后，您将看到：\n\n```\nClaude Code Router is running on http://localhost:3456\nAPI endpoint: http://localhost:3456/v1\n```\n\n## 相关命令\n\n- [ccr stop](/zh/docs/cli/other-commands#ccr-stop) - 停止服务器\n- [ccr restart](/zh/docs/cli/other-commands#ccr-restart) - 重启服务器\n- [ccr status](/zh/docs/cli/other-commands#ccr-status) - 检查服务器状态\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/cli/commands/status.md",
    "content": "---\ntitle: ccr status\nsidebar_position: 3\n---\n\n# ccr status\n\n显示 Claude Code Router 服务器的当前状态。\n\n## 用法\n\n```bash\nccr status\n```\n\n## 输出\n\n### 运行中的服务器\n\n当服务器正在运行时：\n\n```\nClaude Code Router 状态: 运行中\n版本: 2.0.0\nPID: 12345\n端口: 3456\n运行时间: 2小时34分钟\n配置: /home/user/.claude-code-router/config.json\n```\n\n### 已停止的服务器\n\n当服务器未运行时：\n\n```\nClaude Code Router 状态: 已停止\n```\n\n## 退出代码\n\n| 代码 | 说明 |\n|------|------|\n| 0 | 服务器正在运行 |\n| 1 | 服务器已停止 |\n| 2 | 检查状态时出错 |\n\n## 示例\n\n```bash\n$ ccr status\n\nClaude Code Router 状态: 运行中\n版本: 2.0.0\nPID: 12345\n端口: 3456\n运行时间: 2小时34分钟\n```\n\n## 相关命令\n\n- [ccr start](/zh/docs/cli/start) - 启动服务器\n- [ccr stop](/zh/docs/cli/other-commands#ccr-stop) - 停止服务器\n- [ccr restart](/zh/docs/cli/other-commands#ccr-restart) - 重启服务器\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/cli/commands/statusline.md",
    "content": "---\ntitle: ccr statusline\nsidebar_position: 5\n---\n\n# ccr statusline\n\n显示可自定义的状态栏，实时展示 Claude Code 会话信息，包括工作区、Git 分支、模型、token 使用情况等。\n\n## 概述\n\n`ccr statusline` 命令从 stdin 读取 JSON 数据，并在终端中渲染格式精美的状态栏。它设计用于与 Claude Code 的 hook 系统集成，以显示实时会话信息。\n\n## 使用方法\n\n### 基本用法\n\n```bash\nccr statusline\n```\n\n该命令期望通过 stdin 接收 JSON 数据，通常通过管道从 Claude Code hook 传递：\n\n```bash\necho '{\"hook_event_name\":\"...\",\"session_id\":\"...\",\"...\"}' | ccr statusline\n```\n\n### Hook 集成\n\n在您的 Claude Code 设置中配置：\n\n```json\n{\n  \"hooks\": {\n    \"postResponse\": {\n      \"command\": \"ccr statusline\",\n      \"input\": \"json\"\n    }\n  }\n}\n```\n\n## 可用主题\n\n### 默认主题\n\n简洁优雅的主题，使用 Nerd Font 图标和彩色文本：\n\n```\n 󰉋 my-project   main  󰚩 claude-3-5-sonnet-20241022  ↑ 12.3k  ↓ 5.2k\n```\n\n### Powerline 主题\n\nvim-powerline 风格，带背景色和箭头分隔符：\n\n```\n 󰉋 my-project   main  󰚩 claude-3-5-sonnet-20241022  ↑ 12.3k  ↓ 5.2k\n```\n\n通过在配置中设置 `currentStyle: \"powerline\"` 激活。\n\n### 简单主题\n\n回退主题，不带图标，适用于不支持 Nerd Font 的终端：\n\n```\nmy-project  main  claude-3-5-sonnet-20241022  ↑ 12.3k  ↓ 5.2k\n```\n\n当 `USE_SIMPLE_ICONS=true` 或在不支持的终端上自动使用。\n\n## 可用模块\n\n状态栏模块显示不同类型的信息：\n\n| 模块 | 说明 | 变量 |\n|------|------|------|\n| **workDir** | 当前工作目录名称 | `{{workDirName}}` |\n| **gitBranch** | 当前 Git 分支 | `{{gitBranch}}` |\n| **model** | 使用的模型 | `{{model}}` |\n| **usage** | Token 使用情况（输入/输出） | `{{inputTokens}}`, `{{outputTokens}}` |\n| **context** | 上下文窗口使用情况 | `{{contextPercent}}`, `{{contextWindowSize}}` |\n| **speed** | Token 处理速度 | `{{tokenSpeed}}`, `{{isStreaming}}` |\n| **cost** | API 成本 | `{{cost}}` |\n| **duration** | 会话持续时间 | `{{duration}}` |\n| **lines** | 代码变更 | `{{linesAdded}}`, `{{linesRemoved}}` |\n| **script** | 自定义脚本输出 | 动态 |\n\n## 配置\n\n在 `~/.claude-code-router/config.json` 中配置 statusline：\n\n### 默认样式示例\n\n```json\n{\n  \"StatusLine\": {\n    \"currentStyle\": \"default\",\n    \"default\": {\n      \"modules\": [\n        {\n          \"type\": \"workDir\",\n          \"icon\": \"󰉋\",\n          \"text\": \"{{workDirName}}\",\n          \"color\": \"bright_blue\"\n        },\n        {\n          \"type\": \"gitBranch\",\n          \"icon\": \"\",\n          \"text\": \"{{gitBranch}}\",\n          \"color\": \"bright_magenta\"\n        },\n        {\n          \"type\": \"model\",\n          \"icon\": \"󰚩\",\n          \"text\": \"{{model}}\",\n          \"color\": \"bright_cyan\"\n        },\n        {\n          \"type\": \"usage\",\n          \"icon\": \"↑\",\n          \"text\": \"{{inputTokens}}\",\n          \"color\": \"bright_green\"\n        },\n        {\n          \"type\": \"usage\",\n          \"icon\": \"↓\",\n          \"text\": \"{{outputTokens}}\",\n          \"color\": \"bright_yellow\"\n        }\n      ]\n    }\n  }\n}\n```\n\n### Powerline 样式示例\n\n```json\n{\n  \"StatusLine\": {\n    \"currentStyle\": \"powerline\",\n    \"powerline\": {\n      \"modules\": [\n        {\n          \"type\": \"workDir\",\n          \"icon\": \"󰉋\",\n          \"text\": \"{{workDirName}}\",\n          \"color\": \"white\",\n          \"background\": \"bg_bright_blue\"\n        },\n        {\n          \"type\": \"gitBranch\",\n          \"icon\": \"\",\n          \"text\": \"{{gitBranch}}\",\n          \"color\": \"white\",\n          \"background\": \"bg_bright_magenta\"\n        }\n      ]\n    }\n  }\n}\n```\n\n### 完整功能示例\n\n```json\n{\n  \"StatusLine\": {\n    \"currentStyle\": \"default\",\n    \"default\": {\n      \"modules\": [\n        {\n          \"type\": \"workDir\",\n          \"icon\": \"󰉋\",\n          \"text\": \"{{workDirName}}\",\n          \"color\": \"bright_blue\"\n        },\n        {\n          \"type\": \"gitBranch\",\n          \"icon\": \"\",\n          \"text\": \"{{gitBranch}}\",\n          \"color\": \"bright_magenta\"\n        },\n        {\n          \"type\": \"model\",\n          \"icon\": \"󰚩\",\n          \"text\": \"{{model}}\",\n          \"color\": \"bright_cyan\"\n        },\n        {\n          \"type\": \"context\",\n          \"icon\": \"🪟\",\n          \"text\": \"{{contextPercent}}% / {{contextWindowSize}}\",\n          \"color\": \"bright_green\"\n        },\n        {\n          \"type\": \"speed\",\n          \"icon\": \"⚡\",\n          \"text\": \"{{tokenSpeed}} t/s {{isStreaming}}\",\n          \"color\": \"bright_yellow\"\n        },\n        {\n          \"type\": \"cost\",\n          \"icon\": \"💰\",\n          \"text\": \"{{cost}}\",\n          \"color\": \"bright_magenta\"\n        },\n        {\n          \"type\": \"duration\",\n          \"icon\": \"⏱️\",\n          \"text\": \"{{duration}}\",\n          \"color\": \"bright_white\"\n        },\n        {\n          \"type\": \"lines\",\n          \"icon\": \"📝\",\n          \"text\": \"+{{linesAdded}}/-{{linesRemoved}}\",\n          \"color\": \"bright_cyan\"\n        }\n      ]\n    }\n  }\n}\n```\n\n## 自定义脚本\n\n您可以通过执行脚本创建自定义模块：\n\n```json\n{\n  \"type\": \"script\",\n  \"icon\": \"🔧\",\n  \"scriptPath\": \"/path/to/script.js\",\n  \"options\": {\n    \"customOption\": \"value\"\n  }\n}\n```\n\n脚本格式（CommonJS）：\n\n```javascript\n// my-status-module.js\nmodule.exports = function(variables, options) {\n  // 访问变量如 model、gitBranch 等\n  // 从配置中访问选项\n  return `Custom: ${variables.model}`;\n};\n\n// 或异步\nmodule.exports = async function(variables, options) {\n  const data = await fetchSomeData();\n  return data;\n};\n```\n\n## 颜色选项\n\n### 标准颜色\n\n- `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`\n- `bright_black`, `bright_red`, `bright_green`, `bright_yellow`, `bright_blue`, `bright_magenta`, `bright_cyan`, `bright_white`\n\n### 背景颜色\n\n添加前缀 `bg_`：`bg_blue`, `bg_bright_red` 等。\n\n### 十六进制颜色\n\n使用 24 位 TrueColor 和十六进制代码：\n\n```json\n{\n  \"color\": \"#FF5733\",\n  \"background\": \"bg_#1E90FF\"\n}\n```\n\n## 可用变量\n\n所有变量都可以在模块文本中使用 `{{variableName}}` 访问：\n\n| 变量 | 说明 | 示例 |\n|------|------|------|\n| `{{workDirName}}` | 当前目录名称 | `my-project` |\n| `{{gitBranch}}` | Git 分支名称 | `main` |\n| `{{model}}` | 模型名称 | `claude-3-5-sonnet-20241022` |\n| `{{inputTokens}}` | 输入 tokens（格式化） | `12.3k` |\n| `{{outputTokens}}` | 输出 tokens（格式化） | `5.2k` |\n| `{{tokenSpeed}}` | 每秒 tokens 数 | `45` |\n| `{{isStreaming}}` | 流式传输状态 | `streaming` 或空 |\n| `{{contextPercent}}` | 上下文使用百分比 | `45` |\n| `{{contextWindowSize}}` | 总上下文窗口 | `200k` |\n| `{{cost}}` | 总成本 | `$0.15` |\n| `{{duration}}` | 会话持续时间 | `2m34s` |\n| `{{linesAdded}}` | 添加的行数 | `150` |\n| `{{linesRemoved}}` | 删除的行数 | `25` |\n| `{{sessionId}}` | 会话 ID（前 8 个字符） | `a1b2c3d4` |\n\n## 环境变量\n\n使用环境变量控制行为：\n\n| 变量 | 值 | 说明 |\n|------|------|------|\n| `USE_SIMPLE_ICONS` | `true`/`false` | 强制使用不带图标的简单主题 |\n| `NERD_FONT` | 任意值 | 自动检测 Nerd Font 支持 |\n\n## 示例\n\n### 极简状态栏\n\n```json\n{\n  \"StatusLine\": {\n    \"default\": {\n      \"modules\": [\n        {\n          \"type\": \"model\",\n          \"text\": \"{{model}}\"\n        },\n        {\n          \"type\": \"usage\",\n          \"text\": \"↑{{inputTokens}} ↓{{outputTokens}}\"\n        }\n      ]\n    }\n  }\n}\n```\n\n输出：`claude-3-5-sonnet-20241022 ↑12.3k ↓5.2k`\n\n### 开发者生产力重点\n\n```json\n{\n  \"StatusLine\": {\n    \"default\": {\n      \"modules\": [\n        {\n          \"type\": \"gitBranch\",\n          \"icon\": \"\",\n          \"text\": \"{{gitBranch}}\",\n          \"color\": \"bright_magenta\"\n        },\n        {\n          \"type\": \"lines\",\n          \"icon\": \"📝\",\n          \"text\": \"+{{linesAdded}}/-{{linesRemoved}}\",\n          \"color\": \"bright_cyan\"\n        },\n        {\n          \"type\": \"duration\",\n          \"icon\": \"⏱️\",\n          \"text\": \"{{duration}}\",\n          \"color\": \"bright_white\"\n        }\n      ]\n    }\n  }\n}\n```\n\n输出：` feature/auth  📝 +150/-25  ⏱️ 2m34s`\n\n## Preset 集成\n\nStatusline 主题可以包含在 presets 中。当您安装带有 statusline 配置的 preset 时，激活该 preset 时会自动应用。\n\n查看 [Presets](/zh/docs/presets/intro) 了解更多信息。\n\n## 故障排除\n\n### 图标不显示\n\n在环境中设置 `USE_SIMPLE_ICONS=true`：\n\n```bash\nexport USE_SIMPLE_ICONS=true\n```\n\n### 颜色不工作\n\n确保您的终端支持 TrueColor（24 位颜色）：\n\n```bash\nexport COLORTERM=truecolor\n```\n\n### Git 分支不显示\n\n确保您在 Git 仓库中并安装了 `git` 命令。\n\n## 相关命令\n\n- [ccr status](/docs/cli/commands/status) - 检查服务状态\n- [ccr preset](/docs/cli/commands/preset) - 管理带 statusline 主题的 presets\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/cli/config/basic.md",
    "content": "# CLI 基础配置\n\nCLI 使用与 Server 相同的配置文件：`~/.claude-code-router/config.json`\n\n## 配置文件位置\n\n```bash\n~/.claude-code-router/config.json\n```\n\n## 快速配置\n\n使用交互式命令配置：\n\n```bash\nccr model\n```\n\n这将引导你完成：\n1. 选择 LLM 提供商\n2. 配置 API Key\n3. 选择模型\n4. 设置路由规则\n\n## 手动配置\n\n### 编辑配置文件\n\n```bash\n# 打开配置文件\nnano ~/.claude-code-router/config.json\n```\n\n### 最小配置示例\n\n```json5\n{\n  // API 密钥（可选，用于保护服务）\n  \"APIKEY\": \"your-api-key-here\",\n\n  // LLM 提供商\n  \"Providers\": [\n    {\n      \"name\": \"openai\",\n      \"baseUrl\": \"https://api.openai.com/v1\",\n      \"apiKey\": \"$OPENAI_API_KEY\",\n      \"models\": [\"gpt-4\", \"gpt-3.5-turbo\"]\n    }\n  ],\n\n  // 默认路由\n  \"Router\": {\n    \"default\": \"openai,gpt-4\"\n  }\n}\n```\n\n## 环境变量\n\n配置支持环境变量插值：\n\n```json5\n{\n  \"Providers\": [\n    {\n      \"apiKey\": \"$OPENAI_API_KEY\"  // 从环境变量读取\n    }\n  ]\n}\n```\n\n在 `.bashrc` 或 `.zshrc` 中设置：\n\n```bash\nexport OPENAI_API_KEY=\"sk-...\"\nexport ANTHROPIC_API_KEY=\"sk-ant-...\"\n```\n\n## 常用配置项\n\n### HOST 和 PORT\n\n```json5\n{\n  \"HOST\": \"127.0.0.1\",  // 监听地址\n  \"PORT\": 3456          // 监听端口\n}\n```\n\n### 日志配置\n\n```json5\n{\n  \"LOG\": true,          // 启用日志\n  \"LOG_LEVEL\": \"info\"   // 日志级别\n}\n```\n\n### 路由配置\n\n```json5\n{\n  \"Router\": {\n    \"default\": \"openai,gpt-4\",\n    \"background\": \"openai,gpt-3.5-turbo\",\n    \"think\": \"openai,gpt-4\",\n    \"longContext\": \"anthropic,claude-3-opus\"\n  }\n}\n```\n\n## 配置验证\n\n配置文件会自动验证。常见错误：\n\n- **缺少 Providers**：必须至少配置一个提供商\n- **API Key 缺失**：如果配置了 Providers，必须提供 API Key\n- **模型不存在**：确保模型在提供商的 models 列表中\n\n## 配置备份\n\n每次更新配置时会自动备份：\n\n```\n~/.claude-code-router/config.backup.{timestamp}.json\n```\n\n## 重新加载配置\n\n修改配置后需要重启服务：\n\n```bash\nccr restart\n```\n\n## 查看当前配置\n\n```bash\n# 通过 API 查看\ncurl http://localhost:3456/api/config\n\n# 或查看配置文件\ncat ~/.claude-code-router/config.json\n```\n\n## 示例配置\n\n### OpenAI\n\n```json5\n{\n  \"Providers\": [\n    {\n      \"name\": \"openai\",\n      \"baseUrl\": \"https://api.openai.com/v1\",\n      \"apiKey\": \"$OPENAI_API_KEY\",\n      \"models\": [\"gpt-4\", \"gpt-3.5-turbo\"]\n    }\n  ],\n  \"Router\": {\n    \"default\": \"openai,gpt-4\"\n  }\n}\n```\n\n### Anthropic\n\n```json5\n{\n  \"Providers\": [\n    {\n      \"name\": \"anthropic\",\n      \"baseUrl\": \"https://api.anthropic.com/v1\",\n      \"apiKey\": \"$ANTHROPIC_API_KEY\",\n      \"models\": [\"claude-3-5-sonnet-20241022\", \"claude-3-opus-20240229\"]\n    }\n  ],\n  \"Router\": {\n    \"default\": \"anthropic,claude-3-5-sonnet-20241022\"\n  }\n}\n```\n\n### 多提供商\n\n```json5\n{\n  \"Providers\": [\n    {\n      \"name\": \"openai\",\n      \"baseUrl\": \"https://api.openai.com/v1\",\n      \"apiKey\": \"$OPENAI_API_KEY\",\n      \"models\": [\"gpt-4\", \"gpt-3.5-turbo\"]\n    },\n    {\n      \"name\": \"anthropic\",\n      \"baseUrl\": \"https://api.anthropic.com/v1\",\n      \"apiKey\": \"$ANTHROPIC_API_KEY\",\n      \"models\": [\"claude-3-5-sonnet-20241022\", \"claude-3-opus-20240229\"]\n    }\n  ],\n  \"Router\": {\n    \"default\": \"openai,gpt-4\",\n    \"think\": \"anthropic,claude-3-5-sonnet-20241022\",\n    \"background\": \"openai,gpt-3.5-turbo\"\n  }\n}\n```\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/cli/config/project-level.md",
    "content": "# 项目级配置\n\n除了全局配置，`ccr` 还支持为特定项目设置不同的路由规则。\n\n## 项目配置文件\n\n项目配置文件位于：\n\n```\n~/.claude/projects/<project-id>/claude-code-router.json\n```\n\n其中 `<project-id>` 是 Claude Code 项目的唯一标识符。\n\n## 项目配置结构\n\n```json5\n{\n  \"Router\": {\n    \"default\": \"openai,gpt-4\",\n    \"background\": \"openai,gpt-3.5-turbo\"\n  }\n}\n```\n\n## 查找项目 ID\n\n### 方法一：使用 CLI\n\n```bash\n# 在项目目录中运行\nccr status\n```\n\n输出会显示当前项目 ID：\n\n```\nProject: my-project (abc123def456)\n```\n\n### 方法二：查看 Claude Code 配置\n\n```bash\ncat ~/.claude.json\n```\n\n找到你的项目 ID：\n\n```json\n{\n  \"projects\": {\n    \"abc123def456\": {\n      \"path\": \"/path/to/your/project\",\n      \"name\": \"my-project\"\n    }\n  }\n}\n```\n\n## 创建项目配置\n\n### 手动创建\n\n```bash\n# 创建项目配置目录\nmkdir -p ~/.claude/projects/abc123def456\n\n# 创建配置文件\ncat > ~/.claude/projects/abc123def456/claude-code-router.json << 'EOF'\n{\n  \"Router\": {\n    \"default\": \"anthropic,claude-3-5-sonnet-20241022\",\n    \"background\": \"openai,gpt-3.5-turbo\"\n  }\n}\nEOF\n```\n\n### 使用 ccr model 命令\n\n```bash\n# 在项目目录中运行\ncd /path/to/your/project\nccr model --project\n```\n\n## 配置优先级\n\n路由配置的优先级（从高到低）：\n\n1. **自定义路由函数** (`CUSTOM_ROUTER_PATH`)\n2. **项目级配置** (`~/.claude/projects/<id>/claude-code-router.json`)\n3. **全局配置** (`~/.claude-code-router/config.json`)\n4. **内置路由规则**\n\n## 使用场景\n\n### 场景一：不同项目使用不同模型\n\n```json5\n// Web 项目使用 GPT-4\n~/.claude/projects/web-project-id/claude-code-router.json:\n{\n  \"Router\": {\n    \"default\": \"openai,gpt-4\"\n  }\n}\n\n// AI 项目使用 Claude\n~/.claude/projects/ai-project-id/claude-code-router.json:\n{\n  \"Router\": {\n    \"default\": \"anthropic,claude-3-5-sonnet-20241022\"\n  }\n}\n```\n\n### 场景二：测试项目使用低成本模型\n\n```json5\n~/.claude/projects/test-project-id/claude-code-router.json:\n{\n  \"Router\": {\n    \"default\": \"openai,gpt-3.5-turbo\",\n    \"background\": \"openai,gpt-3.5-turbo\"\n  }\n}\n```\n\n### 场景三：长上下文项目\n\n```json5\n~/.claude/projects/long-context-project-id/claude-code-router.json:\n{\n  \"Router\": {\n    \"default\": \"anthropic,claude-3-opus-20240229\",\n    \"longContext\": \"anthropic,claude-3-opus-20240229\"\n  }\n}\n```\n\n## 验证项目配置\n\n```bash\n# 查看当前项目使用的路由\nccr status\n\n# 查看日志确认路由决策\ntail -f ~/.claude-code-router/claude-code-router.log\n```\n\n## 删除项目配置\n\n```bash\nrm ~/.claude/projects/<project-id>/claude-code-router.json\n```\n\n删除后会回退到全局配置。\n\n## 完整示例\n\n假设你有两个项目：\n\n### 全局配置（`~/.claude-code-router/config.json`）\n\n```json5\n{\n  \"Providers\": [\n    {\n      \"name\": \"openai\",\n      \"baseUrl\": \"https://api.openai.com/v1\",\n      \"apiKey\": \"$OPENAI_API_KEY\",\n      \"models\": [\"gpt-4\", \"gpt-3.5-turbo\"]\n    },\n    {\n      \"name\": \"anthropic\",\n      \"baseUrl\": \"https://api.anthropic.com/v1\",\n      \"apiKey\": \"$ANTHROPIC_API_KEY\",\n      \"models\": [\"claude-3-5-sonnet-20241022\"]\n    }\n  ],\n  \"Router\": {\n    \"default\": \"openai,gpt-4\",\n    \"background\": \"openai,gpt-3.5-turbo\"\n  }\n}\n```\n\n### Web 项目配置\n\n```json5\n{\n  \"Router\": {\n    \"default\": \"openai,gpt-4\"\n  }\n}\n```\n\n### AI 项目配置\n\n```json5\n{\n  \"Router\": {\n    \"default\": \"anthropic,claude-3-5-sonnet-20241022\",\n    \"think\": \"anthropic,claude-3-5-sonnet-20241022\"\n  }\n}\n```\n\n这样：\n- Web 项目使用 GPT-4\n- AI 项目使用 Claude\n- 所有项目的后台任务使用 GPT-3.5-turbo（继承全局配置）\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/cli/installation.md",
    "content": "---\ntitle: 安装\nsidebar_position: 2\n---\n\n# 安装\n\n使用您喜欢的包管理器全局安装 Claude Code Router。\n\n## 前置要求\n\n- **Node.js**: >= 18.0.0\n- **pnpm**: >= 8.0.0（如果使用 pnpm）\n- 来自您偏好的 LLM 提供商的 API 密钥\n\n## 通过 npm 安装\n\n```bash\nnpm install -g @musistudio/claude-code-router\n```\n\n## 通过 pnpm 安装\n\n```bash\npnpm add -g @musistudio/claude-code-router\n```\n\n## 通过 Yarn 安装\n\n```bash\nyarn global add @musistudio/claude-code-router\n```\n\n## 验证安装\n\n安装完成后，验证 `ccr` 命令是否可用：\n\n```bash\nccr --version\n```\n\n您应该看到版本号显示。\n\n## 下一步\n\n安装完成后，前往 [快速开始](/zh/docs/quick-start) 了解如何配置和使用路由器。\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/cli/intro.md",
    "content": "---\ntitle: 欢迎使用 Claude Code Router\nsidebar_position: 1\nslug: /\n---\n\n# 欢迎使用 Claude Code Router\n\n[![npm version](https://badge.fury.io/js/%40musistudio%2Fclaude-code-router.svg)](https://www.npmjs.com/package/@musistudio/claude-code-router)\n![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)\n![Node Version](https://img.shields.io/node/v/@musistudio/claude-code-router.svg)\n\n**Claude Code Router** 是一个强大的工具，允许你在没有 Anthropic 账户的情况下使用 [Claude Code](https://claude.ai/code)，并将请求路由到其他 LLM 提供商。\n\n## 特性\n\n- **多提供商支持**: 路由到 DeepSeek、Gemini、Groq、OpenRouter 等\n- **智能路由**: 内置不同任务类型的场景（后台、思考、网络搜索、图像）\n- **项目级配置**: 每个项目自定义路由\n- **自定义路由函数**: 编写 JavaScript 定义自己的路由逻辑\n- **转换器系统**: 无缝适配不同提供商之间的 API 差异\n- **代理系统**: 可扩展的插件架构，实现自定义功能\n- **Web UI**: 内置管理界面，方便配置\n- **CLI 集成**: 与现有的 Claude Code 工作流无缝集成\n\n## 快速开始\n\n### 安装\n\n```bash\nnpm install -g @musistudio/claude-code-router\n# 或\npnpm add -g @musistudio/claude-code-router\n# 或\nyarn global add @musistudio/claude-code-router\n```\n\n### 基本使用\n\n```bash\n# 启动路由器服务器\nccr start\n\n# 配置 Claude Code 使用路由器\nexport ANTHROPIC_API_URL=\"http://localhost:8080/v1\"\nexport ANTHROPIC_API_KEY=\"your-api-key\"\n\n# 现在可以正常使用 Claude Code！\nclaude code\n```\n\n## 下一步\n\n- [安装指南](/docs/installation) - 详细安装说明\n- [快速开始](/docs/quick-start) - 5 分钟入门\n- [配置](/docs/config/basic) - 了解如何配置路由器\n- [CLI 参考](/docs/cli/start) - 完整的 CLI 命令参考\n\n## 架构\n\nClaude Code Router 由四个主要组件组成：\n\n- **CLI** (`@musistudio/claude-code-router`): 提供 `ccr` 命令的命令行工具\n- **Server** (`@CCR/server`): 处理 API 路由和转换的核心服务器\n- **Shared** (`@CCR/shared`): 共享常量和工具\n- **UI** (`@CCR/ui`): Web 管理界面（React + Vite）\n\n## 许可证\n\nMIT © [musistudio](https://github.com/musistudio)\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/cli/quick-start.md",
    "content": "---\ntitle: 快速开始\nsidebar_position: 3\n---\n\n# 快速开始\n\n5 分钟内启动并运行 Claude Code Router。\n\n## 1. 启动路由器\n\n```bash\nccr start\n```\n\n路由器默认将在 `http://localhost:8080` 上启动。\n\n## 2. 配置环境变量\n\n在您的 shell 中设置以下环境变量：\n\n```bash\nexport ANTHROPIC_API_URL=\"http://localhost:8080/v1\"\nexport ANTHROPIC_API_KEY=\"your-provider-api-key\"\n```\n\n或者使用 `ccr activate` 命令获取环境变量：\n\n```bash\neval \"$(ccr activate)\"\n```\n\n## 3. 使用 Claude Code\n\n现在您可以正常使用 Claude Code：\n\n```bash\nclaude code\n```\n\n您的请求将通过 Claude Code Router 路由到您配置的提供商。\n\n## 4. 配置提供商（可选）\n\n要配置多个提供商或自定义路由，使用：\n\n```bash\nccr model\n```\n\n这将打开一个交互式菜单来选择和配置模型。\n\n或者直接编辑配置文件：\n\n```bash\n# 在默认编辑器中打开配置\nccr config edit\n```\n\n配置文件示例 (`~/.claude-code-router/config.json`)：\n\n```json\n{\n  \"Providers\": [\n    {\n      \"name\": \"deepseek\",\n      \"api_base_url\": \"https://api.deepseek.com/chat/completions\",\n      \"api_key\": \"your-deepseek-api-key\",\n      \"models\": [\"deepseek-chat\", \"deepseek-coder\"]\n    }\n  ],\n  \"Router\": {\n    \"default\": \"deepseek,deepseek-chat\"\n  }\n}\n```\n\n## 下一步\n\n- [基础配置](/zh/docs/config/basic) - 了解配置选项\n- [路由配置](/zh/docs/config/routing) - 配置智能路由规则\n- [CLI 命令](/zh/docs/cli/start) - 探索所有 CLI 命令\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/presets/intro.md",
    "content": "---\ntitle: 预设配置\nsidebar_position: 3\n---\n\n# 预设配置\n\n使用预定义配置进行快速设置。\n\n## 什么是预设？\n\n预设是预配置的设置，包括针对特定用例优化的提供商配置、路由规则和转换器。\n\n## 使用预设\n\n### CLI 方式（命令行）\n\nCLI 方式适合开发者通过命令行快速操作。\n\n#### 安装预设\n\n**从本地目录安装：**\n\n```bash\nccr preset install /path/to/preset-directory\n```\n\n**重新配置已安装的预设：**\n\n```bash\nccr preset install my-preset\n```\n\n#### 使用预设\n\n安装预设后，可以使用预设名称启动 Claude Code：\n\n```bash\n# 使用指定预设启动\nccr my-preset \"your prompt\"\n```\n\n预设会：\n- 自动加载预配置的 Provider\n- 应用预设的路由规则\n- 使用预设中配置的 transformer\n\n#### 列出所有预设\n\n```bash\nccr preset list\n```\n\n此命令将显示所有已安装的预设及其名称、版本和描述。\n\n#### 查看预设信息\n\n```bash\nccr preset info my-preset\n```\n\n#### 删除预设\n\n```bash\nccr preset delete my-preset\n```\n\n### Web UI 方式\n\nWeb UI 提供更友好的可视化界面，支持更多安装方式。\n\n#### 访问 Web UI\n\n```bash\nccr ui\n```\n\n然后在浏览器中打开 `http://localhost:3000`\n\n#### 从 GitHub 仓库安装\n\n1. 点击\"预设商城\"按钮\n2. 在预设列表中选择要安装的预设\n3. 点击\"安装\"按钮\n\n#### 重新配置预设\n\n1. 在预设列表中点击\"查看详情\"按钮\n2. 在详情页面中修改配置项\n3. 点击\"应用\"保存配置\n\n#### 管理预设\n\n- **查看**：点击预设右侧的信息图标\n- **删除**：点击预设右侧的删除图标\n\n## 创建自定义预设\n\n### 预设目录结构\n\n预设以目录形式存储，每个预设包含以下结构：\n\n```\n~/.claude-code-router/presets/<preset-name>/\n├── manifest.json           # 必填：预设配置文件\n├── transformers/           # 可选：自定义转换器\n│   └── custom-transformer.js\n├── scripts/               # 可选：自定义脚本\n│   └── status.js\n└── README.md              # 可选：说明文档\n```\n\n### 动态配置系统\n\nCCR 引入了强大的动态配置系统，支持：\n\n- **多种输入类型**：选择器、多选、确认框、文本输入、数字输入等\n- **条件逻辑**：根据用户输入动态显示/隐藏配置项\n- **变量引用**：配置项之间可以互相引用\n- **动态选项**：选项列表可以从预设配置或用户输入中动态生成\n\n#### Schema 字段类型\n\n| 类型 | 说明 | 示例 |\n|------|------|------|\n| `password` | 密码输入（隐藏显示） | API Key |\n| `input` | 单行文本输入 | Base URL |\n| `number` | 数字输入 | 最大Token数 |\n| `select` | 单选下拉框 | 选择Provider |\n| `multiselect` | 多选框 | 启用功能 |\n| `confirm` | 确认框 | 是否使用代理 |\n| `editor` | 多行文本编辑器 | 自定义配置 |\n\n#### 条件运算符\n\n| 运算符 | 说明 | 示例 |\n|--------|------|------|\n| `eq` | 等于 | `{\"field\": \"provider\", \"operator\": \"eq\", \"value\": \"openai\"}` |\n| `ne` | 不等于 | `{\"field\": \"advanced\", \"operator\": \"ne\", \"value\": true}` |\n| `in` | 包含于 | `{\"field\": \"feature\", \"operator\": \"in\", \"value\": [\"a\", \"b\"]}` |\n| `nin` | 不包含于 | `{\"field\": \"type\", \"operator\": \"nin\", \"value\": [\"x\", \"y\"]}` |\n| `exists` | 字段存在 | `{\"field\": \"apiKey\", \"operator\": \"exists\"}` |\n| `gt/lt/gte/lte` | 大于/小于/大于等于/小于等于 | 用于数字比较 |\n\n#### 动态选项类型\n\n##### static - 静态选项\n```json\n\"options\": {\n  \"type\": \"static\",\n  \"options\": [\n    {\"label\": \"选项1\", \"value\": \"value1\"},\n    {\"label\": \"选项2\", \"value\": \"value2\"}\n  ]\n}\n```\n\n##### providers - 从 Providers 配置提取\n```json\n\"options\": {\n  \"type\": \"providers\"\n}\n```\n自动从 `Providers` 数组中提取 name 作为选项。\n\n##### models - 从指定 Provider 的 models 提取\n```json\n\"options\": {\n  \"type\": \"models\",\n  \"providerField\": \"{{selectedProvider}}\"\n}\n```\n根据用户选择的 Provider，动态显示该 Provider 的 models。\n\n#### 模板变量\n\n使用 `{{变量名}}` 语法在 template 中引用用户输入：\n\n```json\n\"template\": {\n  \"Providers\": [\n    {\n      \"name\": \"{{providerName}}\",\n      \"api_key\": \"{{apiKey}}\"\n    }\n  ]\n}\n```\n\n#### 配置映射\n\n对于复杂的配置需求，使用 `configMappings` 精确控制值的位置：\n\n```json\n\"configMappings\": [\n  {\n    \"target\": \"Providers[0].api_key\",\n    \"value\": \"{{apiKey}}\"\n  },\n  {\n    \"target\": \"PROXY_URL\",\n    \"value\": \"{{proxyUrl}}\",\n    \"when\": {\n      \"field\": \"useProxy\",\n      \"operator\": \"eq\",\n      \"value\": true\n    }\n  }\n]\n```\n\n#### 完整示例\n\n```json\n{\n  \"name\": \"multi-provider-example\",\n  \"version\": \"1.0.0\",\n  \"description\": \"多Provider配置示例 - 支持OpenAI和DeepSeek切换\",\n  \"author\": \"CCR Team\",\n  \"keywords\": [\"openai\", \"deepseek\", \"multi-provider\"],\n  \"ccrVersion\": \"2.0.0\",\n  \"schema\": [\n    {\n      \"id\": \"primaryProvider\",\n      \"type\": \"select\",\n      \"label\": \"主要Provider\",\n      \"prompt\": \"选择您主要使用的LLM提供商\",\n      \"options\": {\n        \"type\": \"static\",\n        \"options\": [\n          {\n            \"label\": \"OpenAI\",\n            \"value\": \"openai\",\n            \"description\": \"使用OpenAI的GPT模型\"\n          },\n          {\n            \"label\": \"DeepSeek\",\n            \"value\": \"deepseek\",\n            \"description\": \"使用DeepSeek的高性价比模型\"\n          }\n        ]\n      },\n      \"required\": true,\n      \"defaultValue\": \"openai\"\n    },\n    {\n      \"id\": \"apiKey\",\n      \"type\": \"password\",\n      \"label\": \"API Key\",\n      \"prompt\": \"请输入您的API Key\",\n      \"placeholder\": \"sk-...\",\n      \"required\": true\n    },\n    {\n      \"id\": \"defaultModel\",\n      \"type\": \"select\",\n      \"label\": \"默认模型\",\n      \"prompt\": \"选择默认使用的模型\",\n      \"options\": {\n        \"type\": \"static\",\n        \"options\": [\n          {\"label\": \"GPT-4o\", \"value\": \"gpt-4o\"},\n          {\"label\": \"GPT-4o-mini\", \"value\": \"gpt-4o-mini\"}\n        ]\n      },\n      \"required\": true,\n      \"defaultValue\": \"gpt-4o\",\n      \"when\": {\n        \"field\": \"primaryProvider\",\n        \"operator\": \"eq\",\n        \"value\": \"openai\"\n      }\n    },\n    {\n      \"id\": \"enableProxy\",\n      \"type\": \"confirm\",\n      \"label\": \"启用代理\",\n      \"prompt\": \"是否通过代理访问API？\",\n      \"defaultValue\": false\n    },\n    {\n      \"id\": \"proxyUrl\",\n      \"type\": \"input\",\n      \"label\": \"代理地址\",\n      \"prompt\": \"输入代理服务器地址\",\n      \"placeholder\": \"http://127.0.0.1:7890\",\n      \"required\": true,\n      \"when\": {\n        \"field\": \"enableProxy\",\n        \"operator\": \"eq\",\n        \"value\": true\n      }\n    }\n  ],\n  \"template\": {\n    \"Providers\": [\n      {\n        \"name\": \"{{primaryProvider}}\",\n        \"api_base_url\": \"https://api.openai.com/v1/chat/completions\",\n        \"api_key\": \"{{apiKey}}\",\n        \"models\": [\"{{defaultModel}}\"]\n      }\n    ],\n    \"Router\": {\n      \"default\": \"{{primaryProvider}},{{defaultModel}}\"\n    },\n    \"PROXY_URL\": \"{{proxyUrl}}\"\n  },\n  \"configMappings\": [\n    {\n      \"target\": \"PROXY_URL\",\n      \"value\": \"{{proxyUrl}}\",\n      \"when\": {\n        \"field\": \"enableProxy\",\n        \"operator\": \"eq\",\n        \"value\": true\n      }\n    }\n  ]\n}\n```\n\n### manifest.json 完整字段说明\n\n`manifest.json` 是预设的核心配置文件，使用 JSON5 格式（支持注释）。\n\n#### 1. 元数据字段（Metadata）\n\n这些字段用于描述预设的基本信息：\n\n| 字段 | 类型 | 必填 | 说明 |\n|------|------|------|------|\n| `name` | string | ✓ | 预设名称（唯一标识符） |\n| `version` | string | ✓ | 版本号（遵循 semver 规范） |\n| `description` | string | - | 预设描述 |\n| `author` | string | - | 作者信息 |\n| `homepage` | string | - | 项目主页 URL |\n| `repository` | string | - | 源代码仓库 URL |\n| `license` | string | - | 许可证类型 |\n| `keywords` | string[] | - | 关键词标签 |\n| `ccrVersion` | string | - | 兼容的 CCR 版本 |\n\n示例：\n\n```json\n{\n  \"name\": \"my-preset\",\n  \"version\": \"1.0.0\",\n  \"description\": \"我的自定义预设\",\n  \"author\": \"Your Name\",\n  \"homepage\": \"https://github.com/yourname/ccr-presets\",\n  \"repository\": \"https://github.com/yourname/ccr-presets.git\",\n  \"license\": \"MIT\",\n  \"keywords\": [\"openai\", \"production\"],\n  \"ccrVersion\": \"2.0.0\"\n}\n```\n\n#### 2. 配置字段（Configuration）\n\n这些字段会直接合并到 CCR 的配置中，所有 `config.json` 支持的字段都可以在这里使用：\n\n| 字段 | 类型 | 说明 |\n|------|------|------|\n| `Providers` | array | Provider 配置数组 |\n| `Router` | object | 路由配置 |\n| `transformers` | array | 转换器配置 |\n| `StatusLine` | object | 状态栏配置 |\n| `NON_INTERACTIVE_MODE` | boolean | 启用非交互模式（用于 CI/CD） |\n\n**CLI 专用字段**（这些字段仅在 CLI 模式下有效，服务器不使用）：\n\n| 字段 | 类型 | 说明 |\n|------|------|------|\n| `noServer` | boolean | 跳过本地服务器启动，直接使用 Provider 的 API |\n| `claudeCodeSettings` | object | Claude Code 特定设置（环境变量、状态栏等） |\n\n示例：\n\n```json\n{\n  \"Providers\": [\n    {\n      \"name\": \"openai\",\n      \"api_base_url\": \"https://api.openai.com/v1/chat/completions\",\n      \"api_key\": \"${OPENAI_API_KEY}\",\n      \"models\": [\"gpt-4o\", \"gpt-4o-mini\"]\n    }\n  ],\n  \"Router\": {\n    \"default\": \"openai,gpt-4o\",\n    \"background\": \"openai,gpt-4o-mini\"\n  },\n  \"PORT\": 8080\n}\n```\n\n#### 3. 动态配置系统字段\n\n这些字段用于创建可交互的配置模板：\n\n| 字段 | 类型 | 说明 |\n|------|------|------|\n| `schema` | array | 配置输入表单定义 |\n| `template` | object | 配置模板（使用变量引用） |\n| `configMappings` | array | 配置映射规则 |\n| `userValues` | object | 用户填写的值（运行时使用） |\n\n**schema 字段类型：**\n\n| 类型 | 说明 | 使用场景 |\n|------|------|----------|\n| `password` | 密码输入（隐藏） | API Key |\n| `input` | 单行文本输入 | URL |\n| `number` | 数字输入 | 端口号 |\n| `select` | 单选下拉框 | 选择 Provider |\n| `multiselect` | 多选框 | 启用功能 |\n| `confirm` | 确认框 | 是否启用 |\n| `editor` | 多行文本编辑器 | 自定义配置 |\n\n动态配置示例：\n\n```json\n{\n  \"schema\": [\n    {\n      \"id\": \"apiKey\",\n      \"type\": \"password\",\n      \"label\": \"API Key\",\n      \"prompt\": \"请输入您的 API Key\",\n      \"required\": true\n    },\n    {\n      \"id\": \"provider\",\n      \"type\": \"select\",\n      \"label\": \"Provider\",\n      \"options\": {\n        \"type\": \"static\",\n        \"options\": [\n          {\"label\": \"OpenAI\", \"value\": \"openai\"},\n          {\"label\": \"DeepSeek\", \"value\": \"deepseek\"}\n        ]\n      },\n      \"defaultValue\": \"openai\"\n    }\n  ],\n  \"template\": {\n    \"Providers\": [\n      {\n        \"name\": \"#{provider}\",\n        \"api_key\": \"#{apiKey}\"\n      }\n    ]\n  }\n}\n```\n\n### 创建预设示例\n\n#### 示例 1：简单预设（无动态配置）\n\n```bash\n# 创建预设目录\nmkdir -p ~/.claude-code-router/presets/simple-openai\n\n# 创建 manifest.json\ncat > ~/.claude-code-router/presets/simple-openai/manifest.json << 'EOF'\n{\n  \"name\": \"simple-openai\",\n  \"version\": \"1.0.0\",\n  \"description\": \"简单的 OpenAI 配置\",\n  \"author\": \"Your Name\",\n\n  \"Providers\": [\n    {\n      \"name\": \"openai\",\n      \"api_base_url\": \"https://api.openai.com/v1/chat/completions\",\n      \"api_key\": \"${OPENAI_API_KEY}\",\n      \"models\": [\"gpt-4o\", \"gpt-4o-mini\"]\n    }\n  ],\n\n  \"Router\": {\n    \"default\": \"openai,gpt-4o\",\n    \"background\": \"openai,gpt-4o-mini\"\n  }\n}\nEOF\n\n# 配置预设（输入 API Key）\nccr preset install simple-openai\n\n# 使用预设\nccr simple-openai \"your prompt\"\n```\n\n#### 示例 2：高级预设（动态配置）\n\n```bash\n# 创建预设目录\nmkdir -p ~/.claude-code-router/presets/advanced-config\n\n# 创建 manifest.json\ncat > ~/.claude-code-router/presets/advanced-config/manifest.json << 'EOF'\n{\n  \"name\": \"advanced-config\",\n  \"version\": \"1.0.0\",\n  \"description\": \"支持多 Provider 选择的高级配置\",\n  \"author\": \"Your Name\",\n  \"keywords\": [\"openai\", \"deepseek\", \"multi-provider\"],\n\n  \"schema\": [\n    {\n      \"id\": \"provider\",\n      \"type\": \"select\",\n      \"label\": \"选择 Provider\",\n      \"prompt\": \"选择您主要使用的 LLM 提供商\",\n      \"options\": {\n        \"type\": \"static\",\n        \"options\": [\n          {\n            \"label\": \"OpenAI\",\n            \"value\": \"openai\",\n            \"description\": \"使用 OpenAI 的 GPT 模型\"\n          },\n          {\n            \"label\": \"DeepSeek\",\n            \"value\": \"deepseek\",\n            \"description\": \"使用 DeepSeek 的高性价比模型\"\n          }\n        ]\n      },\n      \"defaultValue\": \"openai\",\n      \"required\": true\n    },\n    {\n      \"id\": \"apiKey\",\n      \"type\": \"password\",\n      \"label\": \"API Key\",\n      \"prompt\": \"请输入您的 API Key\",\n      \"placeholder\": \"sk-...\",\n      \"required\": true\n    },\n    {\n      \"id\": \"enableProxy\",\n      \"type\": \"confirm\",\n      \"label\": \"启用代理\",\n      \"prompt\": \"是否通过代理访问 API？\",\n      \"defaultValue\": false\n    },\n    {\n      \"id\": \"proxyUrl\",\n      \"type\": \"input\",\n      \"label\": \"代理地址\",\n      \"prompt\": \"输入代理服务器地址\",\n      \"placeholder\": \"http://127.0.0.1:7890\",\n      \"required\": true,\n      \"when\": {\n        \"field\": \"enableProxy\",\n        \"operator\": \"eq\",\n        \"value\": true\n      }\n    }\n  ],\n\n  \"template\": {\n    \"Providers\": [\n      {\n        \"name\": \"#{provider}\",\n        \"api_base_url\": \"#{provider === 'openai' ? 'https://api.openai.com/v1/chat/completions' : 'https://api.deepseek.com/v1/chat/completions'}\",\n        \"api_key\": \"#{apiKey}\",\n        \"models\": [\"gpt-4o\", \"gpt-4o-mini\"]\n      }\n    ],\n    \"Router\": {\n      \"default\": \"#{provider},gpt-4o\",\n      \"background\": \"#{provider},gpt-4o-mini\"\n    }\n  },\n\n  \"configMappings\": [\n    {\n      \"target\": \"PROXY_URL\",\n      \"value\": \"#{proxyUrl}\",\n      \"when\": {\n        \"field\": \"enableProxy\",\n        \"operator\": \"eq\",\n        \"value\": true\n      }\n    }\n  ]\n}\nEOF\n\n# 配置预设（会提示输入）\nccr preset install advanced-config\n\n# 使用预设\nccr advanced-config \"your prompt\"\n```\n\n### 导出当前配置为预设\n\n如果您已经配置好了 CCR，可以导出当前配置：\n\n```bash\n# 导出当前配置\nccr preset export my-exported-preset\n```\n\n导出时会自动：\n- 识别敏感字段（如 `api_key`）并替换为环境变量占位符\n- 生成 `schema` 用于收集用户输入\n- 生成 `template` 和 `configMappings`\n\n可选项：\n\n```bash\nccr preset export my-exported-preset \\\n  --description \"导出的配置\" \\\n  --author \"Your Name\" \\\n  --tags \"production,openai\"\n```\n\n## 预设文件位置\n\n预设保存在：\n\n```\n~/.claude-code-router/presets/\n```\n\n每个预设都是一个目录，包含 `manifest.json` 文件。\n\n## 最佳实践\n\n1. **使用动态配置**：为需要用户输入的配置项使用schema系统\n2. **提供默认值**：为非必填项提供合理的默认值\n3. **条件显示**：使用when条件避免不必要的输入\n4. **清晰的标签**：为每个字段提供清晰的label和prompt\n5. **验证输入**：使用validator确保输入的有效性\n6. **版本控制**：将常用预设保存在版本控制中\n7. **文档化**：为自定义预设添加描述和版本信息\n\n## 下一步\n\n- [CLI 参考](/zh/docs/cli/start) - 完整的 CLI 命令参考\n- [配置](/zh/docs/config/basic) - 详细配置指南\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/server/advanced/custom-router.md",
    "content": "---\ntitle: 自定义路由器\nsidebar_position: 1\n---\n\n# 自定义路由器\n\n使用 JavaScript 编写自己的路由逻辑。\n\n## 创建自定义路由器\n\n创建一个导出路由函数的 JavaScript 文件：\n\n```javascript\n// custom-router.js\nmodule.exports = async function(req, config) {\n  // 获取用户消息\n  const userMessage = req.body.messages.find(m => m.role === 'user')?.content;\n\n  // 自定义逻辑\n  if (userMessage && userMessage.includes('解释代码')) {\n    return 'openrouter,anthropic/claude-3.5-sonnet';\n  }\n\n  // 返回 null 以使用默认路由\n  return null;\n};\n```\n\n## 参数说明\n\n路由函数接收以下参数：\n\n| 参数 | 类型 | 说明 |\n|------|------|------|\n| `req` | object | 来自 Claude Code 的请求对象，包含请求体 |\n| `config` | object | 应用程序的配置对象 |\n\n## 配置\n\n在 `config.json` 中设置 `CUSTOM_ROUTER_PATH` 以使用您的自定义路由器：\n\n```json\n{\n  \"CUSTOM_ROUTER_PATH\": \"/path/to/custom-router.js\"\n}\n```\n\n## 返回格式\n\n路由函数应返回以下格式的字符串：\n\n```\n{provider-name},{model-name}\n```\n\n示例：\n\n```\ndeepseek,deepseek-chat\n```\n\n如果返回 `null`，则回退到默认路由配置。\n\n## 错误处理\n\n如果路由函数抛出错误或返回无效格式，路由器将回退到默认路由配置。\n\n## 示例：基于时间的路由\n\n```javascript\nmodule.exports = async function(req, config) {\n  const hour = new Date().getHours();\n\n  // 工作时间使用更快的模型\n  if (hour >= 9 && hour <= 18) {\n    return 'groq,llama-3.3-70b-versatile';\n  }\n\n  // 非工作时间使用更强大的模型\n  return 'deepseek,deepseek-chat';\n};\n```\n\n## 示例：成本优化\n\n```javascript\nmodule.exports = async function(req, config) {\n  const userMessage = req.body.messages.find(m => m.role === 'user')?.content;\n\n  // 简单任务使用较便宜的模型\n  if (userMessage && userMessage.length < 100) {\n    return 'groq,llama-3.3-70b-versatile';\n  }\n\n  // 复杂任务使用默认模型\n  return null;\n};\n```\n\n## 示例：任务类型路由\n\n```javascript\nmodule.exports = async function(req, config) {\n  const userMessage = req.body.messages.find(m => m.role === 'user')?.content;\n\n  if (!userMessage) return null;\n\n  // 代码相关任务\n  if (userMessage.includes('代码') || userMessage.includes('code')) {\n    return 'deepseek,deepseek-coder';\n  }\n\n  // 解释任务\n  if (userMessage.includes('解释') || userMessage.includes('explain')) {\n    return 'openrouter,anthropic/claude-3.5-sonnet';\n  }\n\n  // 默认\n  return null;\n};\n```\n\n## 测试您的路由器\n\n通过检查日志来测试您的自定义路由器：\n\n```bash\ntail -f ~/.claude-code-router/claude-code-router.log\n```\n\n查找路由决策以查看正在选择哪个模型。\n\n## 子代理路由\n\n对于子代理内的路由，您必须在子代理提示词的**开头**包含 `<CCR-SUBAGENT-MODEL>provider,model</CCR-SUBAGENT-MODEL>` 来指定特定的提供商和模型。\n\n**示例：**\n\n```\n<CCR-SUBAGENT-MODEL>openrouter,anthropic/claude-3.5-sonnet</CCR-SUBAGENT-MODEL>\n请帮我分析这段代码是否存在潜在的优化空间...\n```\n\n## 下一步\n\n- [Agent](/zh/docs/advanced/agents) - 使用 Agent 扩展功能\n- [预设](/zh/docs/advanced/presets) - 使用预定义配置\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/server/advanced/preset-format.md",
    "content": "---\ntitle: Preset 格式规范\nsidebar_position: 4\n---\n\n# Preset 格式规范\n\n本文档详细说明了 Preset 配置文件的格式规范、字段定义和使用方法。\n\n## 概述\n\nPreset 是一个预定义的配置包，用于快速配置 Claude Code Router。Preset 以目录形式存储，内部包含一个 `manifest.json` 文件。\n\n### 文件结构\n\n```\n~/.claude-code-router/presets/<preset-name>/\n└── manifest.json\n```\n\n### 存储位置\n\n- **预设目录**: `~/.claude-code-router/presets/<preset-name>/`\n\n## manifest.json 结构\n\n`manifest.json` 是一个扁平化的 JSON 文件（支持 JSON5 格式），包含三个主要部分：\n\n1. **元数据（Metadata）**: 描述预设的基本信息\n2. **配置（Configuration）**: 实际的配置内容\n3. **动态配置系统**: Schema、Template 和 ConfigMappings\n\n```json\n{\n  // === 元数据字段 ===\n  \"name\": \"my-preset\",\n  \"version\": \"1.0.0\",\n  \"description\": \"我的预设配置\",\n  \"author\": \"作者名\",\n  \"homepage\": \"https://example.com\",\n  \"repository\": \"https://github.com/user/repo\",\n  \"license\": \"MIT\",\n  \"keywords\": [\"openai\", \"production\"],\n  \"ccrVersion\": \"2.0.0\",\n\n  // === 配置字段 ===\n  \"Providers\": [...],\n  \"Router\": {...},\n  \"transformers\": [...],\n  \"StatusLine\": {...},\n  \"PROXY_URL\": \"...\",\n  \"PORT\": 8080,\n\n  // === 动态配置系统 ===\n  \"schema\": [...],\n  \"template\": {...},\n  \"configMappings\": [...],\n  \"userValues\": {...}\n}\n```\n\n## 元数据字段\n\n### 必填字段\n\n| 字段 | 类型 | 说明 |\n|------|------|------|\n| `name` | string | Preset 名称，唯一标识符 |\n| `version` | string | 版本号（遵循 semver 规范） |\n\n### 可选字段\n\n| 字段 | 类型 | 说明 |\n|------|------|------|\n| `description` | string | Preset 描述 |\n| `author` | string | 作者信息 |\n| `homepage` | string | 项目主页 URL |\n| `repository` | string | 源代码仓库 URL |\n| `license` | string | 许可证类型 |\n| `keywords` | string[] | 关键词标签 |\n| `ccrVersion` | string | 兼容的 CCR 版本 |\n| `source` | string | Preset 来源 URL |\n| `sourceType` | string | 来源类型（`local`/`gist`/`registry`） |\n| `checksum` | string | 内容校验和（SHA256） |\n\n### 元数据示例\n\n```json\n{\n  \"name\": \"openai-production\",\n  \"version\": \"1.2.0\",\n  \"description\": \"OpenAI 生产环境配置，包含代理和多模型支持\",\n  \"author\": \"Your Name\",\n  \"homepage\": \"https://github.com/yourname/ccr-presets\",\n  \"repository\": \"https://github.com/yourname/ccr-presets.git\",\n  \"license\": \"MIT\",\n  \"keywords\": [\"openai\", \"production\", \"proxy\"],\n  \"ccrVersion\": \"2.0.0\"\n}\n```\n\n## 配置字段\n\n配置字段直接对应 CCR 的配置文件结构（`config.json`）。\n\n### Providers\n\nProvider 配置数组，定义 LLM 服务提供商。\n\n```json\n{\n  \"Providers\": [\n    {\n      \"name\": \"openai\",\n      \"api_base_url\": \"https://api.openai.com/v1/chat/completions\",\n      \"api_key\": \"${OPENAI_API_KEY}\",\n      \"models\": [\"gpt-4o\", \"gpt-4o-mini\"],\n      \"transformer\": \"anthropic\",\n      \"timeout\": 60000,\n      \"max_retries\": 3\n    }\n  ]\n}\n```\n\n#### Provider 字段说明\n\n| 字段 | 类型 | 必填 | 说明 |\n|------|------|------|------|\n| `name` | string | 是 | Provider 名称（唯一标识） |\n| `api_base_url` | string | 是 | API 基础地址 |\n| `api_key` | string | 是 | API 密钥（可以是环境变量） |\n| `models` | string[] | 是 | 支持的模型列表 |\n| `transformer` | string | 否 | 使用的转换器 |\n| `timeout` | number | 否 | 超时时间（毫秒） |\n| `max_retries` | number | 否 | 最大重试次数 |\n| `headers` | object | 否 | 自定义 HTTP 头 |\n\n### Router\n\n路由配置，定义请求如何路由到不同的模型。\n\n```json\n{\n  \"Router\": {\n    \"default\": \"openai/gpt-4o\",\n    \"background\": \"openai/gpt-4o-mini\",\n    \"think\": \"openai/gpt-4o\",\n    \"longContext\": \"openai/gpt-4o\",\n    \"longContextThreshold\": 100000,\n    \"webSearch\": \"openai/gpt-4o\",\n    \"image\": \"openai/gpt-4o\"\n  }\n}\n```\n\n#### Router 字段说明\n\n| 字段 | 类型 | 说明 |\n|------|------|------|\n| `default` | string | 默认路由（格式：`provider,model`） |\n| `background` | string | 后台任务路由 |\n| `think` | string | 思考模式路由 |\n| `longContext` | string | 长上下文路由 |\n| `longContextThreshold` | number | 长上下文阈值（token 数） |\n| `webSearch` | string | 网络搜索路由 |\n| `image` | string | 图像处理路由 |\n\n### Transformers\n\n转换器配置数组，用于处理不同 Provider 的 API 差异。\n\n```json\n{\n  \"transformers\": [\n    {\n      \"path\": \"./transformers/custom-transformer.js\",\n      \"use\": [\"provider1\", \"provider2\"],\n      \"options\": {\n        \"max_tokens\": 4096\n      }\n    },\n    {\n      \"use\": [\n        [\"provider3\", { \"option\": \"value\" }]\n      ]\n    }\n  ]\n}\n```\n\n#### Transformer 字段说明\n\n| 字段 | 类型 | 说明 |\n|------|------|------|\n| `path` | string | 自定义转换器路径（相对或绝对） |\n| `use` | array | 应用到哪些 Provider |\n| `options` | object | 转换器选项 |\n\n### StatusLine\n\n状态栏配置，自定义终端状态显示。\n\n```json\n{\n  \"StatusLine\": {\n    \"default\": {\n      \"modules\": [\n        {\n          \"type\": \"text\",\n          \"text\": \"CCR\",\n          \"color\": \"cyan\"\n        },\n        {\n          \"type\": \"provider\",\n          \"showModel\": true\n        },\n        {\n          \"type\": \"script\",\n          \"scriptPath\": \"./scripts/status.js\"\n        }\n      ]\n    }\n  }\n}\n```\n\n### 其他配置字段\n\n支持所有 `config.json` 中的字段：\n\n```json\n{\n  \"PORT\": 8080,\n  \"HOST\": \"0.0.0.0\",\n  \"PROXY_URL\": \"http://127.0.0.1:7890\",\n  \"LOG_LEVEL\": \"info\",\n  \"NON_INTERACTIVE_MODE\": false\n}\n```\n\n## 动态配置系统\n\n动态配置系统是 CCR 2.0 的核心功能，允许创建可交互的配置模板。\n\n### Schema（配置输入表单）\n\nSchema 定义了安装时需要用户输入的字段。\n\n#### Schema 字段类型\n\n| 类型 | 说明 | 使用场景 |\n|------|------|----------|\n| `password` | 密码输入（隐藏） | API Key、密钥 |\n| `input` | 单行文本输入 | URL、名称 |\n| `number` | 数字输入 | 端口号、超时时间 |\n| `select` | 单选下拉框 | 选择 Provider、模型 |\n| `multiselect` | 多选框 | 启用功能列表 |\n| `confirm` | 确认框 | 是否启用某功能 |\n| `editor` | 多行文本编辑器 | 自定义配置、脚本 |\n\n#### Schema 字段定义\n\n```json\n{\n  \"schema\": [\n    {\n      \"id\": \"apiKey\",\n      \"type\": \"password\",\n      \"label\": \"API Key\",\n      \"prompt\": \"请输入您的 OpenAI API Key\",\n      \"placeholder\": \"sk-...\",\n      \"required\": true,\n      \"validator\": \"^sk-.*\"\n    },\n    {\n      \"id\": \"provider\",\n      \"type\": \"select\",\n      \"label\": \"选择 Provider\",\n      \"prompt\": \"选择您主要使用的 LLM 提供商\",\n      \"options\": {\n        \"type\": \"static\",\n        \"options\": [\n          {\n            \"label\": \"OpenAI\",\n            \"value\": \"openai\",\n            \"description\": \"使用 OpenAI 的 GPT 模型\"\n          },\n          {\n            \"label\": \"DeepSeek\",\n            \"value\": \"deepseek\",\n            \"description\": \"使用 DeepSeek 的高性价比模型\"\n          }\n        ]\n      },\n      \"defaultValue\": \"openai\",\n      \"required\": true\n    },\n    {\n      \"id\": \"model\",\n      \"type\": \"select\",\n      \"label\": \"模型\",\n      \"prompt\": \"选择默认使用的模型\",\n      \"options\": {\n        \"type\": \"models\",\n        \"providerField\": \"#{provider}\"\n      },\n      \"when\": {\n        \"field\": \"provider\",\n        \"operator\": \"exists\"\n      },\n      \"required\": true\n    },\n    {\n      \"id\": \"maxTokens\",\n      \"type\": \"number\",\n      \"label\": \"最大 Token 数\",\n      \"prompt\": \"设置请求的最大 token 数\",\n      \"min\": 1,\n      \"max\": 128000,\n      \"defaultValue\": 4096\n    },\n    {\n      \"id\": \"useProxy\",\n      \"type\": \"confirm\",\n      \"label\": \"使用代理\",\n      \"prompt\": \"是否通过代理访问 API？\",\n      \"defaultValue\": false\n    },\n    {\n      \"id\": \"proxyUrl\",\n      \"type\": \"input\",\n      \"label\": \"代理地址\",\n      \"prompt\": \"输入代理服务器地址\",\n      \"placeholder\": \"http://127.0.0.1:7890\",\n      \"required\": true,\n      \"when\": {\n        \"field\": \"useProxy\",\n        \"operator\": \"eq\",\n        \"value\": true\n      }\n    },\n    {\n      \"id\": \"customConfig\",\n      \"type\": \"editor\",\n      \"label\": \"自定义配置\",\n      \"prompt\": \"输入 JSON 格式的自定义配置\",\n      \"rows\": 10\n    }\n  ]\n}\n```\n\n#### Schema 字段详细说明\n\n##### 基础字段\n\n| 字段 | 类型 | 必填 | 说明 |\n|------|------|------|------|\n| `id` | string | 是 | 字段唯一标识符（用于变量引用） |\n| `type` | string | 否 | 字段类型（默认 `password`） |\n| `label` | string | 否 | 显示标签 |\n| `prompt` | string | 否 | 提示信息/描述 |\n| `placeholder` | string | 否 | 占位符文本 |\n\n##### 验证字段\n\n| 字段 | 类型 | 说明 |\n|------|------|------|\n| `required` | boolean | 是否必填（默认 `true`） |\n| `validator` | RegExp/string/function | 验证规则 |\n| `min` | number | 最小值（number 类型） |\n| `max` | number | 最大值（number 类型） |\n\n##### 选项字段（select/multiselect）\n\n| 字段 | 类型 | 说明 |\n|------|------|------|\n| `options` | array/object | 静态选项数组或动态选项配置 |\n\n##### 条件字段\n\n| 字段 | 类型 | 说明 |\n|------|------|------|\n| `when` | object/object[] | 显示条件（支持 AND 逻辑） |\n| `defaultValue` | any | 默认值 |\n| `dependsOn` | string[] | 显式声明依赖字段 |\n\n#### 条件运算符\n\n| 运算符 | 说明 | 示例 |\n|--------|------|------|\n| `eq` | 等于 | `{\"field\": \"type\", \"operator\": \"eq\", \"value\": \"openai\"}` |\n| `ne` | 不等于 | `{\"field\": \"advanced\", \"operator\": \"ne\", \"value\": true}` |\n| `in` | 包含于（数组） | `{\"field\": \"feature\", \"operator\": \"in\", \"value\": [\"a\", \"b\"]}` |\n| `nin` | 不包含于（数组） | `{\"field\": \"type\", \"operator\": \"nin\", \"value\": [\"x\", \"y\"]}` |\n| `exists` | 字段存在 | `{\"field\": \"apiKey\", \"operator\": \"exists\"}` |\n| `gt` | 大于 | `{\"field\": \"count\", \"operator\": \"gt\", \"value\": 0}` |\n| `lt` | 小于 | `{\"field\": \"count\", \"operator\": \"lt\", \"value\": 100}` |\n| `gte` | 大于等于 | `{\"field\": \"count\", \"operator\": \"gte\", \"value\": 1}` |\n| `lte` | 小于等于 | `{\"field\": \"count\", \"operator\": \"lte\", \"value\": 99}` |\n\n#### 动态选项类型\n\n##### static - 静态选项\n\n```json\n{\n  \"options\": {\n    \"type\": \"static\",\n    \"options\": [\n      {\"label\": \"选项1\", \"value\": \"value1\"},\n      {\"label\": \"选项2\", \"value\": \"value2\"}\n    ]\n  }\n}\n```\n\n##### providers - 从 Providers 配置提取\n\n```json\n{\n  \"options\": {\n    \"type\": \"providers\"\n  }\n}\n```\n自动从 `Providers` 数组中提取 `name` 作为选项。\n\n##### models - 从指定 Provider 的 models 提取\n\n```json\n{\n  \"options\": {\n    \"type\": \"models\",\n    \"providerField\": \"#{selectedProvider}\"\n  }\n}\n```\n根据用户选择的 Provider，动态显示该 Provider 的 models。\n\n### Template（配置模板）\n\nTemplate 定义了如何根据用户输入生成配置。\n\n#### 变量语法\n\n使用 `#{变量名}` 语法引用用户输入：\n\n```json\n{\n  \"template\": {\n    \"Providers\": [\n      {\n        \"name\": \"#{providerName}\",\n        \"api_base_url\": \"#{baseUrl}\",\n        \"api_key\": \"#{apiKey}\",\n        \"models\": [\"#{defaultModel}\"]\n      }\n    ],\n    \"Router\": {\n      \"default\": \"#{providerName}/#{defaultModel}\"\n    }\n  }\n}\n```\n\n#### Template 示例\n\n```json\n{\n  \"template\": {\n    \"Providers\": [\n      {\n        \"name\": \"#{primaryProvider}\",\n        \"api_base_url\": \"#{baseUrl}\",\n        \"api_key\": \"#{apiKey}\",\n        \"models\": [\"#{defaultModel}\"],\n        \"timeout\": #{timeout}\n      }\n    ],\n    \"Router\": {\n      \"default\": \"#{primaryProvider}/#{defaultModel}\",\n      \"background\": \"#{primaryProvider}/#{backgroundModel}\"\n    },\n    \"PROXY_URL\": \"#{proxyUrl}\",\n    \"PORT\": #{port}\n  }\n}\n```\n\n### ConfigMappings（配置映射）\n\nConfigMappings 用于精确控制用户输入值如何映射到配置的特定位置。\n\n#### ConfigMapping 结构\n\n```json\n{\n  \"configMappings\": [\n    {\n      \"target\": \"Providers[0].api_key\",\n      \"value\": \"#{apiKey}\"\n    },\n    {\n      \"target\": \"PROXY_URL\",\n      \"value\": \"#{proxyUrl}\",\n      \"when\": {\n        \"field\": \"useProxy\",\n        \"operator\": \"eq\",\n        \"value\": true\n      }\n    },\n    {\n      \"target\": \"PORT\",\n      \"value\": 8080\n    }\n  ]\n}\n```\n\n#### 字段说明\n\n| 字段 | 类型 | 说明 |\n|------|------|------|\n| `target` | string | 目标字段路径（支持数组语法） |\n| `value` | string/any | 值来源（变量引用或固定值） |\n| `when` | object/object[] | 应用条件 |\n\n#### 目标路径语法\n\n- `Providers[0].api_key` - 第一个 Provider 的 api_key\n- `Router.default` - Router 的 default 字段\n- `PORT` - 顶层配置字段\n\n### userValues（用户值存储）\n\nuserValues 存储用户在安装时填写的值，运行时自动应用。\n\n```json\n{\n  \"userValues\": {\n    \"apiKey\": \"sk-xxx...\",\n    \"provider\": \"openai\",\n    \"defaultModel\": \"gpt-4o\",\n    \"useProxy\": true,\n    \"proxyUrl\": \"http://127.0.0.1:7890\"\n  }\n}\n```\n\n## 敏感字段处理\n\nCCR 会自动识别敏感字段（如 `api_key`、`secret`、`password` 等），并将其替换为环境变量占位符。\n\n### 自动识别的敏感字段\n\n- `api_key`, `apiKey`, `apikey`\n- `api_secret`, `apiSecret`\n- `secret`, `SECRET`\n- `token`, `TOKEN`\n- `password`, `PASSWORD`\n- `private_key`, `privateKey`\n- `access_key`, `accessKey`\n\n### 环境变量占位符格式\n\n```bash\n# 推荐格式\n${VARIABLE_NAME}\n\n# 也支持\n$VARIABLE_NAME\n```\n\n### 示例\n\n**原始配置:**\n```json\n{\n  \"Providers\": [\n    {\n      \"name\": \"openai\",\n      \"api_key\": \"sk-abc123...\"\n    }\n  ]\n}\n```\n\n**导出后:**\n```json\n{\n  \"Providers\": [\n    {\n      \"name\": \"openai\",\n      \"api_key\": \"${OPENAI_API_KEY}\"\n    }\n  ]\n}\n```\n\n## 完整示例\n\n### 简单预设（无动态配置）\n\n```json\n{\n  \"name\": \"simple-openai\",\n  \"version\": \"1.0.0\",\n  \"description\": \"简单的 OpenAI 配置\",\n  \"author\": \"Your Name\",\n\n  \"Providers\": [\n    {\n      \"name\": \"openai\",\n      \"api_base_url\": \"https://api.openai.com/v1/chat/completions\",\n      \"api_key\": \"${OPENAI_API_KEY}\",\n      \"models\": [\"gpt-4o\", \"gpt-4o-mini\"]\n    }\n  ],\n\n  \"Router\": {\n    \"default\": \"openai/gpt-4o\",\n    \"background\": \"openai/gpt-4o-mini\"\n  }\n}\n```\n\n### 高级预设（动态配置）\n\n```json\n{\n  \"name\": \"multi-provider-advanced\",\n  \"version\": \"2.0.0\",\n  \"description\": \"多 Provider 高级配置，支持动态选择和代理\",\n  \"author\": \"Your Name\",\n  \"keywords\": [\"openai\", \"deepseek\", \"proxy\", \"multi-provider\"],\n  \"ccrVersion\": \"2.0.0\",\n\n  \"schema\": [\n    {\n      \"id\": \"primaryProvider\",\n      \"type\": \"select\",\n      \"label\": \"主要 Provider\",\n      \"prompt\": \"选择您主要使用的 LLM 提供商\",\n      \"options\": {\n        \"type\": \"static\",\n        \"options\": [\n          {\n            \"label\": \"OpenAI\",\n            \"value\": \"openai\",\n            \"description\": \"使用 OpenAI 的 GPT 模型，质量高\"\n          },\n          {\n            \"label\": \"DeepSeek\",\n            \"value\": \"deepseek\",\n            \"description\": \"使用 DeepSeek 的高性价比模型\"\n          }\n        ]\n      },\n      \"required\": true,\n      \"defaultValue\": \"openai\"\n    },\n    {\n      \"id\": \"apiKey\",\n      \"type\": \"password\",\n      \"label\": \"API Key\",\n      \"prompt\": \"请输入您的 API Key\",\n      \"placeholder\": \"sk-...\",\n      \"required\": true,\n      \"validator\": \"^sk-.+\"\n    },\n    {\n      \"id\": \"defaultModel\",\n      \"type\": \"select\",\n      \"label\": \"默认模型\",\n      \"prompt\": \"选择默认使用的模型\",\n      \"options\": {\n        \"type\": \"static\",\n        \"options\": [\n          {\"label\": \"GPT-4o\", \"value\": \"gpt-4o\"},\n          {\"label\": \"GPT-4o-mini\", \"value\": \"gpt-4o-mini\"}\n        ]\n      },\n      \"required\": true,\n      \"defaultValue\": \"gpt-4o\",\n      \"when\": {\n        \"field\": \"primaryProvider\",\n        \"operator\": \"eq\",\n        \"value\": \"openai\"\n      }\n    },\n    {\n      \"id\": \"backgroundModel\",\n      \"type\": \"select\",\n      \"label\": \"后台任务模型\",\n      \"prompt\": \"选择用于后台任务的轻量级模型\",\n      \"options\": {\n        \"type\": \"static\",\n        \"options\": [\n          {\"label\": \"GPT-4o-mini\", \"value\": \"gpt-4o-mini\"}\n        ]\n      },\n      \"required\": true,\n      \"defaultValue\": \"gpt-4o-mini\",\n      \"when\": {\n        \"field\": \"primaryProvider\",\n        \"operator\": \"eq\",\n        \"value\": \"openai\"\n      }\n    },\n    {\n      \"id\": \"maxTokens\",\n      \"type\": \"number\",\n      \"label\": \"最大 Token 数\",\n      \"prompt\": \"设置单次请求的最大 token 数\",\n      \"min\": 1,\n      \"max\": 128000,\n      \"defaultValue\": 4096\n    },\n    {\n      \"id\": \"timeout\",\n      \"type\": \"number\",\n      \"label\": \"超时时间（秒）\",\n      \"prompt\": \"设置 API 请求超时时间\",\n      \"min\": 10,\n      \"max\": 300,\n      \"defaultValue\": 60\n    },\n    {\n      \"id\": \"enableProxy\",\n      \"type\": \"confirm\",\n      \"label\": \"启用代理\",\n      \"prompt\": \"是否通过代理访问 API？\",\n      \"defaultValue\": false\n    },\n    {\n      \"id\": \"proxyUrl\",\n      \"type\": \"input\",\n      \"label\": \"代理地址\",\n      \"prompt\": \"输入代理服务器地址\",\n      \"placeholder\": \"http://127.0.0.1:7890\",\n      \"required\": true,\n      \"when\": {\n        \"field\": \"enableProxy\",\n        \"operator\": \"eq\",\n        \"value\": true\n      },\n      \"validator\": \"^https?://.+\"\n    },\n    {\n      \"id\": \"features\",\n      \"type\": \"multiselect\",\n      \"label\": \"启用功能\",\n      \"prompt\": \"选择要启用的额外功能\",\n      \"options\": {\n        \"type\": \"static\",\n        \"options\": [\n          {\"label\": \"长上下文支持\", \"value\": \"longContext\"},\n          {\"label\": \"网络搜索\", \"value\": \"webSearch\"},\n          {\"label\": \"图像处理\", \"value\": \"image\"}\n        ]\n      },\n      \"defaultValue\": []\n    }\n  ],\n\n  \"template\": {\n    \"Providers\": [\n      {\n        \"name\": \"#{primaryProvider}\",\n        \"api_base_url\": \"#{primaryProvider === 'openai' ? 'https://api.openai.com/v1/chat/completions' : 'https://api.deepseek.com/v1/chat/completions'}\",\n        \"api_key\": \"#{apiKey}\",\n        \"models\": [\n          \"#{defaultModel}\",\n          \"#{backgroundModel}\"\n        ],\n        \"timeout\": #{timeout * 1000}\n      }\n    ],\n    \"Router\": {\n      \"default\": \"#{primaryProvider}/#{defaultModel}\",\n      \"background\": \"#{primaryProvider}/#{backgroundModel}\"\n    },\n    \"NON_INTERACTIVE_MODE\": false\n  },\n\n  \"configMappings\": [\n    {\n      \"target\": \"PROXY_URL\",\n      \"value\": \"#{proxyUrl}\",\n      \"when\": {\n        \"field\": \"enableProxy\",\n        \"operator\": \"eq\",\n        \"value\": true\n      }\n    },\n    {\n      \"target\": \"Router.longContext\",\n      \"value\": \"#{primaryProvider}/#{defaultModel}\",\n      \"when\": {\n        \"field\": \"features\",\n        \"operator\": \"in\",\n        \"value\": [\"longContext\"]\n      }\n    },\n    {\n      \"target\": \"Router.webSearch\",\n      \"value\": \"#{primaryProvider}/#{defaultModel}\",\n      \"when\": {\n        \"field\": \"features\",\n        \"operator\": \"in\",\n        \"value\": [\"webSearch\"]\n      }\n    },\n    {\n      \"target\": \"Router.image\",\n      \"value\": \"#{primaryProvider}/#{defaultModel}\",\n      \"when\": {\n        \"field\": \"features\",\n        \"operator\": \"in\",\n        \"value\": [\"image\"]\n      }\n    }\n  ]\n}\n```\n\n## 验证规则\n\n### Preset 验证检查项\n\n1. **元数据验证**\n   - ✓ `name` 字段存在\n   - ✓ `version` 字段存在（警告）\n\n2. **配置验证**\n   - ✓ `config` 部分存在\n   - ✓ 每个 Provider 有 `name` 字段\n   - ✓ 每个 Provider 有 `api_base_url` 字段\n   - ✓ 每个 Provider 有 `models` 数组（警告）\n\n3. **Schema 验证**\n   - ✓ 字段 `id` 唯一\n   - ✓ 条件字段引用存在\n   - ✓ 动态选项配置正确\n\n### 错误和警告\n\n**错误（Error）:**\n- 缺少必填字段\n- Provider 配置不完整\n- Schema 字段重复\n\n**警告（Warning）:**\n- 缺少可选字段\n- Provider 没有 models\n- 未使用的 schema 字段\n\n## 最佳实践\n\n### 1. 使用动态配置系统\n\n```json\n{\n  \"schema\": [\n    {\n      \"id\": \"apiKey\",\n      \"type\": \"password\",\n      \"label\": \"API Key\",\n      \"required\": true\n    }\n  ],\n  \"template\": {\n    \"Providers\": [\n      {\n        \"api_key\": \"#{apiKey}\"\n      }\n    ]\n  }\n}\n```\n\n### 2. 提供合理的默认值\n\n```json\n{\n  \"id\": \"timeout\",\n  \"type\": \"number\",\n  \"label\": \"超时时间\",\n  \"defaultValue\": 60,\n  \"min\": 10,\n  \"max\": 300\n}\n```\n\n### 3. 使用条件显示减少不必要的输入\n\n```json\n{\n  \"id\": \"proxyUrl\",\n  \"type\": \"input\",\n  \"label\": \"代理地址\",\n  \"when\": {\n    \"field\": \"useProxy\",\n    \"operator\": \"eq\",\n    \"value\": true\n  }\n}\n```\n\n### 4. 清晰的标签和提示\n\n```json\n{\n  \"id\": \"apiKey\",\n  \"type\": \"password\",\n  \"label\": \"OpenAI API Key\",\n  \"prompt\": \"请输入您的 OpenAI API Key（以 sk- 开头）\",\n  \"placeholder\": \"sk-...\",\n  \"validator\": \"^sk-.+\"\n}\n```\n\n### 5. 使用验证确保数据质量\n\n```json\n{\n  \"id\": \"port\",\n  \"type\": \"number\",\n  \"label\": \"端口号\",\n  \"min\": 1024,\n  \"max\": 65535,\n  \"validator\": (value) => {\n    if (value < 1024 || value > 65535) {\n      return \"端口号必须在 1024-65535 之间\";\n    }\n    return true;\n  }\n}\n```\n\n### 6. 版本控制\n\n遵循 semver 规范：\n- `1.0.0` - 初始版本\n- `1.1.0` - 新增功能（向后兼容）\n- `1.0.1` - Bug 修复\n- `2.0.0` - 破坏性变更\n\n### 7. 文档化\n\n```json\n{\n  \"name\": \"my-preset\",\n  \"version\": \"1.0.0\",\n  \"description\": \"详细的预设描述，说明用途和特点\",\n  \"author\": \"作者名 <email@example.com>\",\n  \"homepage\": \"https://github.com/user/preset\",\n  \"repository\": \"https://github.com/user/preset.git\",\n  \"keywords\": [\"openai\", \"production\", \"proxy\"],\n  \"license\": \"MIT\"\n}\n```\n\n### 8. 使用相对路径\n\n对于预设中的自定义文件（如转换器、脚本），使用相对路径：\n\n```json\n{\n  \"transformers\": [\n    {\n      \"path\": \"./transformers/custom.js\"\n    }\n  ],\n  \"StatusLine\": {\n    \"default\": {\n      \"modules\": [\n        {\n          \"type\": \"script\",\n          \"scriptPath\": \"./scripts/status.js\"\n        }\n      ]\n    }\n  }\n}\n```\n\n相对路径会在安装时自动转换为绝对路径。\n\n## 导出和导入\n\n### 导出当前配置\n\n```bash\nccr preset export my-preset\n```\n\n可选项：\n\n```bash\nccr preset export my-preset \\\n  --description \"我的预设\" \\\n  --author \"Your Name\" \\\n  --tags \"openai,production\"\n```\n\n### 安装预设\n\n**CLI 方式：**\n\n```bash\n# 从本地目录安装\nccr preset install /path/to/preset\n\n# 重新配置已安装的预设\nccr preset install my-preset\n```\n\n:::note 注意\nCLI 方式**不支持**从 URL 安装。如需从 GitHub 安装，请使用 Web UI 或先克隆到本地。\n:::\n\n**Web UI 方式：**\n\n1. 访问 Web UI：`ccr ui`\n2. 点击\"预设商城\"按钮\n3. 选择预设或输入 GitHub 仓库 URL\n4. 点击安装\n\n### 管理预设\n\n```bash\n# 列出所有预设\nccr preset list\n\n# 查看预设信息\nccr preset info my-preset\n\n# 删除预设\nccr preset delete my-preset\n```\n\n## 常见问题\n\n### Q: 如何处理多个 Provider？\n\nA: 在 template 中定义多个 Provider，使用条件逻辑：\n\n```json\n{\n  \"schema\": [\n    {\n      \"id\": \"useSecondary\",\n      \"type\": \"confirm\",\n      \"label\": \"启用备用 Provider\"\n    },\n    {\n      \"id\": \"secondaryKey\",\n      \"type\": \"password\",\n      \"label\": \"备用 API Key\",\n      \"when\": {\n        \"field\": \"useSecondary\",\n        \"operator\": \"eq\",\n        \"value\": true\n      }\n    }\n  ],\n  \"template\": {\n    \"Providers\": [\n      {\n        \"name\": \"primary\",\n        \"api_key\": \"#{primaryKey}\"\n      },\n      {\n        \"name\": \"secondary\",\n        \"api_key\": \"#{secondaryKey}\"\n      }\n    ]\n  },\n  \"configMappings\": [\n    {\n      \"target\": \"Providers\",\n      \"value\": [\n        {\n          \"name\": \"primary\",\n          \"api_key\": \"#{primaryKey}\"\n        }\n      ],\n      \"when\": {\n        \"field\": \"useSecondary\",\n        \"operator\": \"ne\",\n        \"value\": true\n      }\n    }\n  ]\n}\n```\n\n### Q: 如何支持条件配置？\n\nA: 使用 `when` 条件和 `configMappings`：\n\n```json\n{\n  \"configMappings\": [\n    {\n      \"target\": \"PROXY_URL\",\n      \"value\": \"#{proxyUrl}\",\n      \"when\": {\n        \"field\": \"useProxy\",\n        \"operator\": \"eq\",\n        \"value\": true\n      }\n    }\n  ]\n}\n```\n\n### Q: 如何验证用户输入？\n\nA: 使用 `validator` 字段：\n\n```json\n{\n  \"id\": \"url\",\n  \"type\": \"input\",\n  \"label\": \"API 地址\",\n  \"validator\": \"^https?://.+\"\n}\n```\n\n### Q: 如何创建多语言预设？\n\nA: 使用条件选择语言：\n\n```json\n{\n  \"schema\": [\n    {\n      \"id\": \"language\",\n      \"type\": \"select\",\n      \"label\": \"语言\",\n      \"options\": [\n        {\"label\": \"中文\", \"value\": \"zh\"},\n        {\"label\": \"English\", \"value\": \"en\"}\n      ]\n    }\n  ]\n}\n```\n\n## 相关文档\n\n- [预设配置使用指南](/zh/docs/advanced/presets)\n- [配置基础](/zh/docs/config/basic)\n- [Provider 配置](/zh/docs/config/providers)\n- [路由配置](/zh/docs/config/routing)\n- [转换器配置](/zh/docs/config/transformers)\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/server/api/config-api.md",
    "content": "# 配置 API\n\n## GET /api/config\n\n获取当前服务器配置。\n\n### 请求示例\n\n```bash\ncurl http://localhost:3456/api/config \\\n  -H \"x-api-key: your-api-key\"\n```\n\n### 响应示例\n\n```json\n{\n  \"HOST\": \"0.0.0.0\",\n  \"PORT\": 3456,\n  \"APIKEY\": \"sk-xxxxx\",\n  \"Providers\": [\n    {\n      \"name\": \"openai\",\n      \"baseUrl\": \"https://api.openai.com/v1\",\n      \"apiKey\": \"sk-...\",\n      \"models\": [\"gpt-4\", \"gpt-3.5-turbo\"]\n    }\n  ],\n  \"Router\": {\n    \"default\": \"openai,gpt-4\"\n  },\n  \"transformers\": [\n    \"anthropic\"\n  ]\n}\n```\n\n## POST /api/config\n\n更新服务器配置。更新后会自动备份旧配置。\n\n### 请求示例\n\n```bash\ncurl -X POST http://localhost:3456/api/config \\\n  -H \"x-api-key: your-api-key\" \\\n  -H \"content-type: application/json\" \\\n  -d '{\n    \"HOST\": \"0.0.0.0\",\n    \"PORT\": 3456,\n    \"Providers\": [\n      {\n        \"name\": \"openai\",\n        \"baseUrl\": \"https://api.openai.com/v1\",\n        \"apiKey\": \"$OPENAI_API_KEY\",\n        \"models\": [\"gpt-4\"]\n      }\n    ],\n    \"Router\": {\n      \"default\": \"openai,gpt-4\"\n    }\n  }'\n```\n\n### 配置对象结构\n\n#### 基础配置\n\n| 字段 | 类型 | 必需 | 说明 |\n|------|------|------|------|\n| `HOST` | string | 否 | 监听地址（默认 127.0.0.1） |\n| `PORT` | integer | 否 | 监听端口（默认 3456） |\n| `APIKEY` | string | 否 | API 密钥 |\n| `LOG` | boolean | 否 | 是否启用日志（默认 true） |\n| `LOG_LEVEL` | string | 否 | 日志级别（debug/info/warn/error） |\n\n#### Providers 配置\n\n```json\n{\n  \"Providers\": [\n    {\n      \"name\": \"provider-name\",\n      \"baseUrl\": \"https://api.example.com/v1\",\n      \"apiKey\": \"your-api-key\",\n      \"models\": [\"model-1\", \"model-2\"]\n    }\n  ]\n}\n```\n\n| 字段 | 类型 | 必需 | 说明 |\n|------|------|------|------|\n| `name` | string | 是 | 提供商名称 |\n| `baseUrl` | string | 是 | API 基础 URL |\n| `apiKey` | string | 是 | API 密钥 |\n| `models` | array | 是 | 支持的模型列表 |\n\n#### Router 配置\n\n```json\n{\n  \"Router\": {\n    \"default\": \"provider,model\",\n    \"longContextThreshold\": 100000,\n    \"routes\": {\n      \"background\": \"lightweight-model\",\n      \"think\": \"powerful-model\",\n      \"longContext\": \"long-context-model\",\n      \"webSearch\": \"search-model\",\n      \"image\": \"vision-model\"\n    }\n  }\n}\n```\n\n#### Transformers 配置\n\n```json\n{\n  \"transformers\": [\n    {\n      \"name\": \"anthropic\",\n      \"provider\": \"provider-name\",\n      \"models\": [\"model-1\"],\n      \"options\": {}\n    }\n  ]\n}\n```\n\n### 响应示例\n\n成功：\n\n```json\n{\n  \"success\": true,\n  \"message\": \"Config saved successfully\"\n}\n```\n\n### 配置备份\n\n每次更新配置时，旧配置会自动备份到：\n\n```\n~/.claude-code-router/config.backup.{timestamp}.json\n```\n\n保留最近 3 个备份。\n\n## GET /api/transformers\n\n获取服务器加载的所有转换器列表。\n\n### 请求示例\n\n```bash\ncurl http://localhost:3456/api/transformers \\\n  -H \"x-api-key: your-api-key\"\n```\n\n### 响应示例\n\n```json\n{\n  \"transformers\": [\n    {\n      \"name\": \"anthropic\",\n      \"endpoint\": null\n    },\n    {\n      \"name\": \"openai\",\n      \"endpoint\": null\n    },\n    {\n      \"name\": \"gemini\",\n      \"endpoint\": \"https://generativelanguage.googleapis.com\"\n    }\n  ]\n}\n```\n\n### 转换器列表\n\n内置转换器：\n\n- `anthropic` - Anthropic Claude 格式\n- `openai` - OpenAI 格式\n- `deepseek` - DeepSeek 格式\n- `gemini` - Google Gemini 格式\n- `openrouter` - OpenRouter 格式\n- `groq` - Groq 格式\n- `maxtoken` - 调整 max_tokens 参数\n- `tooluse` - 工具使用转换\n- `reasoning` - 推理模式转换\n- `enhancetool` - 增强工具功能\n\n## 环境变量插值\n\n配置支持环境变量插值：\n\n```json\n{\n  \"Providers\": [\n    {\n      \"apiKey\": \"$OPENAI_API_KEY\"\n    }\n  ]\n}\n```\n\n或使用 `${VAR_NAME}` 格式：\n\n```json\n{\n  \"baseUrl\": \"${API_BASE_URL}\"\n}\n```\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/server/api/logs-api.md",
    "content": "# 日志 API\n\n## GET /api/logs/files\n\n获取所有可用的日志文件列表。\n\n### 请求示例\n\n```bash\ncurl http://localhost:3456/api/logs/files \\\n  -H \"x-api-key: your-api-key\"\n```\n\n### 响应示例\n\n```json\n[\n  {\n    \"name\": \"ccr-20241226143022.log\",\n    \"path\": \"/home/user/.claude-code-router/logs/ccr-20241226143022.log\",\n    \"size\": 1024000,\n    \"lastModified\": \"2024-12-26T14:30:22.000Z\"\n  },\n  {\n    \"name\": \"ccr-20241226143021.log\",\n    \"path\": \"/home/user/.claude-code-router/logs/ccr-20241226143021.log\",\n    \"size\": 980000,\n    \"lastModified\": \"2024-12-26T14:30:21.000Z\"\n  }\n]\n```\n\n### 字段说明\n\n| 字段 | 类型 | 说明 |\n|------|------|------|\n| `name` | string | 文件名 |\n| `path` | string | 完整文件路径 |\n| `size` | integer | 文件大小（字节） |\n| `lastModified` | string | 最后修改时间（ISO 8601） |\n\n文件按修改时间倒序排列。\n\n## GET /api/logs\n\n获取指定日志文件的内容。\n\n### 查询参数\n\n| 参数 | 类型 | 必需 | 说明 |\n|------|------|------|------|\n| `file` | string | 否 | 日志文件路径（默认使用 app.log） |\n\n### 请求示例（获取默认日志）\n\n```bash\ncurl \"http://localhost:3456/api/logs\" \\\n  -H \"x-api-key: your-api-key\"\n```\n\n### 请求示例（获取指定文件）\n\n```bash\ncurl \"http://localhost:3456/api/logs?file=/home/user/.claude-code-router/logs/ccr-20241226143022.log\" \\\n  -H \"x-api-key: your-api-key\"\n```\n\n### 响应示例\n\n```json\n[\n  \"{\\\"level\\\":30,\\\"time\\\":1703550622000,\\\"pid\\\":12345,\\\"hostname\\\":\\\"server\\\",\\\"msg\\\":\\\"Incoming request\\\",\\\"req\\\":{\\\"id\\\":1,\\\"method\\\":\\\"POST\\\",\\\"url\\\":\\\"/v1/messages\\\",\\\"remoteAddress\\\":\\\"127.0.0.1\\\"}}\",\n  \"{\\\"level\\\":30,\\\"time\\\":1703550622500,\\\"pid\\\":12345,\\\"hostname\\\":\\\"server\\\",\\\"msg\\\":\\\"Request completed\\\",\\\"res\\\":{\\\"statusCode\\\":200,\\\"responseTime\\\":500}}\",\n  \"...\"\n]\n```\n\n返回的是日志行数组，每行是一个 JSON 字符串。\n\n### 日志格式\n\n日志使用 Pino 格式：\n\n```json\n{\n  \"level\": 30,\n  \"time\": 1703550622000,\n  \"pid\": 12345,\n  \"hostname\": \"server\",\n  \"msg\": \"Incoming request\",\n  \"req\": {\n    \"id\": 1,\n    \"method\": \"POST\",\n    \"url\": \"/v1/messages\",\n    \"remoteAddress\": \"127.0.0.1\"\n  }\n}\n```\n\n### 日志级别\n\n| 级别 | 值 | 说明 |\n|------|------|------|\n| `trace` | 10 | 最详细的日志 |\n| `debug` | 20 | 调试信息 |\n| `info` | 30 | 一般信息 |\n| `warn` | 40 | 警告信息 |\n| `error` | 50 | 错误信息 |\n| `fatal` | 60 | 致命错误 |\n\n## DELETE /api/logs\n\n清除指定日志文件的内容。\n\n### 查询参数\n\n| 参数 | 类型 | 必需 | 说明 |\n|------|------|------|------|\n| `file` | string | 否 | 日志文件路径（默认使用 app.log） |\n\n### 请求示例（清除默认日志）\n\n```bash\ncurl -X DELETE \"http://localhost:3456/api/logs\" \\\n  -H \"x-api-key: your-api-key\"\n```\n\n### 请求示例（清除指定文件）\n\n```bash\ncurl -X DELETE \"http://localhost:3456/api/logs?file=/home/user/.claude-code-router/logs/ccr-20241226143022.log\" \\\n  -H \"x-api-key: your-api-key\"\n```\n\n### 响应示例\n\n```json\n{\n  \"success\": true,\n  \"message\": \"Logs cleared successfully\"\n}\n```\n\n## 日志位置\n\n### 服务器日志\n\n位置：`~/.claude-code-router/logs/`\n\n文件命名：`ccr-{YYYYMMDD}{HH}{MM}{SS}.log`\n\n内容：HTTP 请求、API 调用、服务器事件\n\n### 应用日志\n\n位置：`~/.claude-code-router/claude-code-router.log`\n\n内容：路由决策、业务逻辑事件\n\n## 日志轮转\n\n服务器日志使用 rotating-file-stream 自动轮转：\n\n- **maxFiles**: 3 - 保留最近 3 个日志文件\n- **interval**: 1d - 每天轮转\n- **maxSize**: 50M - 单个文件最大 50MB\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/server/api/messages-api.md",
    "content": "# 消息 API\n\n## POST /v1/messages\n\n发送消息到 LLM，兼容 Anthropic Claude API 格式。\n\n### 请求格式\n\n```bash\ncurl -X POST http://localhost:3456/v1/messages \\\n  -H \"x-api-key: your-api-key\" \\\n  -H \"content-type: application/json\" \\\n  -d '{\n    \"model\": \"claude-3-5-sonnet-20241022\",\n    \"max_tokens\": 1024,\n    \"messages\": [\n      {\n        \"role\": \"user\",\n        \"content\": \"Hello, Claude!\"\n      }\n    ]\n  }'\n```\n\n### 请求参数\n\n| 参数 | 类型 | 必需 | 说明 |\n|------|------|------|------|\n| `model` | string | 是 | 模型名称（会被路由到实际提供商） |\n| `messages` | array | 是 | 消息数组 |\n| `max_tokens` | integer | 是 | 最大生成 Token 数 |\n| `system` | string | 否 | 系统提示词 |\n| `tools` | array | 否 | 可用工具列表 |\n| `stream` | boolean | 否 | 是否使用流式响应（默认 false） |\n| `temperature` | number | 否 | 温度参数（0-1） |\n\n### 消息对象格式\n\n```json\n{\n  \"role\": \"user|assistant\",\n  \"content\": \"string | array\"\n}\n```\n\n### 响应格式（非流式）\n\n```json\n{\n  \"id\": \"msg_xxx\",\n  \"type\": \"message\",\n  \"role\": \"assistant\",\n  \"content\": [\n    {\n      \"type\": \"text\",\n      \"text\": \"Hello! How can I help you today?\"\n    }\n  ],\n  \"model\": \"claude-3-5-sonnet-20241022\",\n  \"stop_reason\": \"end_turn\",\n  \"usage\": {\n    \"input_tokens\": 10,\n    \"output_tokens\": 20\n  }\n}\n```\n\n### 流式响应\n\n设置 `stream: true` 启用流式响应：\n\n```json\n{\n  \"model\": \"claude-3-5-sonnet-20241022\",\n  \"max_tokens\": 1024,\n  \"messages\": [...],\n  \"stream\": true\n}\n```\n\n流式响应事件类型：\n\n- `message_start` - 消息开始\n- `content_block_start` - 内容块开始\n- `content_block_delta` - 内容增量\n- `content_block_stop` - 内容块结束\n- `message_delta` - 消息元数据（usage）\n- `message_stop` - 消息结束\n\n### 工具使用\n\n支持函数调用（Tool Use）：\n\n```json\n{\n  \"model\": \"claude-3-5-sonnet-20241022\",\n  \"max_tokens\": 1024,\n  \"messages\": [\n    {\n      \"role\": \"user\",\n      \"content\": \"What's the weather like?\"\n    }\n  ],\n  \"tools\": [\n    {\n      \"name\": \"get_weather\",\n      \"description\": \"Get the current weather\",\n      \"input_schema\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"location\": {\n            \"type\": \"string\",\n            \"description\": \"City name\"\n          }\n        },\n        \"required\": [\"location\"]\n      }\n    }\n  ]\n}\n```\n\n### 多模态支持\n\n支持图片输入：\n\n```json\n{\n  \"role\": \"user\",\n  \"content\": [\n    {\n      \"type\": \"image\",\n      \"source\": {\n        \"type\": \"base64\",\n        \"media_type\": \"image/png\",\n        \"data\": \"iVBORw0KGgo...\"\n      }\n    },\n    {\n      \"type\": \"text\",\n      \"text\": \"Describe this image\"\n    }\n  ]\n}\n```\n\n## POST /v1/messages/count_tokens\n\n计算消息的 Token 数量。\n\n### 请求格式\n\n```bash\ncurl -X POST http://localhost:3456/v1/messages/count_tokens \\\n  -H \"x-api-key: your-api-key\" \\\n  -H \"content-type: application/json\" \\\n  -d '{\n    \"model\": \"claude-3-5-sonnet-20241022\",\n    \"messages\": [\n      {\n        \"role\": \"user\",\n        \"content\": \"Hello!\"\n      }\n    ],\n    \"tools\": [],\n    \"system\": \"You are a helpful assistant.\"\n  }'\n```\n\n### 请求参数\n\n| 参数 | 类型 | 必需 | 说明 |\n|------|------|------|------|\n| `model` | string | 是 | 模型名称 |\n| `messages` | array | 是 | 消息数组 |\n| `tools` | array | 否 | 工具列表 |\n| `system` | string | 否 | 系统提示词 |\n\n### 响应格式\n\n```json\n{\n  \"input_tokens\": 42\n}\n```\n\n## 错误响应\n\n### 400 Bad Request\n\n```json\n{\n  \"error\": {\n    \"type\": \"invalid_request_error\",\n    \"message\": \"messages is required\"\n  }\n}\n```\n\n### 401 Unauthorized\n\n```json\n{\n  \"error\": {\n    \"type\": \"authentication_error\",\n    \"message\": \"Invalid API key\"\n  }\n}\n```\n\n### 500 Internal Server Error\n\n```json\n{\n  \"error\": {\n    \"type\": \"api_error\",\n    \"message\": \"Failed to connect to provider\"\n  }\n}\n```\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/server/api/overview.md",
    "content": "# API 概览\n\nClaude Code Router Server 提供了完整的 HTTP API，支持：\n\n- **消息 API**：兼容 Anthropic Claude API 的消息接口\n- **配置 API**：读取和更新服务器配置\n- **日志 API**：查看和管理服务日志\n- **工具 API**：计算 Token 数量\n\n## 基础信息\n\n**Base URL**: `http://localhost:3456`\n\n**认证方式**: API Key（通过 `x-api-key` 请求头）\n\n```bash\ncurl -H \"x-api-key: your-api-key\" http://localhost:3456/api/config\n```\n\n## API 端点列表\n\n### 消息相关\n\n| 端点 | 方法 | 描述 |\n|------|------|------|\n| `/v1/messages` | POST | 发送消息（兼容 Anthropic API） |\n| `/v1/messages/count_tokens` | POST | 计算消息的 Token 数量 |\n\n### 配置管理\n\n| 端点 | 方法 | 描述 |\n|------|------|------|\n| `/api/config` | GET | 获取当前配置 |\n| `/api/config` | POST | 更新配置 |\n| `/api/transformers` | GET | 获取可用的转换器列表 |\n\n### 日志管理\n\n| 端点 | 方法 | 描述 |\n|------|------|------|\n| `/api/logs/files` | GET | 获取日志文件列表 |\n| `/api/logs` | GET | 获取日志内容 |\n| `/api/logs` | DELETE | 清除日志 |\n\n### 服务管理\n\n| 端点 | 方法 | 描述 |\n|------|------|------|\n| `/api/restart` | POST | 重启服务 |\n| `/ui` | GET | Web 管理界面 |\n| `/ui/` | GET | Web 管理界面（重定向） |\n\n## 认证\n\n### API Key 认证\n\n在请求头中添加 API Key：\n\n```bash\ncurl -X POST http://localhost:3456/v1/messages \\\n  -H \"x-api-key: your-api-key\" \\\n  -H \"content-type: application/json\" \\\n  -d '...'\n```\n\n## 流式响应\n\n消息 API 支持流式响应（Server-Sent Events）：\n\n```bash\ncurl -X POST http://localhost:3456/v1/messages \\\n  -H \"x-api-key: your-api-key\" \\\n  -H \"content-type: application/json\" \\\n  -d '{\"stream\": true, ...}'\n```\n\n流式响应格式：\n\n```\nevent: message_start\ndata: {\"type\":\"message_start\",\"message\":{...}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"delta\":{\"type\":\"text_delta\",\"text\":\"Hello\"}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n```\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/server/config/basic.md",
    "content": "---\ntitle: 基础配置\nsidebar_position: 1\n---\n\n# 基础配置\n\n学习如何配置 Claude Code Router 以满足您的需求。\n\n## 配置文件位置\n\n配置文件位于：\n\n```\n~/.claude-code-router/config.json\n```\n\n## 配置结构\n\n### Providers（提供商）\n\n配置 LLM 提供商以将请求路由到：\n\n```json\n{\n  \"Providers\": [\n    {\n      \"name\": \"deepseek\",\n      \"api_base_url\": \"https://api.deepseek.com/chat/completions\",\n      \"api_key\": \"your-api-key\",\n      \"models\": [\"deepseek-chat\", \"deepseek-coder\"]\n    },\n    {\n      \"name\": \"groq\",\n      \"api_base_url\": \"https://api.groq.com/openai/v1/chat/completions\",\n      \"api_key\": \"your-groq-api-key\",\n      \"models\": [\"llama-3.3-70b-versatile\"]\n    }\n  ]\n}\n```\n\n### Router（路由器）\n\n配置默认使用的模型：\n\n```json\n{\n  \"Router\": {\n    \"default\": \"deepseek,deepseek-chat\"\n  }\n}\n```\n\n格式：`{provider-name},{model-name}`\n\n### Transformers（转换器）\n\n对请求/响应应用转换：\n\n```json\n{\n  \"transformers\": [\n    {\n      \"path\": \"/path/to/custom-transformer.js\",\n      \"options\": {\n        \"key\": \"value\"\n      }\n    }\n  ]\n}\n```\n\n### 环境变量\n\n在配置中使用环境变量：\n\n```json\n{\n  \"Providers\": [\n    {\n      \"name\": \"deepseek\",\n      \"api_base_url\": \"https://api.deepseek.com/chat/completions\",\n      \"api_key\": \"$DEEPSEEK_API_KEY\"\n    }\n  ]\n}\n```\n\n同时支持 `$VAR_NAME` 和 `${VAR_NAME}` 语法。\n\n## 完整示例\n\n```json\n{\n  \"PORT\": 8080,\n  \"APIKEY\": \"your-secret-key\",\n  \"PROXY_URL\": \"http://127.0.0.1:7890\",\n  \"LOG\": true,\n  \"LOG_LEVEL\": \"debug\",\n  \"API_TIMEOUT_MS\": 600000,\n  \"Providers\": [\n    {\n      \"name\": \"deepseek\",\n      \"api_base_url\": \"https://api.deepseek.com/chat/completions\",\n      \"api_key\": \"$DEEPSEEK_API_KEY\",\n      \"models\": [\"deepseek-chat\", \"deepseek-coder\"],\n      \"transformer\": {\n        \"use\": [\"deepseek\"]\n      }\n    },\n    {\n      \"name\": \"groq\",\n      \"api_base_url\": \"https://api.groq.com/openai/v1/chat/completions\",\n      \"api_key\": \"$GROQ_API_KEY\",\n      \"models\": [\"llama-3.3-70b-versatile\"]\n    }\n  ],\n  \"Router\": {\n    \"default\": \"deepseek,deepseek-chat\",\n    \"longContextThreshold\": 100000,\n    \"background\": \"groq,llama-3.3-70b-versatile\"\n  }\n}\n```\n\n## 编辑配置\n\n使用 CLI 编辑配置：\n\n```bash\nccr config edit\n```\n\n这将在您的默认编辑器中打开配置文件。\n\n## 重新加载配置\n\n编辑配置后，重启路由器：\n\n```bash\nccr restart\n```\n\n## 配置选项说明\n\n- **PORT**: 服务器端口号（默认：3456）\n- **APIKEY**: API 密钥，用于身份验证\n- **HOST**: 服务器监听地址（默认：127.0.0.1，如果配置了 Providers 且没有设置 APIKEY，则强制为 127.0.0.1）\n- **PROXY_URL**: 代理服务器地址\n- **LOG**: 是否启用日志（默认：true）\n- **LOG_LEVEL**: 日志级别（fatal/error/warn/info/debug/trace）\n- **API_TIMEOUT_MS**: API 请求超时时间（毫秒）\n- **NON_INTERACTIVE_MODE**: 非交互模式（用于 CI/CD 环境）\n\n## 下一步\n\n- [提供商配置](/zh/docs/config/providers) - 详细的提供商配置\n- [路由配置](/zh/docs/config/routing) - 配置路由规则\n- [转换器](/zh/docs/config/transformers) - 应用转换\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/server/config/providers.md",
    "content": "---\ntitle: 提供商配置\nsidebar_position: 2\n---\n\n# 提供商配置\n\n配置 LLM 提供商的详细指南。\n\n## 支持的提供商\n\n### DeepSeek\n\n```json\n{\n  \"name\": \"deepseek\",\n  \"api_base_url\": \"https://api.deepseek.com/chat/completions\",\n  \"api_key\": \"your-api-key\",\n  \"models\": [\"deepseek-chat\", \"deepseek-coder\", \"deepseek-reasoner\"],\n  \"transformer\": {\n    \"use\": [\"deepseek\"]\n  }\n}\n```\n\n### Groq\n\n```json\n{\n  \"name\": \"groq\",\n  \"api_base_url\": \"https://api.groq.com/openai/v1/chat/completions\",\n  \"api_key\": \"your-api-key\",\n  \"models\": [\"llama-3.3-70b-versatile\"]\n}\n```\n\n### Gemini\n\n```json\n{\n  \"name\": \"gemini\",\n  \"api_base_url\": \"https://generativelanguage.googleapis.com/v1beta/models/\",\n  \"api_key\": \"your-api-key\",\n  \"models\": [\"gemini-2.5-flash\", \"gemini-2.5-pro\"],\n  \"transformer\": {\n    \"use\": [\"gemini\"]\n  }\n}\n```\n\n### OpenRouter\n\n```json\n{\n  \"name\": \"openrouter\",\n  \"api_base_url\": \"https://openrouter.ai/api/v1/chat/completions\",\n  \"api_key\": \"your-api-key\",\n  \"models\": [\n    \"anthropic/claude-3.5-sonnet\",\n    \"google/gemini-2.5-pro-preview\"\n  ],\n  \"transformer\": {\n    \"use\": [\"openrouter\"]\n  }\n}\n```\n\n### Ollama（本地模型）\n\n```json\n{\n  \"name\": \"ollama\",\n  \"api_base_url\": \"http://localhost:11434/v1/chat/completions\",\n  \"api_key\": \"ollama\",\n  \"models\": [\"qwen2.5-coder:latest\"]\n}\n```\n\n### 火山引擎\n\n```json\n{\n  \"name\": \"volcengine\",\n  \"api_base_url\": \"https://ark.cn-beijing.volces.com/api/v3/chat/completions\",\n  \"api_key\": \"your-api-key\",\n  \"models\": [\"deepseek-v3-250324\", \"deepseek-r1-250528\"],\n  \"transformer\": {\n    \"use\": [\"deepseek\"]\n  }\n}\n```\n\n### ModelScope\n\n```json\n{\n  \"name\": \"modelscope\",\n  \"api_base_url\": \"https://api-inference.modelscope.cn/v1/chat/completions\",\n  \"api_key\": \"\",\n  \"models\": [\n    \"Qwen/Qwen3-Coder-480B-A35B-Instruct\",\n    \"Qwen/Qwen3-235B-A22B-Thinking-2507\"\n  ],\n  \"transformer\": {\n    \"use\": [\n      [\"maxtoken\", { \"max_tokens\": 65536 }],\n      \"enhancetool\"\n    ],\n    \"Qwen/Qwen3-235B-A22B-Thinking-2507\": {\n      \"use\": [\"reasoning\"]\n    }\n  }\n}\n```\n\n### DashScope（阿里云）\n\n```json\n{\n  \"name\": \"dashscope\",\n  \"api_base_url\": \"https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions\",\n  \"api_key\": \"your-api-key\",\n  \"models\": [\"qwen3-coder-plus\"],\n  \"transformer\": {\n    \"use\": [\n      [\"maxtoken\", { \"max_tokens\": 65536 }],\n      \"enhancetool\"\n    ]\n  }\n}\n```\n\n## 提供商配置选项\n\n| 字段 | 类型 | 必填 | 说明 |\n|------|------|------|------|\n| `name` | string | 是 | 提供商的唯一标识符 |\n| `api_base_url` | string | 是 | API 基础 URL |\n| `api_key` | string | 是 | API 认证密钥 |\n| `models` | string[] | 否 | 可用模型列表 |\n| `transformer` | object | 否 | 应用的转换器配置 |\n\n## 模型选择\n\n在路由中选择模型时，使用以下格式：\n\n```\n{provider-name},{model-name}\n```\n\n例如：\n\n```\ndeepseek,deepseek-chat\n```\n\n## 使用环境变量\n\n您可以在配置中使用环境变量来保护 API 密钥：\n\n```json\n{\n  \"Providers\": [\n    {\n      \"name\": \"deepseek\",\n      \"api_base_url\": \"https://api.deepseek.com/chat/completions\",\n      \"api_key\": \"$DEEPSEEK_API_KEY\",\n      \"models\": [\"deepseek-chat\"]\n    }\n  ]\n}\n```\n\n支持 `$VAR_NAME` 和 `${VAR_NAME}` 两种语法。\n\n## 转换器配置\n\n转换器用于适配不同提供商的 API 差异。您可以在提供商级别或模型级别配置转换器：\n\n### 提供商级别转换器\n\n应用于提供商的所有模型：\n\n```json\n{\n  \"name\": \"openrouter\",\n  \"transformer\": {\n    \"use\": [\"openrouter\"]\n  }\n}\n```\n\n### 模型级别转换器\n\n应用于特定模型：\n\n```json\n{\n  \"name\": \"deepseek\",\n  \"transformer\": {\n    \"use\": [\"deepseek\"],\n    \"deepseek-chat\": {\n      \"use\": [\"tooluse\"]\n    }\n  }\n}\n```\n\n## 下一步\n\n- [路由配置](/zh/docs/config/routing) - 配置请求如何路由\n- [转换器](/zh/docs/config/transformers) - 对请求应用转换\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/server/config/routing.md",
    "content": "---\ntitle: 路由配置\nsidebar_position: 3\n---\n\n# 路由配置\n\n配置如何将请求路由到不同的模型。\n\n## 默认路由\n\n为所有请求设置默认模型：\n\n```json\n{\n  \"Router\": {\n    \"default\": \"deepseek,deepseek-chat\"\n  }\n}\n```\n\n## 内置场景\n\n### 后台任务\n\n将后台任务路由到轻量级模型：\n\n```json\n{\n  \"Router\": {\n    \"background\": \"groq,llama-3.3-70b-versatile\"\n  }\n}\n```\n\n### 思考模式（计划模式）\n\n将思考密集型任务路由到更强大的模型：\n\n```json\n{\n  \"Router\": {\n    \"think\": \"deepseek,deepseek-reasoner\"\n  }\n}\n```\n\n### 长上下文\n\n路由长上下文请求：\n\n```json\n{\n  \"Router\": {\n    \"longContextThreshold\": 100000,\n    \"longContext\": \"gemini,gemini-2.5-pro\"\n  }\n}\n```\n\n### 网络搜索\n\n路由网络搜索任务：\n\n```json\n{\n  \"Router\": {\n    \"webSearch\": \"gemini,gemini-2.5-flash\"\n  }\n}\n```\n\n### 图像任务\n\n路由图像相关任务：\n\n```json\n{\n  \"Router\": {\n    \"image\": \"gemini,gemini-2.5-pro\"\n  }\n}\n```\n\n## 故障转移（Fallback）\n\n当请求失败时，可以配置备用模型列表。系统会按顺序尝试每个模型，直到请求成功：\n\n### 基本配置\n\n```json\n{\n  \"Router\": {\n    \"default\": \"deepseek,deepseek-chat\",\n    \"background\": \"ollama,qwen2.5-coder:latest\",\n    \"think\": \"deepseek,deepseek-reasoner\",\n    \"longContext\": \"openrouter,google/gemini-2.5-pro-preview\",\n    \"longContextThreshold\": 60000,\n    \"webSearch\": \"gemini,gemini-2.5-flash\"\n  },\n  \"fallback\": {\n    \"default\": [\n      \"aihubmix,Z/glm-4.5\",\n      \"openrouter,anthropic/claude-sonnet-4\"\n    ],\n    \"background\": [\n      \"ollama,qwen2.5-coder:latest\"\n    ],\n    \"think\": [\n      \"openrouter,anthropic/claude-3.7-sonnet:thinking\"\n    ],\n    \"longContext\": [\n      \"modelscope,Qwen/Qwen3-Coder-480B-A35B-Instruct\"\n    ],\n    \"webSearch\": [\n      \"openrouter,anthropic/claude-sonnet-4\"\n    ]\n  }\n}\n```\n\n### 工作原理\n\n1. **触发条件**：当某个路由场景的模型请求失败时（HTTP 错误响应）\n2. **自动切换**：系统自动检查该场景的 fallback 配置\n3. **顺序尝试**：按照列表顺序依次尝试每个备用模型\n4. **成功返回**：一旦某个模型成功响应，立即返回结果\n5. **全部失败**：如果所有备用模型都失败，返回原始错误\n\n### 配置说明\n\n- **格式**：每个备用模型格式为 `provider,model`\n- **验证**：备用模型必须在 `Providers` 配置中存在\n- **灵活性**：可以为不同场景配置不同的备用列表\n- **可选性**：如果某个场景不需要备用，可以不配置或使用空数组\n\n### 使用场景\n\n#### 场景一：主模型配额不足\n\n```json\n{\n  \"Router\": {\n    \"default\": \"openrouter,anthropic/claude-sonnet-4\"\n  },\n  \"fallback\": {\n    \"default\": [\n      \"deepseek,deepseek-chat\",\n      \"aihubmix,Z/glm-4.5\"\n    ]\n  }\n}\n```\n\n当主模型配额用完时，自动切换到备用模型。\n\n#### 场景二：服务稳定性保障\n\n```json\n{\n  \"Router\": {\n    \"background\": \"volcengine,deepseek-v3-250324\"\n  },\n  \"fallback\": {\n    \"background\": [\n      \"modelscope,Qwen/Qwen3-Coder-480B-A35B-Instruct\",\n      \"dashscope,qwen3-coder-plus\"\n    ]\n  }\n}\n```\n\n当主服务商出现故障时，自动切换到其他服务商。\n\n### 日志监控\n\n系统会记录详细的 fallback 过程：\n\n```\n[warn] Request failed for default, trying 2 fallback models\n[info] Trying fallback model: aihubmix,Z/glm-4.5\n[warn] Fallback model aihubmix,Z/glm-4.5 failed: API rate limit exceeded\n[info] Trying fallback model: openrouter,anthropic/claude-sonnet-4\n[info] Fallback model openrouter,anthropic/claude-sonnet-4 succeeded\n```\n\n### 注意事项\n\n1. **成本考虑**：备用模型可能产生不同的费用，请合理配置\n2. **性能差异**：不同模型的响应速度和质量可能有差异\n3. **配额管理**：确保备用模型有足够的配额\n4. **测试验证**：定期测试备用模型的可用性\n\n## 项目级路由\n\n在 `~/.claude/projects/<project-id>/claude-code-router.json` 中为每个项目配置路由：\n\n```json\n{\n  \"Router\": {\n    \"default\": \"groq,llama-3.3-70b-versatile\"\n  }\n}\n```\n\n项目级配置优先于全局配置。\n\n## 自定义路由器\n\n创建自定义 JavaScript 路由器函数：\n\n1. 创建路由器文件（例如 `custom-router.js`）：\n\n```javascript\nmodule.exports = async function(req, config) {\n  // 分析请求上下文\n  const userMessage = req.body.messages.find(m => m.role === 'user')?.content;\n\n  // 自定义路由逻辑\n  if (userMessage && userMessage.includes('解释代码')) {\n    return 'openrouter,anthropic/claude-3.5-sonnet';\n  }\n\n  // 返回 null 以使用默认路由\n  return null;\n};\n```\n\n2. 在 `config.json` 中设置 `CUSTOM_ROUTER_PATH`：\n\n```json\n{\n  \"CUSTOM_ROUTER_PATH\": \"/path/to/custom-router.js\"\n}\n```\n\n## Token 计数\n\n路由器使用 `tiktoken` (cl100k_base) 来估算请求 token 数量。这用于：\n\n- 确定请求是否超过 `longContextThreshold`\n- 基于 token 数量的自定义路由逻辑\n\n## 子代理路由\n\n使用特殊标签为子代理指定模型：\n\n```\n<CCR-SUBAGENT-MODEL>provider,model</CCR-SUBAGENT-MODEL>\n请帮我分析这段代码...\n```\n\n## 动态模型切换\n\n在 Claude Code 中使用 `/model` 命令动态切换模型：\n\n```\n/model provider_name,model_name\n```\n\n示例：`/model openrouter,anthropic/claude-3.5-sonnet`\n\n## 路由优先级\n\n1. 项目级配置\n2. 自定义路由器\n3. 内置场景路由\n4. 默认路由\n\n## 下一步\n\n- [转换器](/zh/docs/config/transformers) - 对请求应用转换\n- [自定义路由器](/zh/docs/advanced/custom-router) - 高级自定义路由\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/server/config/transformers.md",
    "content": "---\ntitle: 转换器\nsidebar_position: 4\n---\n\n# 转换器\n\n转换器是适配不同 LLM 提供商 API 差异的核心机制。它们在不同格式之间转换请求和响应，处理认证，并管理提供商特定的功能。\n\n## 理解转换器\n\n### 什么是转换器？\n\n转换器是一个插件，它可以：\n- **转换请求**：从统一格式转换为提供商特定格式\n- **转换响应**：从提供商格式转换回统一格式\n- **处理认证**：为提供商 API 处理认证\n- **修改请求**：添加或调整参数\n\n### 数据流\n\n```\n┌─────────────────┐\n│ 传入请求        │ (来自 Claude Code 的 Anthropic 格式)\n└────────┬────────┘\n         │\n         ▼\n┌─────────────────────────────────┐\n│  transformRequestOut            │ ← 将传入请求解析为统一格式\n└────────┬────────────────────────┘\n         │\n         ▼\n┌─────────────────────────────────┐\n│  UnifiedChatRequest             │\n└────────┬────────────────────────┘\n         │\n         ▼\n┌─────────────────────────────────┐\n│  transformRequestIn (可选)      │ ← 在发送前修改统一请求\n└────────┬────────────────────────┘\n         │\n         ▼\n┌─────────────────────────────────┐\n│  提供商 API 调用                │\n└────────┬────────────────────────┘\n         │\n         ▼\n┌─────────────────────────────────┐\n│  transformResponseIn (可选)     │ ← 将提供商响应转换为统一格式\n└────────┬────────────────────────┘\n         │\n         ▼\n┌─────────────────────────────────┐\n│  transformResponseOut (可选)    │ ← 将统一响应转换为 Anthropic 格式\n└────────┬────────────────────────┘\n         │\n         ▼\n┌─────────────────┐\n│ 传出响应        │ (返回给 Claude Code 的 Anthropic 格式)\n└─────────────────┘\n```\n\n### 转换器接口\n\n所有转换器都实现以下接口：\n\n```typescript\ninterface Transformer {\n  // 将统一请求转换为提供商特定格式\n  transformRequestIn?: (\n    request: UnifiedChatRequest,\n    provider: LLMProvider,\n    context: TransformerContext\n  ) => Promise<Record<string, any>>;\n\n  // 将提供商请求转换为统一格式\n  transformRequestOut?: (\n    request: any,\n    context: TransformerContext\n  ) => Promise<UnifiedChatRequest>;\n\n  // 将提供商响应转换为统一格式\n  transformResponseIn?: (\n    response: Response,\n    context?: TransformerContext\n  ) => Promise<Response>;\n\n  // 将统一响应转换为提供商格式\n  transformResponseOut?: (\n    response: Response,\n    context: TransformerContext\n  ) => Promise<Response>;\n\n  // 自定义端点路径（可选）\n  endPoint?: string;\n\n  // 转换器名称（用于自定义转换器）\n  name?: string;\n\n  // 自定义认证处理器（可选）\n  auth?: (\n    request: any,\n    provider: LLMProvider,\n    context: TransformerContext\n  ) => Promise<any>;\n\n  // Logger 实例（自动注入）\n  logger?: any;\n}\n```\n\n### 关键类型\n\n#### UnifiedChatRequest\n\n```typescript\ninterface UnifiedChatRequest {\n  messages: UnifiedMessage[];\n  model: string;\n  max_tokens?: number;\n  temperature?: number;\n  stream?: boolean;\n  tools?: UnifiedTool[];\n  tool_choice?: any;\n  reasoning?: {\n    effort?: ThinkLevel;  // \"none\" | \"low\" | \"medium\" | \"high\"\n    max_tokens?: number;\n    enabled?: boolean;\n  };\n}\n```\n\n#### UnifiedMessage\n\n```typescript\ninterface UnifiedMessage {\n  role: \"user\" | \"assistant\" | \"system\" | \"tool\";\n  content: string | null | MessageContent[];\n  tool_calls?: Array<{\n    id: string;\n    type: \"function\";\n    function: {\n      name: string;\n      arguments: string;\n    };\n  }>;\n  tool_call_id?: string;\n  thinking?: {\n    content: string;\n    signature?: string;\n  };\n}\n```\n\n## 内置转换器\n\n### anthropic\n\n将请求转换为兼容 Anthropic 风格的 API：\n\n```json\n{\n  \"transformers\": [\n    {\n      \"name\": \"anthropic\",\n      \"providers\": [\"deepseek\", \"groq\"]\n    }\n  ]\n}\n```\n\n**功能：**\n- 在 Anthropic 消息格式和 OpenAI 格式之间转换\n- 处理工具调用和工具结果\n- 支持思考/推理内容块\n- 管理流式响应\n\n### deepseek\n\n专门用于 DeepSeek API 的转换器：\n\n```json\n{\n  \"transformers\": [\n    {\n      \"name\": \"deepseek\",\n      \"providers\": [\"deepseek\"]\n    }\n  ]\n}\n```\n\n**功能：**\n- DeepSeek 特定的推理格式\n- 处理响应中的 `reasoning_content`\n- 支持思考预算令牌\n\n### gemini\n\n用于 Google Gemini API 的转换器：\n\n```json\n{\n  \"transformers\": [\n    {\n      \"name\": \"gemini\",\n      \"providers\": [\"gemini\"]\n    }\n  ]\n}\n```\n\n### maxtoken\n\n限制请求中的 max_tokens：\n\n```json\n{\n  \"transformers\": [\n    {\n      \"name\": \"maxtoken\",\n      \"options\": {\n        \"max_tokens\": 8192\n      },\n      \"models\": [\"deepseek,deepseek-chat\"]\n    }\n  ]\n}\n```\n\n### customparams\n\n向请求中注入自定义参数：\n\n```json\n{\n  \"transformers\": [\n    {\n      \"name\": \"customparams\",\n      \"options\": {\n        \"include_reasoning\": true,\n        \"custom_header\": \"value\"\n      }\n    }\n  ]\n}\n```\n\n## 创建自定义转换器\n\n### 简单转换器：修改请求\n\n最简单的转换器只修改发送到提供商之前的请求。\n\n**示例：为所有请求添加自定义头**\n\n```javascript\n// custom-header-transformer.js\nmodule.exports = class CustomHeaderTransformer {\n  name = 'custom-header';\n\n  constructor(options) {\n    this.headerName = options?.headerName || 'X-Custom-Header';\n    this.headerValue = options?.headerValue || 'default-value';\n  }\n\n  async transformRequestIn(request, provider, context) {\n    // 添加自定义头（将被 auth 方法使用）\n    request._customHeaders = {\n      [this.headerName]: this.headerValue\n    };\n    return request;\n  }\n\n  async auth(request, provider) {\n    const headers = {\n      'authorization': `Bearer ${provider.apiKey}`,\n      ...request._customHeaders\n    };\n    return {\n      body: request,\n      config: { headers }\n    };\n  }\n};\n```\n\n**在配置中使用：**\n\n```json\n{\n  \"transformers\": [\n    {\n      \"name\": \"custom-header\",\n      \"path\": \"/path/to/custom-header-transformer.js\",\n      \"options\": {\n        \"headerName\": \"X-My-Header\",\n        \"headerValue\": \"my-value\"\n      }\n    }\n  ]\n}\n```\n\n### 中级转换器：请求/响应转换\n\n此示例展示如何在不同 API 格式之间转换。\n\n**示例：Mock API 格式转换器**\n\n```javascript\n// mockapi-transformer.js\nmodule.exports = class MockAPITransformer {\n  name = 'mockapi';\n  endPoint = '/v1/chat';  // 自定义端点\n\n  // 从 MockAPI 格式转换为统一格式\n  async transformRequestOut(request, context) {\n    const messages = request.conversation.map(msg => ({\n      role: msg.sender,\n      content: msg.text\n    }));\n\n    return {\n      messages,\n      model: request.model_id,\n      max_tokens: request.max_tokens,\n      temperature: request.temp\n    };\n  }\n\n  // 从统一格式转换为 MockAPI 格式\n  async transformRequestIn(request, provider, context) {\n    return {\n      model_id: request.model,\n      conversation: request.messages.map(msg => ({\n        sender: msg.role,\n        text: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content)\n      })),\n      max_tokens: request.max_tokens || 4096,\n      temp: request.temperature || 0.7\n    };\n  }\n\n  // 将 MockAPI 响应转换为统一格式\n  async transformResponseIn(response, context) {\n    const data = await response.json();\n\n    const unifiedResponse = {\n      id: data.request_id,\n      object: 'chat.completion',\n      created: data.timestamp,\n      model: data.model,\n      choices: [{\n        index: 0,\n        message: {\n          role: 'assistant',\n          content: data.reply.text\n        },\n        finish_reason: data.stop_reason\n      }],\n      usage: {\n        prompt_tokens: data.tokens.input,\n        completion_tokens: data.tokens.output,\n        total_tokens: data.tokens.input + data.tokens.output\n      }\n    };\n\n    return new Response(JSON.stringify(unifiedResponse), {\n      status: response.status,\n      statusText: response.statusText,\n      headers: { 'Content-Type': 'application/json' }\n    });\n  }\n};\n```\n\n### 高级转换器：流式响应处理\n\n此示例展示如何处理流式响应。\n\n**示例：向流式响应添加自定义元数据**\n\n```javascript\n// streaming-metadata-transformer.js\nmodule.exports = class StreamingMetadataTransformer {\n  name = 'streaming-metadata';\n\n  constructor(options) {\n    this.metadata = options?.metadata || {};\n    this.logger = null;  // 将由系统注入\n  }\n\n  async transformResponseOut(response, context) {\n    const contentType = response.headers.get('Content-Type');\n\n    // 处理流式响应\n    if (contentType?.includes('text/event-stream')) {\n      return this.transformStream(response, context);\n    }\n\n    // 处理非流式响应\n    return response;\n  }\n\n  async transformStream(response, context) {\n    const decoder = new TextDecoder();\n    const encoder = new TextEncoder();\n\n    const transformedStream = new ReadableStream({\n      start: async (controller) => {\n        const reader = response.body.getReader();\n        let buffer = '';\n\n        try {\n          while (true) {\n            const { done, value } = await reader.read();\n            if (done) break;\n\n            buffer += decoder.decode(value, { stream: true });\n            const lines = buffer.split('\\n');\n            buffer = lines.pop() || '';\n\n            for (const line of lines) {\n              if (!line.trim() || !line.startsWith('data: ')) {\n                controller.enqueue(encoder.encode(line + '\\n'));\n                continue;\n              }\n\n              const data = line.slice(6).trim();\n              if (data === '[DONE]') {\n                controller.enqueue(encoder.encode(line + '\\n'));\n                continue;\n              }\n\n              try {\n                const chunk = JSON.parse(data);\n\n                // 添加自定义元数据\n                if (chunk.choices && chunk.choices[0]) {\n                  chunk.choices[0].metadata = this.metadata;\n                }\n\n                // 记录日志以便调试\n                this.logger?.debug({\n                  chunk,\n                  context: context.req.id\n                }, '转换流式数据块');\n\n                const modifiedLine = `data: ${JSON.stringify(chunk)}\\n\\n`;\n                controller.enqueue(encoder.encode(modifiedLine));\n              } catch (parseError) {\n                // 如果解析失败，透传原始行\n                controller.enqueue(encoder.encode(line + '\\n'));\n              }\n            }\n          }\n        } catch (error) {\n          this.logger?.error({ error }, '流式转换错误');\n          controller.error(error);\n        } finally {\n          controller.close();\n          reader.releaseLock();\n        }\n      }\n    });\n\n    return new Response(transformedStream, {\n      status: response.status,\n      statusText: response.statusText,\n      headers: {\n        'Content-Type': 'text/event-stream',\n        'Cache-Control': 'no-cache',\n        'Connection': 'keep-alive'\n      }\n    });\n  }\n};\n```\n\n### 真实示例：推理内容转换器\n\n这是基于代码库中实际的 `reasoning.transformer.ts`。\n\n```typescript\n// reasoning-transformer.ts\nimport { Transformer, TransformerOptions } from \"@musistudio/llms\";\n\nexport class ReasoningTransformer implements Transformer {\n  static TransformerName = \"reasoning\";\n  enable: boolean;\n\n  constructor(private readonly options?: TransformerOptions) {\n    this.enable = this.options?.enable ?? true;\n  }\n\n  // 转换请求以添加推理参数\n  async transformRequestIn(request: UnifiedChatRequest): Promise<UnifiedChatRequest> {\n    if (!this.enable) {\n      request.thinking = {\n        type: \"disabled\",\n        budget_tokens: -1,\n      };\n      request.enable_thinking = false;\n      return request;\n    }\n\n    if (request.reasoning) {\n      request.thinking = {\n        type: \"enabled\",\n        budget_tokens: request.reasoning.max_tokens,\n      };\n      request.enable_thinking = true;\n    }\n    return request;\n  }\n\n  // 转换响应以将 reasoning_content 转换为 thinking 格式\n  async transformResponseOut(response: Response): Promise<Response> {\n    if (!this.enable) return response;\n\n    const contentType = response.headers.get(\"Content-Type\");\n\n    // 处理非流式响应\n    if (contentType?.includes(\"application/json\")) {\n      const jsonResponse = await response.json();\n      if (jsonResponse.choices[0]?.message.reasoning_content) {\n        jsonResponse.thinking = {\n          content: jsonResponse.choices[0].message.reasoning_content\n        };\n      }\n      return new Response(JSON.stringify(jsonResponse), {\n        status: response.status,\n        statusText: response.statusText,\n        headers: response.headers,\n      });\n    }\n\n    // 处理流式响应\n    if (contentType?.includes(\"stream\")) {\n      // [流式转换代码在这里]\n      // 参见代码库中的完整实现\n    }\n\n    return response;\n  }\n}\n```\n\n## 转换器注册\n\n### 方法 1：静态名称（基于类）\n\n在 TypeScript/ES6 中创建转换器时使用：\n\n```typescript\nexport class MyTransformer implements Transformer {\n  static TransformerName = \"my-transformer\";\n\n  async transformRequestIn(request: UnifiedChatRequest): Promise<any> {\n    // 转换逻辑\n    return request;\n  }\n}\n```\n\n### 方法 2：实例名称（基于实例）\n\n用于 JavaScript 转换器：\n\n```javascript\nmodule.exports = class MyTransformer {\n  constructor(options) {\n    this.name = 'my-transformer';\n    this.options = options;\n  }\n\n  async transformRequestIn(request, provider, context) {\n    // 转换逻辑\n    return request;\n  }\n};\n```\n\n## 应用转换器\n\n### 全局应用（提供商级别）\n\n为提供商的所有请求应用：\n\n```json\n{\n  \"Providers\": [\n    {\n      \"NAME\": \"deepseek\",\n      \"HOST\": \"https://api.deepseek.com\",\n      \"APIKEY\": \"your-api-key\",\n      \"transformers\": [\"anthropic\"]\n    }\n  ]\n}\n```\n\n### 模型特定应用\n\n仅应用于特定模型：\n\n```json\n{\n  \"transformers\": [\n    {\n      \"name\": \"maxtoken\",\n      \"options\": {\n        \"max_tokens\": 8192\n      },\n      \"models\": [\"deepseek,deepseek-chat\"]\n    }\n  ]\n}\n```\n\n注意：模型格式为 `provider,model`（例如 `deepseek,deepseek-chat`）。\n\n### 全局转换器（所有提供商）\n\n将转换器应用于所有提供商：\n\n```json\n{\n  \"transformers\": [\n    {\n      \"name\": \"custom-logger\",\n      \"path\": \"/path/to/custom-logger.js\"\n    }\n  ]\n}\n```\n\n### 传递选项\n\n某些转换器接受配置选项：\n\n```json\n{\n  \"transformers\": [\n    {\n      \"name\": \"maxtoken\",\n      \"options\": {\n        \"max_tokens\": 8192\n      }\n    },\n    {\n      \"name\": \"customparams\",\n      \"options\": {\n        \"custom_param_1\": \"value1\",\n        \"custom_param_2\": 42\n      }\n    }\n  ]\n}\n```\n\n## 最佳实践\n\n### 1. 不可变性\n\n始终创建新对象而不是修改现有对象：\n\n```javascript\n// 不好的做法\nasync transformRequestIn(request) {\n  request.max_tokens = 4096;\n  return request;\n}\n\n// 好的做法\nasync transformRequestIn(request) {\n  return {\n    ...request,\n    max_tokens: request.max_tokens || 4096\n  };\n}\n```\n\n### 2. 错误处理\n\n始终优雅地处理错误：\n\n```javascript\nasync transformResponseIn(response) {\n  try {\n    const data = await response.json();\n    // 处理数据\n    return new Response(JSON.stringify(processedData), {\n      status: response.status,\n      headers: response.headers\n    });\n  } catch (error) {\n    this.logger?.error({ error }, '转换失败');\n    // 如果转换失败，返回原始响应\n    return response;\n  }\n}\n```\n\n### 3. 日志记录\n\n使用注入的 logger 进行调试：\n\n```javascript\nasync transformRequestIn(request, provider, context) {\n  this.logger?.debug({\n    model: request.model,\n    provider: provider.name\n  }, '转换请求');\n\n  // 转换逻辑\n\n  return modifiedRequest;\n}\n```\n\n### 4. 流处理\n\n处理流式响应时，始终：\n- 使用缓冲区处理不完整的数据块\n- 正确释放 reader 锁\n- 处理流中的错误\n- 完成时关闭 controller\n\n```javascript\nconst transformedStream = new ReadableStream({\n  start: async (controller) => {\n    const reader = response.body.getReader();\n    let buffer = '';\n\n    try {\n      while (true) {\n        const { done, value } = await reader.read();\n        if (done) break;\n\n        // 处理流...\n      }\n    } catch (error) {\n      controller.error(error);\n    } finally {\n      controller.close();\n      reader.releaseLock();\n    }\n  }\n});\n```\n\n### 5. 上下文使用\n\n`context` 参数包含有用信息：\n\n```javascript\nasync transformRequestIn(request, provider, context) {\n  // 访问请求 ID\n  const requestId = context.req.id;\n\n  // 访问原始请求\n  const originalRequest = context.req.original;\n\n  // 转换逻辑\n}\n```\n\n## 测试转换器\n\n### 手动测试\n\n1. 将转换器添加到配置\n2. 启动服务器：`ccr restart`\n3. 检查日志：`tail -f ~/.claude-code-router/logs/ccr-*.log`\n4. 发出测试请求\n5. 验证输出\n\n### 调试技巧\n\n- 添加日志记录以跟踪转换步骤\n- 使用流式和非流式请求进行测试\n- 使用无效输入验证错误处理\n- 检查错误时是否返回原始响应\n\n## 下一步\n\n- [高级主题](/docs/server/advanced/custom-router) - 高级路由自定义\n- [Agents](/docs/server/advanced/agents) - 使用 agents 扩展\n- [核心包](/docs/server/intro) - 了解 @musistudio/llms\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/server/deployment.md",
    "content": "# Server 部署\n\nClaude Code Router Server 支持多种部署方式，从本地开发到生产环境。\n\n## Docker 部署（推荐）\n\n### 使用 Docker Hub 镜像\n\n```bash\ndocker run -d \\\n  --name claude-code-router \\\n  -p 3456:3456 \\\n  -v ~/.claude-code-router:/app/.claude-code-router \\\n  musistudio/claude-code-router:latest\n```\n\n### 使用 Docker Compose\n\n创建 `docker-compose.yml`：\n\n```yaml\nversion: '3.8'\nservices:\n  claude-code-router:\n    image: musistudio/claude-code-router:latest\n    container_name: claude-code-router\n    ports:\n      - \"3456:3456\"\n    volumes:\n      - ./config:/app/.claude-code-router\n    environment:\n      - LOG_LEVEL=info\n      - HOST=0.0.0.0\n      - PORT=3456\n    restart: unless-stopped\n```\n\n启动服务：\n\n```bash\ndocker-compose up -d\n```\n\n### 自定义构建\n\n从源码构建 Docker 镜像：\n\n```bash\ngit clone https://github.com/musistudio/claude-code-router.git\ncd claude-code-router\ndocker build -t claude-code-router:latest .\n```\n\n## 配置文件挂载\n\n将配置文件挂载到容器中：\n\n```bash\ndocker run -d \\\n  --name claude-code-router \\\n  -p 3456:3456 \\\n  -v $(pwd)/config.json:/app/.claude-code-router/config.json \\\n  musistudio/claude-code-router:latest\n```\n\n配置文件示例：\n\n```json5\n{\n  // 服务器配置\n  \"HOST\": \"0.0.0.0\",\n  \"PORT\": 3456,\n  \"APIKEY\": \"your-api-key-here\",\n\n  // 日志配置\n  \"LOG\": true,\n  \"LOG_LEVEL\": \"info\",\n\n  // LLM 提供商配置\n  \"Providers\": [\n    {\n      \"name\": \"openai\",\n      \"baseUrl\": \"https://api.openai.com/v1\",\n      \"apiKey\": \"$OPENAI_API_KEY\",\n      \"models\": [\"gpt-4\", \"gpt-3.5-turbo\"]\n    }\n  ],\n\n  // 路由配置\n  \"Router\": {\n    \"default\": \"openai,gpt-4\"\n  }\n}\n```\n\n## 环境变量\n\n支持通过环境变量覆盖配置：\n\n| 变量名 | 说明 | 默认值 |\n|--------|------|--------|\n| `HOST` | 监听地址 | `127.0.0.1` |\n| `PORT` | 监听端口 | `3456` |\n| `APIKEY` | API 密钥 | - |\n| `LOG_LEVEL` | 日志级别 | `debug` |\n| `LOG` | 是否启用日志 | `true` |\n\n## 生产环境建议\n\n### 1. 使用反向代理\n\n使用 Nginx 作为反向代理：\n\n```nginx\nserver {\n    listen 80;\n    server_name your-domain.com;\n\n    location / {\n        proxy_pass http://localhost:3456;\n        proxy_http_version 1.1;\n        proxy_set_header Upgrade $http_upgrade;\n        proxy_set_header Connection 'upgrade';\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_cache_bypass $http_upgrade;\n    }\n}\n```\n\n### 2. 配置 HTTPS\n\n使用 Let's Encrypt 获取免费证书：\n\n```bash\nsudo certbot --nginx -d your-domain.com\n```\n\n### 3. 日志管理\n\n配置日志轮转和持久化：\n\n```yaml\nversion: '3.8'\nservices:\n  claude-code-router:\n    image: musistudio/claude-code-router:latest\n    volumes:\n      - ./logs:/app/.claude-code-router/logs\n    environment:\n      - LOG_LEVEL=warn\n```\n\n### 4. 健康检查\n\n配置 Docker 健康检查：\n\n```yaml\nhealthcheck:\n  test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:3456/api/config\"]\n  interval: 30s\n  timeout: 10s\n  retries: 3\n```\n\n## 访问 Web UI\n\n部署完成后，访问 Web UI：\n\n```\nhttp://localhost:3456/ui/\n```\n\n通过 Web UI 可以：\n- 查看和管理配置\n- 监控日志\n- 查看服务状态\n\n## 二次开发\n\n如果需要基于 CCR Server 进行二次开发，请查看 [API 参考](/docs/category/api)。\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/server/intro.md",
    "content": "# Server 简介\n\nClaude Code Router Server 是一个核心服务组件，负责将 Claude Code 的 API 请求路由到不同的 LLM 提供商。它提供了完整的 HTTP API，支持：\n\n- **API 请求路由**：将 Anthropic 格式的请求转换为各种提供商的 API 格式\n- **认证与授权**：支持 API Key 认证\n- **配置管理**：动态配置提供商、路由规则和转换器\n- **Web UI**：内置管理界面\n- **日志系统**：完整的请求日志记录\n\n## 架构概述\n\n```\n┌─────────────┐     ┌─────────────────────────────┐     ┌──────────────┐\n│ Claude Code │────▶│ CCR Server                  │────▶│ LLM Provider │\n│   Client    │     │  ┌─────────────────────┐    │     │  (OpenAI/    │\n└─────────────┘     │  │ @musistudio/llms    │    │     │   Gemini/etc)│\n                    │  │ (核心包)             │    │     └──────────────┘\n                    │  │ - 请求转换           │    │\n                    │  │ - 响应转换           │    │\n                    │  │ - 认证处理           │    │\n                    │  └─────────────────────┘    │\n                    │                             │\n                    │  - 路由逻辑                 │\n                    │  - Agent 系统               │\n                    │  - 配置管理                 │\n                    └─────────────────────────────┘\n                           │\n                           ├─ Web UI\n                           ├─ Config API\n                           └─ Logs API\n```\n\n## 核心包：@musistudio/llms\n\n服务器构建于 **@musistudio/llms** 之上，这是一个通用的 LLM API 转换库，提供了核心的请求/响应转换能力。\n\n### 什么是 @musistudio/llms？\n\n`@musistudio/llms` 是一个独立的 npm 包（`@musistudio/llms`），负责处理：\n\n- **API 格式转换**：在不同的 LLM 提供商 API 之间转换（Anthropic、OpenAI、Gemini 等）\n- **请求/响应转换**：将请求和响应转换为统一格式\n- **认证处理**：管理不同提供商的认证方法\n- **流式响应支持**：处理来自不同提供商的流式响应\n- **转换器系统**：提供可扩展的架构来添加新的提供商\n\n### 核心概念\n\n#### 1. 统一请求/响应格式\n\n核心包定义了统一格式（`UnifiedChatRequest`、`UnifiedChatResponse`），抽象了提供商特定的差异：\n\n```typescript\ninterface UnifiedChatRequest {\n  messages: UnifiedMessage[];\n  model: string;\n  max_tokens?: number;\n  temperature?: number;\n  stream?: boolean;\n  tools?: UnifiedTool[];\n  tool_choice?: any;\n  reasoning?: {\n    effort?: ThinkLevel;\n    max_tokens?: number;\n    enabled?: boolean;\n  };\n}\n```\n\n#### 2. 转换器接口\n\n所有转换器都实现一个通用接口：\n\n```typescript\ninterface Transformer {\n  transformRequestIn?: (request: UnifiedChatRequest, provider: LLMProvider, context: TransformerContext) => Promise<any>;\n  transformRequestOut?: (request: any, context: TransformerContext) => Promise<UnifiedChatRequest>;\n  transformResponseIn?: (response: Response, context?: TransformerContext) => Promise<Response>;\n  transformResponseOut?: (response: Response, context: TransformerContext) => Promise<Response>;\n  endPoint?: string;\n  name?: string;\n  auth?: (request: any, provider: LLMProvider, context: TransformerContext) => Promise<any>;\n}\n```\n\n#### 3. 内置转换器\n\n核心包包含以下转换器：\n- **anthropic**：Anthropic API 格式\n- **openai**：OpenAI API 格式\n- **gemini**：Google Gemini API 格式\n- **deepseek**：DeepSeek API 格式\n- **groq**：Groq API 格式\n- **openrouter**：OpenRouter API 格式\n- 等等...\n\n### 与 CCR Server 的集成\n\nCCR server 通过以下方式集成 `@musistudio/llms`：\n\n1. **转换器服务**（`packages/core/src/services/transformer.ts`）：管理转换器的注册和实例化\n2. **提供商配置**：将提供商配置映射到核心包的 LLMProvider 接口\n3. **请求管道**：在请求处理过程中按顺序应用转换器\n4. **自定义转换器**：支持加载外部转换器插件\n\n### 版本和更新\n\n`@musistudio/llms` 的当前版本是 `1.0.51`。它作为独立的 npm 包发布，可以独立使用或作为 CCR Server 的一部分使用。\n\n## 核心功能\n\n### 1. 请求路由\n- 基于 Token 数量的智能路由\n- 项目级路由配置\n- 自定义路由函数\n- 场景化路由（background、think、longContext 等）\n\n### 2. 请求转换\n- 支持多种 LLM 提供商的 API 格式转换\n- 内置转换器：Anthropic、DeepSeek、Gemini、OpenRouter、Groq 等\n- 可扩展的转换器系统\n\n### 3. Agent 系统\n- 插件式的 Agent 架构\n- 内置图片处理 Agent\n- 自定义 Agent 支持\n\n### 4. 配置管理\n- JSON5 格式配置文件\n- 环境变量插值\n- 配置热更新（需重启服务）\n\n## 使用场景\n\n### 场景一：个人本地服务\n在本地运行服务，供个人 Claude Code 使用：\n\n```bash\nccr start\n```\n\n### 场景二：团队共享服务\n使用 Docker 部署，为团队成员提供共享服务：\n\n```bash\ndocker run -d -p 3456:3456 musistudio/claude-code-router\n```\n\n### 场景三：二次开发\n基于暴露的 API 构建自定义应用：\n\n```bash\nGET /api/config\nPOST /v1/messages\nGET /api/logs\n```\n\n## 下一步\n\n- [Docker 部署指南](/docs/server/deployment) - 学习如何部署服务\n- [API 参考](/docs/category/api) - 查看完整的 API 文档\n- [配置说明](/docs/category/server-config) - 了解服务器配置选项\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs/current.json",
    "content": "{\n  \"version.label\": {\n    \"message\": \"Next\",\n    \"description\": \"The label for version current\"\n  },\n  \"sidebar.tutorialSidebar.category.Server\": {\n    \"message\": \"服务器\",\n    \"description\": \"The label for category 'Server' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.Server.link.generated-index.title\": {\n    \"message\": \"Claude Code Router 服务器\",\n    \"description\": \"The generated-index page title for category 'Server' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.Server.link.generated-index.description\": {\n    \"message\": \"部署和管理 Claude Code Router 服务器\",\n    \"description\": \"The generated-index page description for category 'Server' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.API Reference\": {\n    \"message\": \"API 参考\",\n    \"description\": \"The label for category 'API Reference' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.API Reference.link.generated-index.title\": {\n    \"message\": \"API 参考\",\n    \"description\": \"The generated-index page title for category 'API Reference' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.API Reference.link.generated-index.description\": {\n    \"message\": \"服务器 API 接口文档\",\n    \"description\": \"The generated-index page description for category 'API Reference' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.server-configuration-category\": {\n    \"message\": \"配置\",\n    \"description\": \"The label for category 'Configuration' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.server-configuration-category.link.generated-index.title\": {\n    \"message\": \"服务器配置\",\n    \"description\": \"The generated-index page title for category 'Configuration' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.server-configuration-category.link.generated-index.description\": {\n    \"message\": \"服务器配置说明\",\n    \"description\": \"The generated-index page description for category 'Configuration' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.Advanced\": {\n    \"message\": \"高级\",\n    \"description\": \"The label for category 'Advanced' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.Advanced.link.generated-index.title\": {\n    \"message\": \"高级主题\",\n    \"description\": \"The generated-index page title for category 'Advanced' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.Advanced.link.generated-index.description\": {\n    \"message\": \"高级功能和自定义\",\n    \"description\": \"The generated-index page description for category 'Advanced' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.CLI\": {\n    \"message\": \"CLI\",\n    \"description\": \"The label for category 'CLI' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.CLI.link.generated-index.title\": {\n    \"message\": \"Claude Code Router CLI\",\n    \"description\": \"The generated-index page title for category 'CLI' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.CLI.link.generated-index.description\": {\n    \"message\": \"命令行工具使用指南\",\n    \"description\": \"The generated-index page description for category 'CLI' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.Commands\": {\n    \"message\": \"命令\",\n    \"description\": \"The label for category 'Commands' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.Commands.link.generated-index.title\": {\n    \"message\": \"CLI 命令\",\n    \"description\": \"The generated-index page title for category 'Commands' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.Commands.link.generated-index.description\": {\n    \"message\": \"完整的命令参考\",\n    \"description\": \"The generated-index page description for category 'Commands' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.cli-configuration-category\": {\n    \"message\": \"配置\",\n    \"description\": \"The label for category 'Configuration' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.cli-configuration-category.link.generated-index.title\": {\n    \"message\": \"CLI 配置\",\n    \"description\": \"The generated-index page title for category 'Configuration' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.cli-configuration-category.link.generated-index.description\": {\n    \"message\": \"CLI 配置指南\",\n    \"description\": \"The generated-index page description for category 'Configuration' in sidebar 'tutorialSidebar'\"\n  }\n}\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs.backup.20260101_205603/advanced/custom-router.md",
    "content": "---\nid: advanced/custom-router\ntitle: 自定义路由器\nsidebar_position: 1\n---\n\n# 自定义路由器\n\n使用 JavaScript 编写自己的路由逻辑。\n\n## 创建自定义路由器\n\n创建一个导出路由函数的 JavaScript 文件：\n\n```javascript\n// custom-router.js\nmodule.exports = async function(req, config) {\n  // 获取用户消息\n  const userMessage = req.body.messages.find(m => m.role === 'user')?.content;\n\n  // 自定义逻辑\n  if (userMessage && userMessage.includes('解释代码')) {\n    return 'openrouter,anthropic/claude-3.5-sonnet';\n  }\n\n  // 返回 null 以使用默认路由\n  return null;\n};\n```\n\n## 参数说明\n\n路由函数接收以下参数：\n\n| 参数 | 类型 | 说明 |\n|------|------|------|\n| `req` | object | 来自 Claude Code 的请求对象，包含请求体 |\n| `config` | object | 应用程序的配置对象 |\n\n## 配置\n\n在 `config.json` 中设置 `CUSTOM_ROUTER_PATH` 以使用您的自定义路由器：\n\n```json\n{\n  \"CUSTOM_ROUTER_PATH\": \"/path/to/custom-router.js\"\n}\n```\n\n## 返回格式\n\n路由函数应返回以下格式的字符串：\n\n```\n{provider-name},{model-name}\n```\n\n示例：\n\n```\ndeepseek,deepseek-chat\n```\n\n如果返回 `null`，则回退到默认路由配置。\n\n## 错误处理\n\n如果路由函数抛出错误或返回无效格式，路由器将回退到默认路由配置。\n\n## 示例：基于时间的路由\n\n```javascript\nmodule.exports = async function(req, config) {\n  const hour = new Date().getHours();\n\n  // 工作时间使用更快的模型\n  if (hour >= 9 && hour <= 18) {\n    return 'groq,llama-3.3-70b-versatile';\n  }\n\n  // 非工作时间使用更强大的模型\n  return 'deepseek,deepseek-chat';\n};\n```\n\n## 示例：成本优化\n\n```javascript\nmodule.exports = async function(req, config) {\n  const userMessage = req.body.messages.find(m => m.role === 'user')?.content;\n\n  // 简单任务使用较便宜的模型\n  if (userMessage && userMessage.length < 100) {\n    return 'groq,llama-3.3-70b-versatile';\n  }\n\n  // 复杂任务使用默认模型\n  return null;\n};\n```\n\n## 示例：任务类型路由\n\n```javascript\nmodule.exports = async function(req, config) {\n  const userMessage = req.body.messages.find(m => m.role === 'user')?.content;\n\n  if (!userMessage) return null;\n\n  // 代码相关任务\n  if (userMessage.includes('代码') || userMessage.includes('code')) {\n    return 'deepseek,deepseek-coder';\n  }\n\n  // 解释任务\n  if (userMessage.includes('解释') || userMessage.includes('explain')) {\n    return 'openrouter,anthropic/claude-3.5-sonnet';\n  }\n\n  // 默认\n  return null;\n};\n```\n\n## 测试您的路由器\n\n通过检查日志来测试您的自定义路由器：\n\n```bash\ntail -f ~/.claude-code-router/claude-code-router.log\n```\n\n查找路由决策以查看正在选择哪个模型。\n\n## 子代理路由\n\n对于子代理内的路由，您必须在子代理提示词的**开头**包含 `<CCR-SUBAGENT-MODEL>provider,model</CCR-SUBAGENT-MODEL>` 来指定特定的提供商和模型。\n\n**示例：**\n\n```\n<CCR-SUBAGENT-MODEL>openrouter,anthropic/claude-3.5-sonnet</CCR-SUBAGENT-MODEL>\n请帮我分析这段代码是否存在潜在的优化空间...\n```\n\n## 下一步\n\n- [Agent](/zh/docs/advanced/agents) - 使用 Agent 扩展功能\n- [预设](/zh/docs/advanced/presets) - 使用预定义配置\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs.backup.20260101_205603/advanced/preset-format.md",
    "content": "---\nid: advanced/preset-format\ntitle: Preset 格式规范\nsidebar_position: 4\n---\n\n# Preset 格式规范\n\n本文档详细说明了 Preset 配置文件的格式规范、字段定义和使用方法。\n\n## 概述\n\nPreset 是一个预定义的配置包，用于快速配置 Claude Code Router。Preset 以目录形式存储，内部包含一个 `manifest.json` 文件。\n\n### 文件结构\n\n```\n~/.claude-code-router/presets/<preset-name>/\n└── manifest.json\n```\n\n### 存储位置\n\n- **预设目录**: `~/.claude-code-router/presets/<preset-name>/`\n\n## manifest.json 结构\n\n`manifest.json` 是一个扁平化的 JSON 文件（支持 JSON5 格式），包含三个主要部分：\n\n1. **元数据（Metadata）**: 描述预设的基本信息\n2. **配置（Configuration）**: 实际的配置内容\n3. **动态配置系统**: Schema、Template 和 ConfigMappings\n\n```json\n{\n  // === 元数据字段 ===\n  \"name\": \"my-preset\",\n  \"version\": \"1.0.0\",\n  \"description\": \"我的预设配置\",\n  \"author\": \"作者名\",\n  \"homepage\": \"https://example.com\",\n  \"repository\": \"https://github.com/user/repo\",\n  \"license\": \"MIT\",\n  \"keywords\": [\"openai\", \"production\"],\n  \"ccrVersion\": \"2.0.0\",\n\n  // === 配置字段 ===\n  \"Providers\": [...],\n  \"Router\": {...},\n  \"transformers\": [...],\n  \"StatusLine\": {...},\n  \"PROXY_URL\": \"...\",\n  \"PORT\": 8080,\n\n  // === 动态配置系统 ===\n  \"schema\": [...],\n  \"template\": {...},\n  \"configMappings\": [...],\n  \"requiredInputs\": [...],\n  \"userValues\": {...}\n}\n```\n\n## 元数据字段\n\n### 必填字段\n\n| 字段 | 类型 | 说明 |\n|------|------|------|\n| `name` | string | Preset 名称，唯一标识符 |\n| `version` | string | 版本号（遵循 semver 规范） |\n\n### 可选字段\n\n| 字段 | 类型 | 说明 |\n|------|------|------|\n| `description` | string | Preset 描述 |\n| `author` | string | 作者信息 |\n| `homepage` | string | 项目主页 URL |\n| `repository` | string | 源代码仓库 URL |\n| `license` | string | 许可证类型 |\n| `keywords` | string[] | 关键词标签 |\n| `ccrVersion` | string | 兼容的 CCR 版本 |\n| `source` | string | Preset 来源 URL |\n| `sourceType` | string | 来源类型（`local`/`gist`/`registry`） |\n| `checksum` | string | 内容校验和（SHA256） |\n\n### 元数据示例\n\n```json\n{\n  \"name\": \"openai-production\",\n  \"version\": \"1.2.0\",\n  \"description\": \"OpenAI 生产环境配置，包含代理和多模型支持\",\n  \"author\": \"Your Name\",\n  \"homepage\": \"https://github.com/yourname/ccr-presets\",\n  \"repository\": \"https://github.com/yourname/ccr-presets.git\",\n  \"license\": \"MIT\",\n  \"keywords\": [\"openai\", \"production\", \"proxy\"],\n  \"ccrVersion\": \"2.0.0\"\n}\n```\n\n## 配置字段\n\n配置字段直接对应 CCR 的配置文件结构（`config.json`）。\n\n### Providers\n\nProvider 配置数组，定义 LLM 服务提供商。\n\n```json\n{\n  \"Providers\": [\n    {\n      \"name\": \"openai\",\n      \"api_base_url\": \"https://api.openai.com/v1\",\n      \"api_key\": \"${OPENAI_API_KEY}\",\n      \"models\": [\"gpt-4o\", \"gpt-4o-mini\"],\n      \"transformer\": \"anthropic\",\n      \"timeout\": 60000,\n      \"max_retries\": 3\n    }\n  ]\n}\n```\n\n#### Provider 字段说明\n\n| 字段 | 类型 | 必填 | 说明 |\n|------|------|------|------|\n| `name` | string | 是 | Provider 名称（唯一标识） |\n| `api_base_url` | string | 是 | API 基础地址 |\n| `api_key` | string | 是 | API 密钥（可以是环境变量） |\n| `models` | string[] | 是 | 支持的模型列表 |\n| `transformer` | string | 否 | 使用的转换器 |\n| `timeout` | number | 否 | 超时时间（毫秒） |\n| `max_retries` | number | 否 | 最大重试次数 |\n| `headers` | object | 否 | 自定义 HTTP 头 |\n\n### Router\n\n路由配置，定义请求如何路由到不同的模型。\n\n```json\n{\n  \"Router\": {\n    \"default\": \"openai/gpt-4o\",\n    \"background\": \"openai/gpt-4o-mini\",\n    \"think\": \"openai/gpt-4o\",\n    \"longContext\": \"openai/gpt-4o\",\n    \"longContextThreshold\": 100000,\n    \"webSearch\": \"openai/gpt-4o\",\n    \"image\": \"openai/gpt-4o\"\n  }\n}\n```\n\n#### Router 字段说明\n\n| 字段 | 类型 | 说明 |\n|------|------|------|\n| `default` | string | 默认路由（格式：`provider/model`） |\n| `background` | string | 后台任务路由 |\n| `think` | string | 思考模式路由 |\n| `longContext` | string | 长上下文路由 |\n| `longContextThreshold` | number | 长上下文阈值（token 数） |\n| `webSearch` | string | 网络搜索路由 |\n| `image` | string | 图像处理路由 |\n\n### Transformers\n\n转换器配置数组，用于处理不同 Provider 的 API 差异。\n\n```json\n{\n  \"transformers\": [\n    {\n      \"path\": \"./transformers/custom-transformer.js\",\n      \"use\": [\"provider1\", \"provider2\"],\n      \"options\": {\n        \"max_tokens\": 4096\n      }\n    },\n    {\n      \"use\": [\n        [\"provider3\", { \"option\": \"value\" }]\n      ]\n    }\n  ]\n}\n```\n\n#### Transformer 字段说明\n\n| 字段 | 类型 | 说明 |\n|------|------|------|\n| `path` | string | 自定义转换器路径（相对或绝对） |\n| `use` | array | 应用到哪些 Provider |\n| `options` | object | 转换器选项 |\n\n### StatusLine\n\n状态栏配置，自定义终端状态显示。\n\n```json\n{\n  \"StatusLine\": {\n    \"default\": {\n      \"modules\": [\n        {\n          \"type\": \"text\",\n          \"text\": \"CCR\",\n          \"color\": \"cyan\"\n        },\n        {\n          \"type\": \"provider\",\n          \"showModel\": true\n        },\n        {\n          \"type\": \"script\",\n          \"scriptPath\": \"./scripts/status.js\"\n        }\n      ]\n    }\n  }\n}\n```\n\n### 其他配置字段\n\n支持所有 `config.json` 中的字段：\n\n```json\n{\n  \"PORT\": 8080,\n  \"HOST\": \"0.0.0.0\",\n  \"PROXY_URL\": \"http://127.0.0.1:7890\",\n  \"LOG_LEVEL\": \"info\",\n  \"NON_INTERACTIVE_MODE\": false\n}\n```\n\n## 动态配置系统\n\n动态配置系统是 CCR 2.0 的核心功能，允许创建可交互的配置模板。\n\n### Schema（配置输入表单）\n\nSchema 定义了安装时需要用户输入的字段。\n\n#### Schema 字段类型\n\n| 类型 | 说明 | 使用场景 |\n|------|------|----------|\n| `password` | 密码输入（隐藏） | API Key、密钥 |\n| `input` | 单行文本输入 | URL、名称 |\n| `number` | 数字输入 | 端口号、超时时间 |\n| `select` | 单选下拉框 | 选择 Provider、模型 |\n| `multiselect` | 多选框 | 启用功能列表 |\n| `confirm` | 确认框 | 是否启用某功能 |\n| `editor` | 多行文本编辑器 | 自定义配置、脚本 |\n\n#### Schema 字段定义\n\n```json\n{\n  \"schema\": [\n    {\n      \"id\": \"apiKey\",\n      \"type\": \"password\",\n      \"label\": \"API Key\",\n      \"prompt\": \"请输入您的 OpenAI API Key\",\n      \"placeholder\": \"sk-...\",\n      \"required\": true,\n      \"validator\": \"^sk-.*\"\n    },\n    {\n      \"id\": \"provider\",\n      \"type\": \"select\",\n      \"label\": \"选择 Provider\",\n      \"prompt\": \"选择您主要使用的 LLM 提供商\",\n      \"options\": {\n        \"type\": \"static\",\n        \"options\": [\n          {\n            \"label\": \"OpenAI\",\n            \"value\": \"openai\",\n            \"description\": \"使用 OpenAI 的 GPT 模型\"\n          },\n          {\n            \"label\": \"DeepSeek\",\n            \"value\": \"deepseek\",\n            \"description\": \"使用 DeepSeek 的高性价比模型\"\n          }\n        ]\n      },\n      \"defaultValue\": \"openai\",\n      \"required\": true\n    },\n    {\n      \"id\": \"model\",\n      \"type\": \"select\",\n      \"label\": \"模型\",\n      \"prompt\": \"选择默认使用的模型\",\n      \"options\": {\n        \"type\": \"models\",\n        \"providerField\": \"#{provider}\"\n      },\n      \"when\": {\n        \"field\": \"provider\",\n        \"operator\": \"exists\"\n      },\n      \"required\": true\n    },\n    {\n      \"id\": \"maxTokens\",\n      \"type\": \"number\",\n      \"label\": \"最大 Token 数\",\n      \"prompt\": \"设置请求的最大 token 数\",\n      \"min\": 1,\n      \"max\": 128000,\n      \"defaultValue\": 4096\n    },\n    {\n      \"id\": \"useProxy\",\n      \"type\": \"confirm\",\n      \"label\": \"使用代理\",\n      \"prompt\": \"是否通过代理访问 API？\",\n      \"defaultValue\": false\n    },\n    {\n      \"id\": \"proxyUrl\",\n      \"type\": \"input\",\n      \"label\": \"代理地址\",\n      \"prompt\": \"输入代理服务器地址\",\n      \"placeholder\": \"http://127.0.0.1:7890\",\n      \"required\": true,\n      \"when\": {\n        \"field\": \"useProxy\",\n        \"operator\": \"eq\",\n        \"value\": true\n      }\n    },\n    {\n      \"id\": \"customConfig\",\n      \"type\": \"editor\",\n      \"label\": \"自定义配置\",\n      \"prompt\": \"输入 JSON 格式的自定义配置\",\n      \"rows\": 10\n    }\n  ]\n}\n```\n\n#### Schema 字段详细说明\n\n##### 基础字段\n\n| 字段 | 类型 | 必填 | 说明 |\n|------|------|------|------|\n| `id` | string | 是 | 字段唯一标识符（用于变量引用） |\n| `type` | string | 否 | 字段类型（默认 `password`） |\n| `label` | string | 否 | 显示标签 |\n| `prompt` | string | 否 | 提示信息/描述 |\n| `placeholder` | string | 否 | 占位符文本 |\n\n##### 验证字段\n\n| 字段 | 类型 | 说明 |\n|------|------|------|\n| `required` | boolean | 是否必填（默认 `true`） |\n| `validator` | RegExp/string/function | 验证规则 |\n| `min` | number | 最小值（number 类型） |\n| `max` | number | 最大值（number 类型） |\n\n##### 选项字段（select/multiselect）\n\n| 字段 | 类型 | 说明 |\n|------|------|------|\n| `options` | array/object | 静态选项数组或动态选项配置 |\n\n##### 条件字段\n\n| 字段 | 类型 | 说明 |\n|------|------|------|\n| `when` | object/object[] | 显示条件（支持 AND 逻辑） |\n| `defaultValue` | any | 默认值 |\n| `dependsOn` | string[] | 显式声明依赖字段 |\n\n#### 条件运算符\n\n| 运算符 | 说明 | 示例 |\n|--------|------|------|\n| `eq` | 等于 | `{\"field\": \"type\", \"operator\": \"eq\", \"value\": \"openai\"}` |\n| `ne` | 不等于 | `{\"field\": \"advanced\", \"operator\": \"ne\", \"value\": true}` |\n| `in` | 包含于（数组） | `{\"field\": \"feature\", \"operator\": \"in\", \"value\": [\"a\", \"b\"]}` |\n| `nin` | 不包含于（数组） | `{\"field\": \"type\", \"operator\": \"nin\", \"value\": [\"x\", \"y\"]}` |\n| `exists` | 字段存在 | `{\"field\": \"apiKey\", \"operator\": \"exists\"}` |\n| `gt` | 大于 | `{\"field\": \"count\", \"operator\": \"gt\", \"value\": 0}` |\n| `lt` | 小于 | `{\"field\": \"count\", \"operator\": \"lt\", \"value\": 100}` |\n| `gte` | 大于等于 | `{\"field\": \"count\", \"operator\": \"gte\", \"value\": 1}` |\n| `lte` | 小于等于 | `{\"field\": \"count\", \"operator\": \"lte\", \"value\": 99}` |\n\n#### 动态选项类型\n\n##### static - 静态选项\n\n```json\n{\n  \"options\": {\n    \"type\": \"static\",\n    \"options\": [\n      {\"label\": \"选项1\", \"value\": \"value1\"},\n      {\"label\": \"选项2\", \"value\": \"value2\"}\n    ]\n  }\n}\n```\n\n##### providers - 从 Providers 配置提取\n\n```json\n{\n  \"options\": {\n    \"type\": \"providers\"\n  }\n}\n```\n自动从 `Providers` 数组中提取 `name` 作为选项。\n\n##### models - 从指定 Provider 的 models 提取\n\n```json\n{\n  \"options\": {\n    \"type\": \"models\",\n    \"providerField\": \"#{selectedProvider}\"\n  }\n}\n```\n根据用户选择的 Provider，动态显示该 Provider 的 models。\n\n### Template（配置模板）\n\nTemplate 定义了如何根据用户输入生成配置。\n\n#### 变量语法\n\n使用 `#{变量名}` 语法引用用户输入：\n\n```json\n{\n  \"template\": {\n    \"Providers\": [\n      {\n        \"name\": \"#{providerName}\",\n        \"api_base_url\": \"#{baseUrl}\",\n        \"api_key\": \"#{apiKey}\",\n        \"models\": [\"#{defaultModel}\"]\n      }\n    ],\n    \"Router\": {\n      \"default\": \"#{providerName}/#{defaultModel}\"\n    }\n  }\n}\n```\n\n#### Template 示例\n\n```json\n{\n  \"template\": {\n    \"Providers\": [\n      {\n        \"name\": \"#{primaryProvider}\",\n        \"api_base_url\": \"#{baseUrl}\",\n        \"api_key\": \"#{apiKey}\",\n        \"models\": [\"#{defaultModel}\"],\n        \"timeout\": #{timeout}\n      }\n    ],\n    \"Router\": {\n      \"default\": \"#{primaryProvider}/#{defaultModel}\",\n      \"background\": \"#{primaryProvider}/#{backgroundModel}\"\n    },\n    \"PROXY_URL\": \"#{proxyUrl}\",\n    \"PORT\": #{port}\n  }\n}\n```\n\n### ConfigMappings（配置映射）\n\nConfigMappings 用于精确控制用户输入值如何映射到配置的特定位置。\n\n#### ConfigMapping 结构\n\n```json\n{\n  \"configMappings\": [\n    {\n      \"target\": \"Providers[0].api_key\",\n      \"value\": \"#{apiKey}\"\n    },\n    {\n      \"target\": \"PROXY_URL\",\n      \"value\": \"#{proxyUrl}\",\n      \"when\": {\n        \"field\": \"useProxy\",\n        \"operator\": \"eq\",\n        \"value\": true\n      }\n    },\n    {\n      \"target\": \"PORT\",\n      \"value\": 8080\n    }\n  ]\n}\n```\n\n#### 字段说明\n\n| 字段 | 类型 | 说明 |\n|------|------|------|\n| `target` | string | 目标字段路径（支持数组语法） |\n| `value` | string/any | 值来源（变量引用或固定值） |\n| `when` | object/object[] | 应用条件 |\n\n#### 目标路径语法\n\n- `Providers[0].api_key` - 第一个 Provider 的 api_key\n- `Router.default` - Router 的 default 字段\n- `PORT` - 顶层配置字段\n\n### userValues（用户值存储）\n\nuserValues 存储用户在安装时填写的值，运行时自动应用。\n\n```json\n{\n  \"userValues\": {\n    \"apiKey\": \"sk-xxx...\",\n    \"provider\": \"openai\",\n    \"defaultModel\": \"gpt-4o\",\n    \"useProxy\": true,\n    \"proxyUrl\": \"http://127.0.0.1:7890\"\n  }\n}\n```\n\n### requiredInputs（必填输入）\n\nrequiredInputs 是导出预设时自动生成的字段列表，用于提示用户需要填写哪些信息。\n\n```json\n{\n  \"requiredInputs\": [\n    {\n      \"id\": \"Providers[0].api_key\",\n      \"prompt\": \"Enter api_key\",\n      \"placeholder\": \"OPENAI_API_KEY\"\n    },\n    {\n      \"id\": \"PROXY_URL\",\n      \"prompt\": \"Enter proxy URL\",\n      \"placeholder\": \"PROXY_URL\"\n    }\n  ]\n}\n```\n\n## 敏感字段处理\n\nCCR 会自动识别敏感字段（如 `api_key`、`secret`、`password` 等），并将其替换为环境变量占位符。\n\n### 自动识别的敏感字段\n\n- `api_key`, `apiKey`, `apikey`\n- `api_secret`, `apiSecret`\n- `secret`, `SECRET`\n- `token`, `TOKEN`\n- `password`, `PASSWORD`\n- `private_key`, `privateKey`\n- `access_key`, `accessKey`\n\n### 环境变量占位符格式\n\n```bash\n# 推荐格式\n${VARIABLE_NAME}\n\n# 也支持\n$VARIABLE_NAME\n```\n\n### 示例\n\n**原始配置:**\n```json\n{\n  \"Providers\": [\n    {\n      \"name\": \"openai\",\n      \"api_key\": \"sk-abc123...\"\n    }\n  ]\n}\n```\n\n**导出后:**\n```json\n{\n  \"Providers\": [\n    {\n      \"name\": \"openai\",\n      \"api_key\": \"${OPENAI_API_KEY}\"\n    }\n  ],\n  \"requiredInputs\": [\n    {\n      \"id\": \"Providers[0].api_key\",\n      \"prompt\": \"Enter api_key\",\n      \"placeholder\": \"OPENAI_API_KEY\"\n    }\n  ]\n}\n```\n\n## 完整示例\n\n### 简单预设（无动态配置）\n\n```json\n{\n  \"name\": \"simple-openai\",\n  \"version\": \"1.0.0\",\n  \"description\": \"简单的 OpenAI 配置\",\n  \"author\": \"Your Name\",\n\n  \"Providers\": [\n    {\n      \"name\": \"openai\",\n      \"api_base_url\": \"https://api.openai.com/v1\",\n      \"api_key\": \"${OPENAI_API_KEY}\",\n      \"models\": [\"gpt-4o\", \"gpt-4o-mini\"]\n    }\n  ],\n\n  \"Router\": {\n    \"default\": \"openai/gpt-4o\",\n    \"background\": \"openai/gpt-4o-mini\"\n  },\n\n  \"requiredInputs\": [\n    {\n      \"id\": \"Providers[0].api_key\",\n      \"prompt\": \"Enter OpenAI API Key\",\n      \"placeholder\": \"OPENAI_API_KEY\"\n    }\n  ]\n}\n```\n\n### 高级预设（动态配置）\n\n```json\n{\n  \"name\": \"multi-provider-advanced\",\n  \"version\": \"2.0.0\",\n  \"description\": \"多 Provider 高级配置，支持动态选择和代理\",\n  \"author\": \"Your Name\",\n  \"keywords\": [\"openai\", \"deepseek\", \"proxy\", \"multi-provider\"],\n  \"ccrVersion\": \"2.0.0\",\n\n  \"schema\": [\n    {\n      \"id\": \"primaryProvider\",\n      \"type\": \"select\",\n      \"label\": \"主要 Provider\",\n      \"prompt\": \"选择您主要使用的 LLM 提供商\",\n      \"options\": {\n        \"type\": \"static\",\n        \"options\": [\n          {\n            \"label\": \"OpenAI\",\n            \"value\": \"openai\",\n            \"description\": \"使用 OpenAI 的 GPT 模型，质量高\"\n          },\n          {\n            \"label\": \"DeepSeek\",\n            \"value\": \"deepseek\",\n            \"description\": \"使用 DeepSeek 的高性价比模型\"\n          }\n        ]\n      },\n      \"required\": true,\n      \"defaultValue\": \"openai\"\n    },\n    {\n      \"id\": \"apiKey\",\n      \"type\": \"password\",\n      \"label\": \"API Key\",\n      \"prompt\": \"请输入您的 API Key\",\n      \"placeholder\": \"sk-...\",\n      \"required\": true,\n      \"validator\": \"^sk-.+\"\n    },\n    {\n      \"id\": \"defaultModel\",\n      \"type\": \"select\",\n      \"label\": \"默认模型\",\n      \"prompt\": \"选择默认使用的模型\",\n      \"options\": {\n        \"type\": \"static\",\n        \"options\": [\n          {\"label\": \"GPT-4o\", \"value\": \"gpt-4o\"},\n          {\"label\": \"GPT-4o-mini\", \"value\": \"gpt-4o-mini\"}\n        ]\n      },\n      \"required\": true,\n      \"defaultValue\": \"gpt-4o\",\n      \"when\": {\n        \"field\": \"primaryProvider\",\n        \"operator\": \"eq\",\n        \"value\": \"openai\"\n      }\n    },\n    {\n      \"id\": \"backgroundModel\",\n      \"type\": \"select\",\n      \"label\": \"后台任务模型\",\n      \"prompt\": \"选择用于后台任务的轻量级模型\",\n      \"options\": {\n        \"type\": \"static\",\n        \"options\": [\n          {\"label\": \"GPT-4o-mini\", \"value\": \"gpt-4o-mini\"}\n        ]\n      },\n      \"required\": true,\n      \"defaultValue\": \"gpt-4o-mini\",\n      \"when\": {\n        \"field\": \"primaryProvider\",\n        \"operator\": \"eq\",\n        \"value\": \"openai\"\n      }\n    },\n    {\n      \"id\": \"maxTokens\",\n      \"type\": \"number\",\n      \"label\": \"最大 Token 数\",\n      \"prompt\": \"设置单次请求的最大 token 数\",\n      \"min\": 1,\n      \"max\": 128000,\n      \"defaultValue\": 4096\n    },\n    {\n      \"id\": \"timeout\",\n      \"type\": \"number\",\n      \"label\": \"超时时间（秒）\",\n      \"prompt\": \"设置 API 请求超时时间\",\n      \"min\": 10,\n      \"max\": 300,\n      \"defaultValue\": 60\n    },\n    {\n      \"id\": \"enableProxy\",\n      \"type\": \"confirm\",\n      \"label\": \"启用代理\",\n      \"prompt\": \"是否通过代理访问 API？\",\n      \"defaultValue\": false\n    },\n    {\n      \"id\": \"proxyUrl\",\n      \"type\": \"input\",\n      \"label\": \"代理地址\",\n      \"prompt\": \"输入代理服务器地址\",\n      \"placeholder\": \"http://127.0.0.1:7890\",\n      \"required\": true,\n      \"when\": {\n        \"field\": \"enableProxy\",\n        \"operator\": \"eq\",\n        \"value\": true\n      },\n      \"validator\": \"^https?://.+\"\n    },\n    {\n      \"id\": \"features\",\n      \"type\": \"multiselect\",\n      \"label\": \"启用功能\",\n      \"prompt\": \"选择要启用的额外功能\",\n      \"options\": {\n        \"type\": \"static\",\n        \"options\": [\n          {\"label\": \"长上下文支持\", \"value\": \"longContext\"},\n          {\"label\": \"网络搜索\", \"value\": \"webSearch\"},\n          {\"label\": \"图像处理\", \"value\": \"image\"}\n        ]\n      },\n      \"defaultValue\": []\n    }\n  ],\n\n  \"template\": {\n    \"Providers\": [\n      {\n        \"name\": \"#{primaryProvider}\",\n        \"api_base_url\": \"#{primaryProvider === 'openai' ? 'https://api.openai.com/v1' : 'https://api.deepseek.com'}\",\n        \"api_key\": \"#{apiKey}\",\n        \"models\": [\n          \"#{defaultModel}\",\n          \"#{backgroundModel}\"\n        ],\n        \"timeout\": #{timeout * 1000}\n      }\n    ],\n    \"Router\": {\n      \"default\": \"#{primaryProvider}/#{defaultModel}\",\n      \"background\": \"#{primaryProvider}/#{backgroundModel}\"\n    },\n    \"NON_INTERACTIVE_MODE\": false\n  },\n\n  \"configMappings\": [\n    {\n      \"target\": \"PROXY_URL\",\n      \"value\": \"#{proxyUrl}\",\n      \"when\": {\n        \"field\": \"enableProxy\",\n        \"operator\": \"eq\",\n        \"value\": true\n      }\n    },\n    {\n      \"target\": \"Router.longContext\",\n      \"value\": \"#{primaryProvider}/#{defaultModel}\",\n      \"when\": {\n        \"field\": \"features\",\n        \"operator\": \"in\",\n        \"value\": [\"longContext\"]\n      }\n    },\n    {\n      \"target\": \"Router.webSearch\",\n      \"value\": \"#{primaryProvider}/#{defaultModel}\",\n      \"when\": {\n        \"field\": \"features\",\n        \"operator\": \"in\",\n        \"value\": [\"webSearch\"]\n      }\n    },\n    {\n      \"target\": \"Router.image\",\n      \"value\": \"#{primaryProvider}/#{defaultModel}\",\n      \"when\": {\n        \"field\": \"features\",\n        \"operator\": \"in\",\n        \"value\": [\"image\"]\n      }\n    }\n  ]\n}\n```\n\n## 验证规则\n\n### Preset 验证检查项\n\n1. **元数据验证**\n   - ✓ `name` 字段存在\n   - ✓ `version` 字段存在（警告）\n\n2. **配置验证**\n   - ✓ `config` 部分存在\n   - ✓ 每个 Provider 有 `name` 字段\n   - ✓ 每个 Provider 有 `api_base_url` 字段\n   - ✓ 每个 Provider 有 `models` 数组（警告）\n\n3. **Schema 验证**\n   - ✓ 字段 `id` 唯一\n   - ✓ 条件字段引用存在\n   - ✓ 动态选项配置正确\n\n### 错误和警告\n\n**错误（Error）:**\n- 缺少必填字段\n- Provider 配置不完整\n- Schema 字段重复\n\n**警告（Warning）:**\n- 缺少可选字段\n- Provider 没有 models\n- 未使用的 schema 字段\n\n## 最佳实践\n\n### 1. 使用动态配置系统\n\n```json\n{\n  \"schema\": [\n    {\n      \"id\": \"apiKey\",\n      \"type\": \"password\",\n      \"label\": \"API Key\",\n      \"required\": true\n    }\n  ],\n  \"template\": {\n    \"Providers\": [\n      {\n        \"api_key\": \"#{apiKey}\"\n      }\n    ]\n  }\n}\n```\n\n### 2. 提供合理的默认值\n\n```json\n{\n  \"id\": \"timeout\",\n  \"type\": \"number\",\n  \"label\": \"超时时间\",\n  \"defaultValue\": 60,\n  \"min\": 10,\n  \"max\": 300\n}\n```\n\n### 3. 使用条件显示减少不必要的输入\n\n```json\n{\n  \"id\": \"proxyUrl\",\n  \"type\": \"input\",\n  \"label\": \"代理地址\",\n  \"when\": {\n    \"field\": \"useProxy\",\n    \"operator\": \"eq\",\n    \"value\": true\n  }\n}\n```\n\n### 4. 清晰的标签和提示\n\n```json\n{\n  \"id\": \"apiKey\",\n  \"type\": \"password\",\n  \"label\": \"OpenAI API Key\",\n  \"prompt\": \"请输入您的 OpenAI API Key（以 sk- 开头）\",\n  \"placeholder\": \"sk-...\",\n  \"validator\": \"^sk-.+\"\n}\n```\n\n### 5. 使用验证确保数据质量\n\n```json\n{\n  \"id\": \"port\",\n  \"type\": \"number\",\n  \"label\": \"端口号\",\n  \"min\": 1024,\n  \"max\": 65535,\n  \"validator\": (value) => {\n    if (value < 1024 || value > 65535) {\n      return \"端口号必须在 1024-65535 之间\";\n    }\n    return true;\n  }\n}\n```\n\n### 6. 版本控制\n\n遵循 semver 规范：\n- `1.0.0` - 初始版本\n- `1.1.0` - 新增功能（向后兼容）\n- `1.0.1` - Bug 修复\n- `2.0.0` - 破坏性变更\n\n### 7. 文档化\n\n```json\n{\n  \"name\": \"my-preset\",\n  \"version\": \"1.0.0\",\n  \"description\": \"详细的预设描述，说明用途和特点\",\n  \"author\": \"作者名 <email@example.com>\",\n  \"homepage\": \"https://github.com/user/preset\",\n  \"repository\": \"https://github.com/user/preset.git\",\n  \"keywords\": [\"openai\", \"production\", \"proxy\"],\n  \"license\": \"MIT\"\n}\n```\n\n### 8. 使用相对路径\n\n对于预设中的自定义文件（如转换器、脚本），使用相对路径：\n\n```json\n{\n  \"transformers\": [\n    {\n      \"path\": \"./transformers/custom.js\"\n    }\n  ],\n  \"StatusLine\": {\n    \"default\": {\n      \"modules\": [\n        {\n          \"type\": \"script\",\n          \"scriptPath\": \"./scripts/status.js\"\n        }\n      ]\n    }\n  }\n}\n```\n\n相对路径会在安装时自动转换为绝对路径。\n\n## 导出和导入\n\n### 导出当前配置\n\n```bash\nccr preset export my-preset\n```\n\n可选项：\n\n```bash\nccr preset export my-preset \\\n  --description \"我的预设\" \\\n  --author \"Your Name\" \\\n  --tags \"openai,production\"\n```\n\n### 安装预设\n\n**CLI 方式：**\n\n```bash\n# 从本地目录安装\nccr preset install /path/to/preset\n\n# 重新配置已安装的预设\nccr preset install my-preset\n```\n\n:::note 注意\nCLI 方式**不支持**从 URL 安装。如需从 GitHub 安装，请使用 Web UI 或先克隆到本地。\n:::\n\n**Web UI 方式：**\n\n1. 访问 Web UI：`ccr ui`\n2. 点击\"预设商城\"按钮\n3. 选择预设或输入 GitHub 仓库 URL\n4. 点击安装\n\n### 管理预设\n\n```bash\n# 列出所有预设\nccr preset list\n\n# 查看预设信息\nccr preset info my-preset\n\n# 删除预设\nccr preset delete my-preset\n```\n\n## 常见问题\n\n### Q: 如何处理多个 Provider？\n\nA: 在 template 中定义多个 Provider，使用条件逻辑：\n\n```json\n{\n  \"schema\": [\n    {\n      \"id\": \"useSecondary\",\n      \"type\": \"confirm\",\n      \"label\": \"启用备用 Provider\"\n    },\n    {\n      \"id\": \"secondaryKey\",\n      \"type\": \"password\",\n      \"label\": \"备用 API Key\",\n      \"when\": {\n        \"field\": \"useSecondary\",\n        \"operator\": \"eq\",\n        \"value\": true\n      }\n    }\n  ],\n  \"template\": {\n    \"Providers\": [\n      {\n        \"name\": \"primary\",\n        \"api_key\": \"#{primaryKey}\"\n      },\n      {\n        \"name\": \"secondary\",\n        \"api_key\": \"#{secondaryKey}\"\n      }\n    ]\n  },\n  \"configMappings\": [\n    {\n      \"target\": \"Providers\",\n      \"value\": [\n        {\n          \"name\": \"primary\",\n          \"api_key\": \"#{primaryKey}\"\n        }\n      ],\n      \"when\": {\n        \"field\": \"useSecondary\",\n        \"operator\": \"ne\",\n        \"value\": true\n      }\n    }\n  ]\n}\n```\n\n### Q: 如何支持条件配置？\n\nA: 使用 `when` 条件和 `configMappings`：\n\n```json\n{\n  \"configMappings\": [\n    {\n      \"target\": \"PROXY_URL\",\n      \"value\": \"#{proxyUrl}\",\n      \"when\": {\n        \"field\": \"useProxy\",\n        \"operator\": \"eq\",\n        \"value\": true\n      }\n    }\n  ]\n}\n```\n\n### Q: 如何验证用户输入？\n\nA: 使用 `validator` 字段：\n\n```json\n{\n  \"id\": \"url\",\n  \"type\": \"input\",\n  \"label\": \"API 地址\",\n  \"validator\": \"^https?://.+\"\n}\n```\n\n### Q: 如何创建多语言预设？\n\nA: 使用条件选择语言：\n\n```json\n{\n  \"schema\": [\n    {\n      \"id\": \"language\",\n      \"type\": \"select\",\n      \"label\": \"语言\",\n      \"options\": [\n        {\"label\": \"中文\", \"value\": \"zh\"},\n        {\"label\": \"English\", \"value\": \"en\"}\n      ]\n    }\n  ]\n}\n```\n\n## 相关文档\n\n- [预设配置使用指南](/zh/docs/advanced/presets)\n- [配置基础](/zh/docs/config/basic)\n- [Provider 配置](/zh/docs/config/providers)\n- [路由配置](/zh/docs/config/routing)\n- [转换器配置](/zh/docs/config/transformers)\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs.backup.20260101_205603/advanced/presets.md",
    "content": "---\nid: advanced/presets\ntitle: 预设配置\nsidebar_position: 3\n---\n\n# 预设配置\n\n使用预定义配置进行快速设置。\n\n## 什么是预设？\n\n预设是预配置的设置，包括针对特定用例优化的提供商配置、路由规则和转换器。\n\n## 使用预设\n\n### CLI 方式（命令行）\n\nCLI 方式适合开发者通过命令行快速操作。\n\n#### 安装预设\n\n**从本地目录安装：**\n\n```bash\nccr preset install /path/to/preset-directory\n```\n\n**重新配置已安装的预设：**\n\n```bash\nccr preset install my-preset\n```\n\n:::note 注意\nCLI 方式**不支持**从 URL 直接安装预设。如需从 GitHub 安装，请先克隆到本地或使用 Web UI。\n:::\n\n#### 使用预设\n\n安装预设后，可以使用预设名称启动 Claude Code：\n\n```bash\n# 使用指定预设启动\nccr my-preset \"your prompt\"\n\n# 后台任务使用预设\nccr my-preset --background \"your prompt\"\n```\n\n预设会：\n- 自动加载预配置的 Provider\n- 应用预设的路由规则\n- 使用预设中配置的 transformer\n\n#### 列出所有预设\n\n```bash\nccr preset list\n```\n\n此命令将显示所有已安装的预设及其名称、版本和描述。\n\n#### 查看预设信息\n\n```bash\nccr preset info my-preset\n```\n\n#### 删除预设\n\n```bash\nccr preset delete my-preset\n```\n\n### Web UI 方式\n\nWeb UI 提供更友好的可视化界面，支持更多安装方式。\n\n#### 访问 Web UI\n\n```bash\nccr ui\n```\n\n然后在浏览器中打开 `http://localhost:3000`\n\n#### 从 GitHub 仓库安装\n\n1. 点击\"预设商城\"按钮\n2. 在预设列表中选择要安装的预设\n3. 点击\"安装\"按钮\n\n或手动输入 GitHub 仓库地址：\n\n```\n格式：https://github.com/username/repo\n示例：https://github.com/example/ccr-presets\n```\n\n#### 重新配置预设\n\n1. 在预设列表中点击\"查看详情\"按钮\n2. 在详情页面中修改配置项\n3. 点击\"应用\"保存配置\n\n#### 管理预设\n\n- **查看**：点击预设右侧的信息图标\n- **删除**：点击预设右侧的删除图标\n\n## 创建自定义预设\n\n### 预设目录结构\n\n预设以目录形式存储，每个预设包含以下结构：\n\n```\n~/.claude-code-router/presets/<preset-name>/\n├── manifest.json           # 必填：预设配置文件\n├── transformers/           # 可选：自定义转换器\n│   └── custom-transformer.js\n├── scripts/               # 可选：自定义脚本\n│   └── status.js\n└── README.md              # 可选：说明文档\n```\n\n### 动态配置系统\n\nCCR 引入了强大的动态配置系统，支持：\n\n- **多种输入类型**：选择器、多选、确认框、文本输入、数字输入等\n- **条件逻辑**：根据用户输入动态显示/隐藏配置项\n- **变量引用**：配置项之间可以互相引用\n- **动态选项**：选项列表可以从预设配置或用户输入中动态生成\n\n#### Schema 字段类型\n\n| 类型 | 说明 | 示例 |\n|------|------|------|\n| `password` | 密码输入（隐藏显示） | API Key |\n| `input` | 单行文本输入 | Base URL |\n| `number` | 数字输入 | 最大Token数 |\n| `select` | 单选下拉框 | 选择Provider |\n| `multiselect` | 多选框 | 启用功能 |\n| `confirm` | 确认框 | 是否使用代理 |\n| `editor` | 多行文本编辑器 | 自定义配置 |\n\n#### 条件运算符\n\n| 运算符 | 说明 | 示例 |\n|--------|------|------|\n| `eq` | 等于 | `{\"field\": \"provider\", \"operator\": \"eq\", \"value\": \"openai\"}` |\n| `ne` | 不等于 | `{\"field\": \"advanced\", \"operator\": \"ne\", \"value\": true}` |\n| `in` | 包含于 | `{\"field\": \"feature\", \"operator\": \"in\", \"value\": [\"a\", \"b\"]}` |\n| `nin` | 不包含于 | `{\"field\": \"type\", \"operator\": \"nin\", \"value\": [\"x\", \"y\"]}` |\n| `exists` | 字段存在 | `{\"field\": \"apiKey\", \"operator\": \"exists\"}` |\n| `gt/lt/gte/lte` | 大于/小于/大于等于/小于等于 | 用于数字比较 |\n\n#### 动态选项类型\n\n##### static - 静态选项\n```json\n\"options\": {\n  \"type\": \"static\",\n  \"options\": [\n    {\"label\": \"选项1\", \"value\": \"value1\"},\n    {\"label\": \"选项2\", \"value\": \"value2\"}\n  ]\n}\n```\n\n##### providers - 从 Providers 配置提取\n```json\n\"options\": {\n  \"type\": \"providers\"\n}\n```\n自动从 `Providers` 数组中提取 name 作为选项。\n\n##### models - 从指定 Provider 的 models 提取\n```json\n\"options\": {\n  \"type\": \"models\",\n  \"providerField\": \"{{selectedProvider}}\"\n}\n```\n根据用户选择的 Provider，动态显示该 Provider 的 models。\n\n#### 模板变量\n\n使用 `{{变量名}}` 语法在 template 中引用用户输入：\n\n```json\n\"template\": {\n  \"Providers\": [\n    {\n      \"name\": \"{{providerName}}\",\n      \"api_key\": \"{{apiKey}}\"\n    }\n  ]\n}\n```\n\n#### 配置映射\n\n对于复杂的配置需求，使用 `configMappings` 精确控制值的位置：\n\n```json\n\"configMappings\": [\n  {\n    \"target\": \"Providers[0].api_key\",\n    \"value\": \"{{apiKey}}\"\n  },\n  {\n    \"target\": \"PROXY_URL\",\n    \"value\": \"{{proxyUrl}}\",\n    \"when\": {\n      \"field\": \"useProxy\",\n      \"operator\": \"eq\",\n      \"value\": true\n    }\n  }\n]\n```\n\n#### 完整示例\n\n```json\n{\n  \"name\": \"multi-provider-example\",\n  \"version\": \"1.0.0\",\n  \"description\": \"多Provider配置示例 - 支持OpenAI和DeepSeek切换\",\n  \"author\": \"CCR Team\",\n  \"keywords\": [\"openai\", \"deepseek\", \"multi-provider\"],\n  \"ccrVersion\": \"2.0.0\",\n  \"schema\": [\n    {\n      \"id\": \"primaryProvider\",\n      \"type\": \"select\",\n      \"label\": \"主要Provider\",\n      \"prompt\": \"选择您主要使用的LLM提供商\",\n      \"options\": {\n        \"type\": \"static\",\n        \"options\": [\n          {\n            \"label\": \"OpenAI\",\n            \"value\": \"openai\",\n            \"description\": \"使用OpenAI的GPT模型\"\n          },\n          {\n            \"label\": \"DeepSeek\",\n            \"value\": \"deepseek\",\n            \"description\": \"使用DeepSeek的高性价比模型\"\n          }\n        ]\n      },\n      \"required\": true,\n      \"defaultValue\": \"openai\"\n    },\n    {\n      \"id\": \"apiKey\",\n      \"type\": \"password\",\n      \"label\": \"API Key\",\n      \"prompt\": \"请输入您的API Key\",\n      \"placeholder\": \"sk-...\",\n      \"required\": true\n    },\n    {\n      \"id\": \"defaultModel\",\n      \"type\": \"select\",\n      \"label\": \"默认模型\",\n      \"prompt\": \"选择默认使用的模型\",\n      \"options\": {\n        \"type\": \"static\",\n        \"options\": [\n          {\"label\": \"GPT-4o\", \"value\": \"gpt-4o\"},\n          {\"label\": \"GPT-4o-mini\", \"value\": \"gpt-4o-mini\"}\n        ]\n      },\n      \"required\": true,\n      \"defaultValue\": \"gpt-4o\",\n      \"when\": {\n        \"field\": \"primaryProvider\",\n        \"operator\": \"eq\",\n        \"value\": \"openai\"\n      }\n    },\n    {\n      \"id\": \"enableProxy\",\n      \"type\": \"confirm\",\n      \"label\": \"启用代理\",\n      \"prompt\": \"是否通过代理访问API？\",\n      \"defaultValue\": false\n    },\n    {\n      \"id\": \"proxyUrl\",\n      \"type\": \"input\",\n      \"label\": \"代理地址\",\n      \"prompt\": \"输入代理服务器地址\",\n      \"placeholder\": \"http://127.0.0.1:7890\",\n      \"required\": true,\n      \"when\": {\n        \"field\": \"enableProxy\",\n        \"operator\": \"eq\",\n        \"value\": true\n      }\n    }\n  ],\n  \"template\": {\n    \"Providers\": [\n      {\n        \"name\": \"{{primaryProvider}}\",\n        \"api_base_url\": \"https://api.openai.com/v1\",\n        \"api_key\": \"{{apiKey}}\",\n        \"models\": [\"{{defaultModel}}\"]\n      }\n    ],\n    \"Router\": {\n      \"default\": \"{{primaryProvider}}/{{defaultModel}}\"\n    },\n    \"PROXY_URL\": \"{{proxyUrl}}\"\n  },\n  \"configMappings\": [\n    {\n      \"target\": \"PROXY_URL\",\n      \"value\": \"{{proxyUrl}}\",\n      \"when\": {\n        \"field\": \"enableProxy\",\n        \"operator\": \"eq\",\n        \"value\": true\n      }\n    }\n  ]\n}\n```\n\n### manifest.json 完整字段说明\n\n`manifest.json` 是预设的核心配置文件，使用 JSON5 格式（支持注释）。\n\n#### 1. 元数据字段（Metadata）\n\n这些字段用于描述预设的基本信息：\n\n| 字段 | 类型 | 必填 | 说明 |\n|------|------|------|------|\n| `name` | string | ✓ | 预设名称（唯一标识符） |\n| `version` | string | ✓ | 版本号（遵循 semver 规范） |\n| `description` | string | - | 预设描述 |\n| `author` | string | - | 作者信息 |\n| `homepage` | string | - | 项目主页 URL |\n| `repository` | string | - | 源代码仓库 URL |\n| `license` | string | - | 许可证类型 |\n| `keywords` | string[] | - | 关键词标签 |\n| `ccrVersion` | string | - | 兼容的 CCR 版本 |\n| `source` | string | - | 预设来源 URL |\n| `sourceType` | string | - | 来源类型（`local`/`gist`/`registry`） |\n| `checksum` | string | - | 内容校验和（SHA256） |\n\n示例：\n\n```json\n{\n  \"name\": \"my-preset\",\n  \"version\": \"1.0.0\",\n  \"description\": \"我的自定义预设\",\n  \"author\": \"Your Name\",\n  \"homepage\": \"https://github.com/yourname/ccr-presets\",\n  \"repository\": \"https://github.com/yourname/ccr-presets.git\",\n  \"license\": \"MIT\",\n  \"keywords\": [\"openai\", \"production\"],\n  \"ccrVersion\": \"2.0.0\"\n}\n```\n\n#### 2. 配置字段（Configuration）\n\n这些字段会直接合并到 CCR 的配置中，所有 `config.json` 支持的字段都可以在这里使用：\n\n| 字段 | 类型 | 说明 |\n|------|------|------|\n| `Providers` | array | Provider 配置数组 |\n| `Router` | object | 路由配置 |\n| `transformers` | array | 转换器配置 |\n| `StatusLine` | object | 状态栏配置 |\n\n示例：\n\n```json\n{\n  \"Providers\": [\n    {\n      \"name\": \"openai\",\n      \"api_base_url\": \"https://api.openai.com/v1\",\n      \"api_key\": \"${OPENAI_API_KEY}\",\n      \"models\": [\"gpt-4o\", \"gpt-4o-mini\"]\n    }\n  ],\n  \"Router\": {\n    \"default\": \"openai/gpt-4o\",\n    \"background\": \"openai/gpt-4o-mini\"\n  },\n  \"PORT\": 8080\n}\n```\n\n#### 3. 动态配置系统字段\n\n这些字段用于创建可交互的配置模板：\n\n| 字段 | 类型 | 说明 |\n|------|------|------|\n| `schema` | array | 配置输入表单定义 |\n| `template` | object | 配置模板（使用变量引用） |\n| `configMappings` | array | 配置映射规则 |\n| `userValues` | object | 用户填写的值（运行时使用） |\n| `requiredInputs` | array | 必填输入项列表（自动生成） |\n\n**schema 字段类型：**\n\n| 类型 | 说明 | 使用场景 |\n|------|------|----------|\n| `password` | 密码输入（隐藏） | API Key |\n| `input` | 单行文本输入 | URL |\n| `number` | 数字输入 | 端口号 |\n| `select` | 单选下拉框 | 选择 Provider |\n| `multiselect` | 多选框 | 启用功能 |\n| `confirm` | 确认框 | 是否启用 |\n| `editor` | 多行文本编辑器 | 自定义配置 |\n\n动态配置示例：\n\n```json\n{\n  \"schema\": [\n    {\n      \"id\": \"apiKey\",\n      \"type\": \"password\",\n      \"label\": \"API Key\",\n      \"prompt\": \"请输入您的 API Key\",\n      \"required\": true\n    },\n    {\n      \"id\": \"provider\",\n      \"type\": \"select\",\n      \"label\": \"Provider\",\n      \"options\": {\n        \"type\": \"static\",\n        \"options\": [\n          {\"label\": \"OpenAI\", \"value\": \"openai\"},\n          {\"label\": \"DeepSeek\", \"value\": \"deepseek\"}\n        ]\n      },\n      \"defaultValue\": \"openai\"\n    }\n  ],\n  \"template\": {\n    \"Providers\": [\n      {\n        \"name\": \"#{provider}\",\n        \"api_key\": \"#{apiKey}\"\n      }\n    ]\n  }\n}\n```\n\n### 创建预设示例\n\n#### 示例 1：简单预设（无动态配置）\n\n```bash\n# 创建预设目录\nmkdir -p ~/.claude-code-router/presets/simple-openai\n\n# 创建 manifest.json\ncat > ~/.claude-code-router/presets/simple-openai/manifest.json << 'EOF'\n{\n  \"name\": \"simple-openai\",\n  \"version\": \"1.0.0\",\n  \"description\": \"简单的 OpenAI 配置\",\n  \"author\": \"Your Name\",\n\n  \"Providers\": [\n    {\n      \"name\": \"openai\",\n      \"api_base_url\": \"https://api.openai.com/v1\",\n      \"api_key\": \"${OPENAI_API_KEY}\",\n      \"models\": [\"gpt-4o\", \"gpt-4o-mini\"]\n    }\n  ],\n\n  \"Router\": {\n    \"default\": \"openai/gpt-4o\",\n    \"background\": \"openai/gpt-4o-mini\"\n  },\n\n  \"requiredInputs\": [\n    {\n      \"id\": \"Providers[0].api_key\",\n      \"prompt\": \"Enter OpenAI API Key\",\n      \"placeholder\": \"OPENAI_API_KEY\"\n    }\n  ]\n}\nEOF\n\n# 配置预设（输入 API Key）\nccr preset install simple-openai\n\n# 使用预设\nccr simple-openai \"your prompt\"\n```\n\n#### 示例 2：高级预设（动态配置）\n\n```bash\n# 创建预设目录\nmkdir -p ~/.claude-code-router/presets/advanced-config\n\n# 创建 manifest.json\ncat > ~/.claude-code-router/presets/advanced-config/manifest.json << 'EOF'\n{\n  \"name\": \"advanced-config\",\n  \"version\": \"1.0.0\",\n  \"description\": \"支持多 Provider 选择的高级配置\",\n  \"author\": \"Your Name\",\n  \"keywords\": [\"openai\", \"deepseek\", \"multi-provider\"],\n\n  \"schema\": [\n    {\n      \"id\": \"provider\",\n      \"type\": \"select\",\n      \"label\": \"选择 Provider\",\n      \"prompt\": \"选择您主要使用的 LLM 提供商\",\n      \"options\": {\n        \"type\": \"static\",\n        \"options\": [\n          {\n            \"label\": \"OpenAI\",\n            \"value\": \"openai\",\n            \"description\": \"使用 OpenAI 的 GPT 模型\"\n          },\n          {\n            \"label\": \"DeepSeek\",\n            \"value\": \"deepseek\",\n            \"description\": \"使用 DeepSeek 的高性价比模型\"\n          }\n        ]\n      },\n      \"defaultValue\": \"openai\",\n      \"required\": true\n    },\n    {\n      \"id\": \"apiKey\",\n      \"type\": \"password\",\n      \"label\": \"API Key\",\n      \"prompt\": \"请输入您的 API Key\",\n      \"placeholder\": \"sk-...\",\n      \"required\": true\n    },\n    {\n      \"id\": \"enableProxy\",\n      \"type\": \"confirm\",\n      \"label\": \"启用代理\",\n      \"prompt\": \"是否通过代理访问 API？\",\n      \"defaultValue\": false\n    },\n    {\n      \"id\": \"proxyUrl\",\n      \"type\": \"input\",\n      \"label\": \"代理地址\",\n      \"prompt\": \"输入代理服务器地址\",\n      \"placeholder\": \"http://127.0.0.1:7890\",\n      \"required\": true,\n      \"when\": {\n        \"field\": \"enableProxy\",\n        \"operator\": \"eq\",\n        \"value\": true\n      }\n    }\n  ],\n\n  \"template\": {\n    \"Providers\": [\n      {\n        \"name\": \"#{provider}\",\n        \"api_base_url\": \"#{provider === 'openai' ? 'https://api.openai.com/v1' : 'https://api.deepseek.com'}\",\n        \"api_key\": \"#{apiKey}\",\n        \"models\": [\"gpt-4o\", \"gpt-4o-mini\"]\n      }\n    ],\n    \"Router\": {\n      \"default\": \"#{provider}/gpt-4o\",\n      \"background\": \"#{provider}/gpt-4o-mini\"\n    }\n  },\n\n  \"configMappings\": [\n    {\n      \"target\": \"PROXY_URL\",\n      \"value\": \"#{proxyUrl}\",\n      \"when\": {\n        \"field\": \"enableProxy\",\n        \"operator\": \"eq\",\n        \"value\": true\n      }\n    }\n  ]\n}\nEOF\n\n# 配置预设（会提示输入）\nccr preset install advanced-config\n\n# 使用预设\nccr advanced-config \"your prompt\"\n```\n\n### 导出当前配置为预设\n\n如果您已经配置好了 CCR，可以导出当前配置：\n\n```bash\n# 导出当前配置\nccr preset export my-exported-preset\n```\n\n导出时会自动：\n- 识别敏感字段（如 `api_key`）并替换为环境变量占位符\n- 生成 `schema` 用于收集用户输入\n- 生成 `template` 和 `configMappings`\n\n可选项：\n\n```bash\nccr preset export my-exported-preset \\\n  --description \"导出的配置\" \\\n  --author \"Your Name\" \\\n  --tags \"production,openai\"\n```\n\n:::tip 分享预设\n导出的预设目录可以直接分享给他人。接收者可以：\n- **CLI 方式**：将目录放到 `~/.claude-code-router/presets/`，然后运行 `ccr preset install 预设名`\n- **Web UI 方式**：将目录上传到 GitHub，然后通过仓库 URL 安装\n:::\n\n## 预设文件位置\n\n预设保存在：\n\n```\n~/.claude-code-router/presets/\n```\n\n每个预设都是一个目录，包含 `manifest.json` 文件。\n\n## 最佳实践\n\n1. **使用动态配置**：为需要用户输入的配置项使用schema系统\n2. **提供默认值**：为非必填项提供合理的默认值\n3. **条件显示**：使用when条件避免不必要的输入\n4. **清晰的标签**：为每个字段提供清晰的label和prompt\n5. **验证输入**：使用validator确保输入的有效性\n6. **版本控制**：将常用预设保存在版本控制中\n7. **文档化**：为自定义预设添加描述和版本信息\n\n## 下一步\n\n- [CLI 参考](/zh/docs/cli/start) - 完整的 CLI 命令参考\n- [配置](/zh/docs/config/basic) - 详细配置指南\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs.backup.20260101_205603/cli/commands/preset.md",
    "content": "---\nsidebar_position: 5\n---\n\n# ccr preset\n\n管理预设（Presets）——可共享和重用的配置模板。\n\n## 概述\n\n预设功能让您可以：\n- 将当前配置保存为可重用的模板\n- 与他人分享配置\n- 安装社区提供的预配置方案\n- 在不同配置之间轻松切换\n\n## 命令\n\n### export\n\n将当前配置导出为预设。\n\n```bash\nccr preset export <名称> [选项]\n```\n\n**选项：**\n- `--output <路径>` - 自定义输出目录路径\n- `--description <文本>` - 预设描述\n- `--author <名称>` - 预设作者\n- `--tags <标签>` - 逗号分隔的关键字\n- `--include-sensitive` - 包含 API 密钥等敏感数据（不推荐）\n\n**示例：**\n```bash\nccr preset export my-config --description \"我的生产环境配置\" --author \"您的名字\"\n```\n\n**执行过程：**\n1. 读取 `~/.claude-code-router/config.json` 中的当前配置\n2. 提示输入描述、作者和关键字（如未通过命令行提供）\n3. 自动清理敏感字段（API 密钥变为占位符）\n4. 在 `~/.claude-code-router/presets/<名称>/` 创建预设目录\n5. 生成包含配置和元数据的 `manifest.json`\n\n### install\n\n从本地目录安装预设。\n\n```bash\nccr preset install <来源>\n```\n\n**来源：**\n- 本地目录路径：`/path/to/preset-directory`\n- 预设名称（用于重新配置已安装的预设）：`preset-name`\n\n**示例：**\n```bash\n# 从目录安装\nccr preset install ./my-preset\n\n# 重新配置已安装的预设\nccr preset install my-preset\n```\n\n**执行过程：**\n1. 从预设目录读取 `manifest.json`\n2. 验证预设结构\n3. 如果预设包含 `schema`，提示输入必需的值（API 密钥等）\n4. 将预设复制到 `~/.claude-code-router/presets/<名称>/`\n5. 在 `manifest.json` 中保存用户输入\n\n**注意：** 目前不支持从 URL 安装。请先下载预设目录。\n\n### list\n\n列出所有已安装的预设。\n\n```bash\nccr preset list\n```\n\n**示例输出：**\n```\nAvailable presets:\n\n• my-config (v1.0.0)\n  My production setup\n  by Your Name\n\n• openai-setup\n  Basic OpenAI configuration\n```\n\n### info\n\n显示预设的详细信息。\n\n```bash\nccr preset info <名称>\n```\n\n**显示内容：**\n- 版本、描述、作者、关键字\n- 配置摘要（Providers、Router 规则）\n- 必需输入（如果有）\n\n**示例：**\n```bash\nccr preset info my-config\n```\n\n### delete / rm / remove\n\n删除已安装的预设。\n\n```bash\nccr preset delete <名称>\nccr preset rm <名称>\nccr preset remove <名称>\n```\n\n**示例：**\n```bash\nccr preset delete my-config\n```\n\n## 预设结构\n\n预设是一个包含 `manifest.json` 文件的目录：\n\n```json\n{\n  \"name\": \"my-preset\",\n  \"version\": \"1.0.0\",\n  \"description\": \"我的配置\",\n  \"author\": \"作者姓名\",\n  \"keywords\": [\"openai\", \"production\"],\n\n  \"Providers\": [\n    {\n      \"name\": \"openai\",\n      \"api_base_url\": \"https://api.openai.com/v1\",\n      \"api_key\": \"{{apiKey}}\",\n      \"models\": [\"gpt-4\", \"gpt-3.5-turbo\"]\n    }\n  ],\n\n  \"Router\": {\n    \"default\": \"openai:gpt-4\"\n  },\n\n  \"schema\": [\n    {\n      \"id\": \"apiKey\",\n      \"type\": \"password\",\n      \"label\": \"OpenAI API 密钥\",\n      \"prompt\": \"请输入您的 OpenAI API 密钥\"\n    }\n  ]\n}\n```\n\n### Schema 系统\n\n`schema` 字段定义用户在安装时必须提供的输入：\n\n**字段类型：**\n- `password` - 隐藏输入（用于 API 密钥）\n- `input` - 文本输入\n- `select` - 单选下拉框\n- `multiselect` - 多选下拉框\n- `confirm` - 是/否确认\n- `editor` - 多行文本编辑器\n- `number` - 数字输入\n\n**动态选项：**\n```json\n{\n  \"id\": \"provider\",\n  \"type\": \"select\",\n  \"label\": \"选择提供商\",\n  \"options\": {\n    \"type\": \"providers\"\n  }\n}\n```\n\n**条件显示：**\n```json\n{\n  \"id\": \"model\",\n  \"type\": \"select\",\n  \"label\": \"选择模型\",\n  \"when\": {\n    \"field\": \"provider\",\n    \"operator\": \"exists\"\n  },\n  \"options\": {\n    \"type\": \"models\",\n    \"providerField\": \"#{selectedProvider}\"\n  }\n}\n```\n\n## 分享预设\n\n分享预设的步骤：\n\n1. **导出配置：**\n   ```bash\n   ccr preset export my-preset\n   ```\n\n2. **分享目录：**\n   ```bash\n   ~/.claude-code-router/presets/my-preset/\n   ```\n\n3. **分发方式：**\n   - 上传到 GitHub 仓库\n   - 创建 GitHub Gist\n   - 打包为 zip 文件分享\n   - 发布到 npm（未来功能）\n\n4. **用户安装：**\n   ```bash\n   ccr preset install /path/to/my-preset\n   ```\n\n## 安全性\n\n### 自动清理\n\n默认情况下，`export` 会清理敏感字段：\n- 名为 `api_key`、`apikey`、`password`、`secret` 的字段会被替换为 `{{字段名}}` 占位符\n- 这些占位符会成为 schema 中的必需输入\n- 用户在安装时会被提示提供自己的值\n\n### 包含敏感数据\n\n要包含实际值（不推荐）：\n```bash\nccr preset export my-preset --include-sensitive\n```\n\n**警告：** 永远不要分享包含敏感数据的预设！\n\n## 相关文档\n\n- [配置指南](/zh/docs/cli/config/basic) - 基础配置\n- [项目级配置](/zh/docs/cli/config/project-level) - 项目特定设置\n- [服务器：预设](/zh/docs/server/advanced/presets) - 高级预设主题\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs.backup.20260101_205603/cli/commands/statusline.md",
    "content": "---\nid: cli/commands/statusline\ntitle: ccr statusline\nsidebar_position: 5\n---\n\n# ccr statusline\n\n显示可自定义的状态栏，实时展示 Claude Code 会话信息，包括工作区、Git 分支、模型、token 使用情况等。\n\n## 概述\n\n`ccr statusline` 命令从 stdin 读取 JSON 数据，并在终端中渲染格式精美的状态栏。它设计用于与 Claude Code 的 hook 系统集成，以显示实时会话信息。\n\n## 使用方法\n\n### 基本用法\n\n```bash\nccr statusline\n```\n\n该命令期望通过 stdin 接收 JSON 数据，通常通过管道从 Claude Code hook 传递：\n\n```bash\necho '{\"hook_event_name\":\"...\",\"session_id\":\"...\",\"...\"}' | ccr statusline\n```\n\n### Hook 集成\n\n在您的 Claude Code 设置中配置：\n\n```json\n{\n  \"hooks\": {\n    \"postResponse\": {\n      \"command\": \"ccr statusline\",\n      \"input\": \"json\"\n    }\n  }\n}\n```\n\n## 可用主题\n\n### 默认主题\n\n简洁优雅的主题，使用 Nerd Font 图标和彩色文本：\n\n```\n 󰉋 my-project   main  󰚩 claude-3-5-sonnet-20241022  ↑ 12.3k  ↓ 5.2k\n```\n\n### Powerline 主题\n\nvim-powerline 风格，带背景色和箭头分隔符：\n\n```\n 󰉋 my-project   main  󰚩 claude-3-5-sonnet-20241022  ↑ 12.3k  ↓ 5.2k\n```\n\n通过在配置中设置 `currentStyle: \"powerline\"` 激活。\n\n### 简单主题\n\n回退主题，不带图标，适用于不支持 Nerd Font 的终端：\n\n```\nmy-project  main  claude-3-5-sonnet-20241022  ↑ 12.3k  ↓ 5.2k\n```\n\n当 `USE_SIMPLE_ICONS=true` 或在不支持的终端上自动使用。\n\n## 可用模块\n\n状态栏模块显示不同类型的信息：\n\n| 模块 | 说明 | 变量 |\n|------|------|------|\n| **workDir** | 当前工作目录名称 | `{{workDirName}}` |\n| **gitBranch** | 当前 Git 分支 | `{{gitBranch}}` |\n| **model** | 使用的模型 | `{{model}}` |\n| **usage** | Token 使用情况（输入/输出） | `{{inputTokens}}`, `{{outputTokens}}` |\n| **context** | 上下文窗口使用情况 | `{{contextPercent}}`, `{{contextWindowSize}}` |\n| **speed** | Token 处理速度 | `{{tokenSpeed}}`, `{{isStreaming}}` |\n| **cost** | API 成本 | `{{cost}}` |\n| **duration** | 会话持续时间 | `{{duration}}` |\n| **lines** | 代码变更 | `{{linesAdded}}`, `{{linesRemoved}}` |\n| **script** | 自定义脚本输出 | 动态 |\n\n## 配置\n\n在 `~/.claude-code-router/config.json` 中配置 statusline：\n\n### 默认样式示例\n\n```json\n{\n  \"StatusLine\": {\n    \"currentStyle\": \"default\",\n    \"default\": {\n      \"modules\": [\n        {\n          \"type\": \"workDir\",\n          \"icon\": \"󰉋\",\n          \"text\": \"{{workDirName}}\",\n          \"color\": \"bright_blue\"\n        },\n        {\n          \"type\": \"gitBranch\",\n          \"icon\": \"\",\n          \"text\": \"{{gitBranch}}\",\n          \"color\": \"bright_magenta\"\n        },\n        {\n          \"type\": \"model\",\n          \"icon\": \"󰚩\",\n          \"text\": \"{{model}}\",\n          \"color\": \"bright_cyan\"\n        },\n        {\n          \"type\": \"usage\",\n          \"icon\": \"↑\",\n          \"text\": \"{{inputTokens}}\",\n          \"color\": \"bright_green\"\n        },\n        {\n          \"type\": \"usage\",\n          \"icon\": \"↓\",\n          \"text\": \"{{outputTokens}}\",\n          \"color\": \"bright_yellow\"\n        }\n      ]\n    }\n  }\n}\n```\n\n### Powerline 样式示例\n\n```json\n{\n  \"StatusLine\": {\n    \"currentStyle\": \"powerline\",\n    \"powerline\": {\n      \"modules\": [\n        {\n          \"type\": \"workDir\",\n          \"icon\": \"󰉋\",\n          \"text\": \"{{workDirName}}\",\n          \"color\": \"white\",\n          \"background\": \"bg_bright_blue\"\n        },\n        {\n          \"type\": \"gitBranch\",\n          \"icon\": \"\",\n          \"text\": \"{{gitBranch}}\",\n          \"color\": \"white\",\n          \"background\": \"bg_bright_magenta\"\n        }\n      ]\n    }\n  }\n}\n```\n\n### 完整功能示例\n\n```json\n{\n  \"StatusLine\": {\n    \"currentStyle\": \"default\",\n    \"default\": {\n      \"modules\": [\n        {\n          \"type\": \"workDir\",\n          \"icon\": \"󰉋\",\n          \"text\": \"{{workDirName}}\",\n          \"color\": \"bright_blue\"\n        },\n        {\n          \"type\": \"gitBranch\",\n          \"icon\": \"\",\n          \"text\": \"{{gitBranch}}\",\n          \"color\": \"bright_magenta\"\n        },\n        {\n          \"type\": \"model\",\n          \"icon\": \"󰚩\",\n          \"text\": \"{{model}}\",\n          \"color\": \"bright_cyan\"\n        },\n        {\n          \"type\": \"context\",\n          \"icon\": \"🪟\",\n          \"text\": \"{{contextPercent}}% / {{contextWindowSize}}\",\n          \"color\": \"bright_green\"\n        },\n        {\n          \"type\": \"speed\",\n          \"icon\": \"⚡\",\n          \"text\": \"{{tokenSpeed}} t/s {{isStreaming}}\",\n          \"color\": \"bright_yellow\"\n        },\n        {\n          \"type\": \"cost\",\n          \"icon\": \"💰\",\n          \"text\": \"{{cost}}\",\n          \"color\": \"bright_magenta\"\n        },\n        {\n          \"type\": \"duration\",\n          \"icon\": \"⏱️\",\n          \"text\": \"{{duration}}\",\n          \"color\": \"bright_white\"\n        },\n        {\n          \"type\": \"lines\",\n          \"icon\": \"📝\",\n          \"text\": \"+{{linesAdded}}/-{{linesRemoved}}\",\n          \"color\": \"bright_cyan\"\n        }\n      ]\n    }\n  }\n}\n```\n\n## 自定义脚本\n\n您可以通过执行脚本创建自定义模块：\n\n```json\n{\n  \"type\": \"script\",\n  \"icon\": \"🔧\",\n  \"scriptPath\": \"/path/to/script.js\",\n  \"options\": {\n    \"customOption\": \"value\"\n  }\n}\n```\n\n脚本格式（CommonJS）：\n\n```javascript\n// my-status-module.js\nmodule.exports = function(variables, options) {\n  // 访问变量如 model、gitBranch 等\n  // 从配置中访问选项\n  return `Custom: ${variables.model}`;\n};\n\n// 或异步\nmodule.exports = async function(variables, options) {\n  const data = await fetchSomeData();\n  return data;\n};\n```\n\n## 颜色选项\n\n### 标准颜色\n\n- `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`\n- `bright_black`, `bright_red`, `bright_green`, `bright_yellow`, `bright_blue`, `bright_magenta`, `bright_cyan`, `bright_white`\n\n### 背景颜色\n\n添加前缀 `bg_`：`bg_blue`, `bg_bright_red` 等。\n\n### 十六进制颜色\n\n使用 24 位 TrueColor 和十六进制代码：\n\n```json\n{\n  \"color\": \"#FF5733\",\n  \"background\": \"bg_#1E90FF\"\n}\n```\n\n## 可用变量\n\n所有变量都可以在模块文本中使用 `{{variableName}}` 访问：\n\n| 变量 | 说明 | 示例 |\n|------|------|------|\n| `{{workDirName}}` | 当前目录名称 | `my-project` |\n| `{{gitBranch}}` | Git 分支名称 | `main` |\n| `{{model}}` | 模型名称 | `claude-3-5-sonnet-20241022` |\n| `{{inputTokens}}` | 输入 tokens（格式化） | `12.3k` |\n| `{{outputTokens}}` | 输出 tokens（格式化） | `5.2k` |\n| `{{tokenSpeed}}` | 每秒 tokens 数 | `45` |\n| `{{isStreaming}}` | 流式传输状态 | `streaming` 或空 |\n| `{{contextPercent}}` | 上下文使用百分比 | `45` |\n| `{{contextWindowSize}}` | 总上下文窗口 | `200k` |\n| `{{cost}}` | 总成本 | `$0.15` |\n| `{{duration}}` | 会话持续时间 | `2m34s` |\n| `{{linesAdded}}` | 添加的行数 | `150` |\n| `{{linesRemoved}}` | 删除的行数 | `25` |\n| `{{sessionId}}` | 会话 ID（前 8 个字符） | `a1b2c3d4` |\n\n## 环境变量\n\n使用环境变量控制行为：\n\n| 变量 | 值 | 说明 |\n|------|------|------|\n| `USE_SIMPLE_ICONS` | `true`/`false` | 强制使用不带图标的简单主题 |\n| `NERD_FONT` | 任意值 | 自动检测 Nerd Font 支持 |\n\n## 示例\n\n### 极简状态栏\n\n```json\n{\n  \"StatusLine\": {\n    \"default\": {\n      \"modules\": [\n        {\n          \"type\": \"model\",\n          \"text\": \"{{model}}\"\n        },\n        {\n          \"type\": \"usage\",\n          \"text\": \"↑{{inputTokens}} ↓{{outputTokens}}\"\n        }\n      ]\n    }\n  }\n}\n```\n\n输出：`claude-3-5-sonnet-20241022 ↑12.3k ↓5.2k`\n\n### 开发者生产力重点\n\n```json\n{\n  \"StatusLine\": {\n    \"default\": {\n      \"modules\": [\n        {\n          \"type\": \"gitBranch\",\n          \"icon\": \"\",\n          \"text\": \"{{gitBranch}}\",\n          \"color\": \"bright_magenta\"\n        },\n        {\n          \"type\": \"lines\",\n          \"icon\": \"📝\",\n          \"text\": \"+{{linesAdded}}/-{{linesRemoved}}\",\n          \"color\": \"bright_cyan\"\n        },\n        {\n          \"type\": \"duration\",\n          \"icon\": \"⏱️\",\n          \"text\": \"{{duration}}\",\n          \"color\": \"bright_white\"\n        }\n      ]\n    }\n  }\n}\n```\n\n输出：` feature/auth  📝 +150/-25  ⏱️ 2m34s`\n\n## Preset 集成\n\nStatusline 主题可以包含在 presets 中。当您安装带有 statusline 配置的 preset 时，激活该 preset 时会自动应用。\n\n查看 [Presets](/docs/server/advanced/presets) 了解更多信息。\n\n## 故障排除\n\n### 图标不显示\n\n在环境中设置 `USE_SIMPLE_ICONS=true`：\n\n```bash\nexport USE_SIMPLE_ICONS=true\n```\n\n### 颜色不工作\n\n确保您的终端支持 TrueColor（24 位颜色）：\n\n```bash\nexport COLORTERM=truecolor\n```\n\n### Git 分支不显示\n\n确保您在 Git 仓库中并安装了 `git` 命令。\n\n## 相关命令\n\n- [ccr status](/docs/cli/commands/status) - 检查服务状态\n- [ccr preset](/docs/cli/commands/preset) - 管理带 statusline 主题的 presets\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs.backup.20260101_205603/cli/config/basic.md",
    "content": "# CLI 基础配置\n\nCLI 使用与 Server 相同的配置文件：`~/.claude-code-router/config.json`\n\n## 配置文件位置\n\n```bash\n~/.claude-code-router/config.json\n```\n\n## 快速配置\n\n使用交互式命令配置：\n\n```bash\nccr model\n```\n\n这将引导你完成：\n1. 选择 LLM 提供商\n2. 配置 API Key\n3. 选择模型\n4. 设置路由规则\n\n## 手动配置\n\n### 编辑配置文件\n\n```bash\n# 打开配置文件\nnano ~/.claude-code-router/config.json\n```\n\n### 最小配置示例\n\n```json5\n{\n  // API 密钥（可选，用于保护服务）\n  \"APIKEY\": \"your-api-key-here\",\n\n  // LLM 提供商\n  \"Providers\": [\n    {\n      \"name\": \"openai\",\n      \"baseUrl\": \"https://api.openai.com/v1\",\n      \"apiKey\": \"$OPENAI_API_KEY\",\n      \"models\": [\"gpt-4\", \"gpt-3.5-turbo\"]\n    }\n  ],\n\n  // 默认路由\n  \"Router\": {\n    \"default\": \"openai,gpt-4\"\n  }\n}\n```\n\n## 环境变量\n\n配置支持环境变量插值：\n\n```json5\n{\n  \"Providers\": [\n    {\n      \"apiKey\": \"$OPENAI_API_KEY\"  // 从环境变量读取\n    }\n  ]\n}\n```\n\n在 `.bashrc` 或 `.zshrc` 中设置：\n\n```bash\nexport OPENAI_API_KEY=\"sk-...\"\nexport ANTHROPIC_API_KEY=\"sk-ant-...\"\n```\n\n## 常用配置项\n\n### HOST 和 PORT\n\n```json5\n{\n  \"HOST\": \"127.0.0.1\",  // 监听地址\n  \"PORT\": 3456          // 监听端口\n}\n```\n\n### 日志配置\n\n```json5\n{\n  \"LOG\": true,          // 启用日志\n  \"LOG_LEVEL\": \"info\"   // 日志级别\n}\n```\n\n### 路由配置\n\n```json5\n{\n  \"Router\": {\n    \"default\": \"openai,gpt-4\",\n    \"background\": \"openai,gpt-3.5-turbo\",\n    \"think\": \"openai,gpt-4\",\n    \"longContext\": \"anthropic,claude-3-opus\"\n  }\n}\n```\n\n## 配置验证\n\n配置文件会自动验证。常见错误：\n\n- **缺少 Providers**：必须至少配置一个提供商\n- **API Key 缺失**：如果配置了 Providers，必须提供 API Key\n- **模型不存在**：确保模型在提供商的 models 列表中\n\n## 配置备份\n\n每次更新配置时会自动备份：\n\n```\n~/.claude-code-router/config.backup.{timestamp}.json\n```\n\n## 重新加载配置\n\n修改配置后需要重启服务：\n\n```bash\nccr restart\n```\n\n## 查看当前配置\n\n```bash\n# 通过 API 查看\ncurl http://localhost:3456/api/config\n\n# 或查看配置文件\ncat ~/.claude-code-router/config.json\n```\n\n## 示例配置\n\n### OpenAI\n\n```json5\n{\n  \"Providers\": [\n    {\n      \"name\": \"openai\",\n      \"baseUrl\": \"https://api.openai.com/v1\",\n      \"apiKey\": \"$OPENAI_API_KEY\",\n      \"models\": [\"gpt-4\", \"gpt-3.5-turbo\"]\n    }\n  ],\n  \"Router\": {\n    \"default\": \"openai,gpt-4\"\n  }\n}\n```\n\n### Anthropic\n\n```json5\n{\n  \"Providers\": [\n    {\n      \"name\": \"anthropic\",\n      \"baseUrl\": \"https://api.anthropic.com/v1\",\n      \"apiKey\": \"$ANTHROPIC_API_KEY\",\n      \"models\": [\"claude-3-5-sonnet-20241022\", \"claude-3-opus-20240229\"]\n    }\n  ],\n  \"Router\": {\n    \"default\": \"anthropic,claude-3-5-sonnet-20241022\"\n  }\n}\n```\n\n### 多提供商\n\n```json5\n{\n  \"Providers\": [\n    {\n      \"name\": \"openai\",\n      \"baseUrl\": \"https://api.openai.com/v1\",\n      \"apiKey\": \"$OPENAI_API_KEY\",\n      \"models\": [\"gpt-4\", \"gpt-3.5-turbo\"]\n    },\n    {\n      \"name\": \"anthropic\",\n      \"baseUrl\": \"https://api.anthropic.com/v1\",\n      \"apiKey\": \"$ANTHROPIC_API_KEY\",\n      \"models\": [\"claude-3-5-sonnet-20241022\", \"claude-3-opus-20240229\"]\n    }\n  ],\n  \"Router\": {\n    \"default\": \"openai,gpt-4\",\n    \"think\": \"anthropic,claude-3-5-sonnet-20241022\",\n    \"background\": \"openai,gpt-3.5-turbo\"\n  }\n}\n```\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs.backup.20260101_205603/cli/config/project-level.md",
    "content": "# 项目级配置\n\n除了全局配置，`ccr` 还支持为特定项目设置不同的路由规则。\n\n## 项目配置文件\n\n项目配置文件位于：\n\n```\n~/.claude/projects/<project-id>/claude-code-router.json\n```\n\n其中 `<project-id>` 是 Claude Code 项目的唯一标识符。\n\n## 项目配置结构\n\n```json5\n{\n  \"Router\": {\n    \"default\": \"openai,gpt-4\",\n    \"background\": \"openai,gpt-3.5-turbo\"\n  }\n}\n```\n\n## 查找项目 ID\n\n### 方法一：使用 CLI\n\n```bash\n# 在项目目录中运行\nccr status\n```\n\n输出会显示当前项目 ID：\n\n```\nProject: my-project (abc123def456)\n```\n\n### 方法二：查看 Claude Code 配置\n\n```bash\ncat ~/.claude.json\n```\n\n找到你的项目 ID：\n\n```json\n{\n  \"projects\": {\n    \"abc123def456\": {\n      \"path\": \"/path/to/your/project\",\n      \"name\": \"my-project\"\n    }\n  }\n}\n```\n\n## 创建项目配置\n\n### 手动创建\n\n```bash\n# 创建项目配置目录\nmkdir -p ~/.claude/projects/abc123def456\n\n# 创建配置文件\ncat > ~/.claude/projects/abc123def456/claude-code-router.json << 'EOF'\n{\n  \"Router\": {\n    \"default\": \"anthropic,claude-3-5-sonnet-20241022\",\n    \"background\": \"openai,gpt-3.5-turbo\"\n  }\n}\nEOF\n```\n\n### 使用 ccr model 命令\n\n```bash\n# 在项目目录中运行\ncd /path/to/your/project\nccr model --project\n```\n\n## 配置优先级\n\n路由配置的优先级（从高到低）：\n\n1. **自定义路由函数** (`CUSTOM_ROUTER_PATH`)\n2. **项目级配置** (`~/.claude/projects/<id>/claude-code-router.json`)\n3. **全局配置** (`~/.claude-code-router/config.json`)\n4. **内置路由规则**\n\n## 使用场景\n\n### 场景一：不同项目使用不同模型\n\n```json5\n// Web 项目使用 GPT-4\n~/.claude/projects/web-project-id/claude-code-router.json:\n{\n  \"Router\": {\n    \"default\": \"openai,gpt-4\"\n  }\n}\n\n// AI 项目使用 Claude\n~/.claude/projects/ai-project-id/claude-code-router.json:\n{\n  \"Router\": {\n    \"default\": \"anthropic,claude-3-5-sonnet-20241022\"\n  }\n}\n```\n\n### 场景二：测试项目使用低成本模型\n\n```json5\n~/.claude/projects/test-project-id/claude-code-router.json:\n{\n  \"Router\": {\n    \"default\": \"openai,gpt-3.5-turbo\",\n    \"background\": \"openai,gpt-3.5-turbo\"\n  }\n}\n```\n\n### 场景三：长上下文项目\n\n```json5\n~/.claude/projects/long-context-project-id/claude-code-router.json:\n{\n  \"Router\": {\n    \"default\": \"anthropic,claude-3-opus-20240229\",\n    \"longContext\": \"anthropic,claude-3-opus-20240229\"\n  }\n}\n```\n\n## 验证项目配置\n\n```bash\n# 查看当前项目使用的路由\nccr status\n\n# 查看日志确认路由决策\ntail -f ~/.claude-code-router/claude-code-router.log\n```\n\n## 删除项目配置\n\n```bash\nrm ~/.claude/projects/<project-id>/claude-code-router.json\n```\n\n删除后会回退到全局配置。\n\n## 完整示例\n\n假设你有两个项目：\n\n### 全局配置（`~/.claude-code-router/config.json`）\n\n```json5\n{\n  \"Providers\": [\n    {\n      \"name\": \"openai\",\n      \"baseUrl\": \"https://api.openai.com/v1\",\n      \"apiKey\": \"$OPENAI_API_KEY\",\n      \"models\": [\"gpt-4\", \"gpt-3.5-turbo\"]\n    },\n    {\n      \"name\": \"anthropic\",\n      \"baseUrl\": \"https://api.anthropic.com/v1\",\n      \"apiKey\": \"$ANTHROPIC_API_KEY\",\n      \"models\": [\"claude-3-5-sonnet-20241022\"]\n    }\n  ],\n  \"Router\": {\n    \"default\": \"openai,gpt-4\",\n    \"background\": \"openai,gpt-3.5-turbo\"\n  }\n}\n```\n\n### Web 项目配置\n\n```json5\n{\n  \"Router\": {\n    \"default\": \"openai,gpt-4\"\n  }\n}\n```\n\n### AI 项目配置\n\n```json5\n{\n  \"Router\": {\n    \"default\": \"anthropic,claude-3-5-sonnet-20241022\",\n    \"think\": \"anthropic,claude-3-5-sonnet-20241022\"\n  }\n}\n```\n\n这样：\n- Web 项目使用 GPT-4\n- AI 项目使用 Claude\n- 所有项目的后台任务使用 GPT-3.5-turbo（继承全局配置）\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs.backup.20260101_205603/cli/intro.md",
    "content": "# CLI 简介\n\nClaude Code Router CLI (`ccr`) 是一个命令行工具，用于管理和控制 Claude Code Router 服务。\n\n## 功能概述\n\n`ccr` 提供以下功能：\n\n- **服务管理**：启动、停止、重启服务\n- **配置管理**：交互式配置模型选择\n- **状态查看**：查看服务运行状态\n- **代码执行**：直接执行 `claude` 命令\n- **环境集成**：输出环境变量用于 shell 集成\n- **Web UI**：打开 Web 管理界面\n- **状态栏**：使用 `ccr statusline` 显示自定义会话状态\n\n## 安装\n\n```bash\nnpm install -g @musistudio/claude-code-router\n```\n\n或使用项目别名：\n\n```bash\nnpm install -g claude-code-router\n```\n\n## 基本使用\n\n### 启动服务\n\n```bash\nccr start\n```\n\n### 查看状态\n\n```bash\nccr status\n```\n\n### 停止服务\n\n```bash\nccr stop\n```\n\n### 查看模型\n\n```bash\nccr model\n```\n\n## 与 Claude Code 集成\n\n`ccr` 可以与 Claude Code 无缝集成，将请求路由到你选择的 LLM 提供商。\n\n### 方式一：设置 API 地址\n\n```bash\nexport ANTHROPIC_BASE_URL=\"http://localhost:3456/v1\"\nexport ANTHROPIC_API_KEY=\"your-api-key\"\n```\n\n### 方式二：使用 activate 命令\n\n```bash\neval \"$(ccr activate)\"\n```\n\n## 配置文件\n\n`ccr` 使用与 Server 相同的配置文件：`~/.claude-code-router/config.json`\n\n配置一次，CLI 和 Server 都会使用。\n\n## 下一步\n\n- [安装指南](/docs/cli/installation) - 详细安装说明\n- [快速开始](/docs/cli/quick-start) - 5 分钟上手\n- [命令参考](/docs/category/cli-commands) - 完整命令列表\n- [状态栏](/docs/cli/commands/statusline) - 自定义状态栏\n- [配置说明](/docs/category/cli-config) - 配置文件详解\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs.backup.20260101_205603/cli/model.md",
    "content": "---\nid: cli/model\ntitle: ccr model\nsidebar_position: 2\n---\n\n# ccr model\n\n交互式模型选择和配置。\n\n## 用法\n\n```bash\nccr model [命令]\n```\n\n## 命令\n\n### 选择模型\n\n交互式选择模型：\n\n```bash\nccr model\n```\n\n这将显示一个包含可用提供商和模型的交互式菜单。\n\n### 设置默认模型\n\n直接设置默认模型：\n\n```bash\nccr model set <provider>,<model>\n```\n\n示例：\n\n```bash\nccr model set deepseek,deepseek-chat\n```\n\n### 列出模型\n\n列出所有配置的模型：\n\n```bash\nccr model list\n```\n\n### 添加模型\n\n添加新模型到配置：\n\n```bash\nccr model add <provider>,<model>\n```\n\n示例：\n\n```bash\nccr model add groq,llama-3.3-70b-versatile\n```\n\n### 删除模型\n\n从配置中删除模型：\n\n```bash\nccr model remove <provider>,<model>\n```\n\n## 示例\n\n### 交互式选择\n\n```bash\n$ ccr model\n\n? 选择一个提供商: deepseek\n? 选择一个模型: deepseek-chat\n\n默认模型设置为: deepseek,deepseek-chat\n```\n\n### 直接配置\n\n```bash\nccr model set deepseek,deepseek-chat\n```\n\n### 查看当前配置\n\n```bash\nccr model list\n```\n\n输出：\n\n```\n已配置的模型:\n  deepseek,deepseek-chat (默认)\n  groq,llama-3.3-70b-versatile\n  gemini,gemini-2.5-pro\n```\n\n## 交互式功能\n\n`ccr model` 命令提供以下功能：\n\n1. **查看当前配置**：查看所有已配置的模型和路由器设置\n2. **切换模型**：快速更改每个路由器类型使用的模型\n3. **添加新模型**：向现有提供商添加模型\n4. **创建新提供商**：设置完整的提供商配置，包括：\n   - 提供商名称和 API 端点\n   - API 密钥\n   - 可用模型\n   - 转换器配置，支持：\n     - 多个转换器（openrouter、deepseek、gemini 等）\n     - 转换器选项（例如，带自定义限制的 maxtoken）\n     - 提供商特定路由（例如，OpenRouter 提供商偏好）\n\nCLI 工具会验证所有输入并提供有用的提示来引导您完成配置过程，使管理复杂设置变得容易，无需手动编辑 JSON 文件。\n\n## 相关命令\n\n- [ccr start](/zh/docs/cli/start) - 启动服务器\n- [ccr config](/zh/docs/cli/other-commands#ccr-config) - 编辑配置\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs.backup.20260101_205603/cli/other-commands.md",
    "content": "---\nid: cli/other-commands\ntitle: 其他命令\nsidebar_position: 4\n---\n\n# 其他命令\n\n管理 Claude Code Router 的其他 CLI 命令。\n\n## ccr stop\n\n停止运行中的服务器。\n\n```bash\nccr stop\n```\n\n## ccr restart\n\n重启服务器。\n\n```bash\nccr restart\n```\n\n## ccr code\n\n通过路由器执行 claude 命令。\n\n```bash\nccr code [参数...]\n```\n\n## ccr ui\n\n在浏览器中打开 Web UI。\n\n```bash\nccr ui\n```\n\n## ccr activate\n\n输出用于与外部工具集成的 shell 环境变量。\n\n```bash\nccr activate\n```\n\n## 全局选项\n\n这些选项可用于任何命令：\n\n| 选项 | 说明 |\n|------|------|\n| `-h, --help` | 显示帮助 |\n| `-v, --version` | 显示版本号 |\n| `--config <路径>` | 配置文件路径 |\n| `--verbose` | 启用详细输出 |\n\n## 示例\n\n### 停止服务器\n\n```bash\nccr stop\n```\n\n### 使用自定义配置重启\n\n```bash\nccr restart --config /path/to/config.json\n```\n\n### 打开 Web UI\n\n```bash\nccr ui\n```\n\n## 相关文档\n\n- [入门](/zh/docs/intro) - Claude Code Router 简介\n- [配置](/zh/docs/config/basic) - 配置指南\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs.backup.20260101_205603/cli/start.md",
    "content": "---\nid: cli/start\ntitle: ccr start\nsidebar_position: 1\n---\n\n# ccr start\n\n启动 Claude Code Router 服务器。\n\n## 用法\n\n```bash\nccr start [选项]\n```\n\n## 选项\n\n| 选项 | 别名 | 说明 |\n|------|------|------|\n| `--port <number>` | `-p` | 监听端口号（默认：3456） |\n| `--config <path>` | `-c` | 配置文件路径 |\n| `--daemon` | `-d` | 作为守护进程运行（后台进程） |\n| `--log-level <level>` | `-l` | 日志级别（fatal/error/warn/info/debug/trace） |\n\n## 示例\n\n### 使用默认设置启动\n\n```bash\nccr start\n```\n\n### 在自定义端口启动\n\n```bash\nccr start --port 3000\n```\n\n### 使用自定义配置启动\n\n```bash\nccr start --config /path/to/config.json\n```\n\n### 作为守护进程启动\n\n```bash\nccr start --daemon\n```\n\n### 启用调试日志\n\n```bash\nccr start --log-level debug\n```\n\n## 环境变量\n\n您也可以使用环境变量配置服务器：\n\n| 变量 | 说明 |\n|------|------|\n| `PORT` | 监听端口号 |\n| `CONFIG_PATH` | 配置文件路径 |\n| `LOG_LEVEL` | 日志级别 |\n| `CUSTOM_ROUTER_PATH` | 自定义路由器函数路径 |\n| `HOST` | 绑定主机地址（默认：0.0.0.0） |\n\n## 输出\n\n启动成功后，您将看到：\n\n```\nClaude Code Router is running on http://localhost:3456\nAPI endpoint: http://localhost:3456/v1\n```\n\n## 相关命令\n\n- [ccr stop](/zh/docs/cli/other-commands#ccr-stop) - 停止服务器\n- [ccr restart](/zh/docs/cli/other-commands#ccr-restart) - 重启服务器\n- [ccr status](/zh/docs/cli/other-commands#ccr-status) - 检查服务器状态\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs.backup.20260101_205603/cli/status.md",
    "content": "---\nid: cli/status\ntitle: ccr status\nsidebar_position: 3\n---\n\n# ccr status\n\n显示 Claude Code Router 服务器的当前状态。\n\n## 用法\n\n```bash\nccr status\n```\n\n## 输出\n\n### 运行中的服务器\n\n当服务器正在运行时：\n\n```\nClaude Code Router 状态: 运行中\n版本: 2.0.0\nPID: 12345\n端口: 3456\n运行时间: 2小时34分钟\n配置: /home/user/.claude-code-router/config.json\n```\n\n### 已停止的服务器\n\n当服务器未运行时：\n\n```\nClaude Code Router 状态: 已停止\n```\n\n## 退出代码\n\n| 代码 | 说明 |\n|------|------|\n| 0 | 服务器正在运行 |\n| 1 | 服务器已停止 |\n| 2 | 检查状态时出错 |\n\n## 示例\n\n```bash\n$ ccr status\n\nClaude Code Router 状态: 运行中\n版本: 2.0.0\nPID: 12345\n端口: 3456\n运行时间: 2小时34分钟\n```\n\n## 相关命令\n\n- [ccr start](/zh/docs/cli/start) - 启动服务器\n- [ccr stop](/zh/docs/cli/other-commands#ccr-stop) - 停止服务器\n- [ccr restart](/zh/docs/cli/other-commands#ccr-restart) - 重启服务器\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs.backup.20260101_205603/config/basic.md",
    "content": "---\nid: config/basic\ntitle: 基础配置\nsidebar_position: 1\n---\n\n# 基础配置\n\n学习如何配置 Claude Code Router 以满足您的需求。\n\n## 配置文件位置\n\n配置文件位于：\n\n```\n~/.claude-code-router/config.json\n```\n\n## 配置结构\n\n### Providers（提供商）\n\n配置 LLM 提供商以将请求路由到：\n\n```json\n{\n  \"Providers\": [\n    {\n      \"name\": \"deepseek\",\n      \"api_base_url\": \"https://api.deepseek.com/chat/completions\",\n      \"api_key\": \"your-api-key\",\n      \"models\": [\"deepseek-chat\", \"deepseek-coder\"]\n    },\n    {\n      \"name\": \"groq\",\n      \"api_base_url\": \"https://api.groq.com/openai/v1/chat/completions\",\n      \"api_key\": \"your-groq-api-key\",\n      \"models\": [\"llama-3.3-70b-versatile\"]\n    }\n  ]\n}\n```\n\n### Router（路由器）\n\n配置默认使用的模型：\n\n```json\n{\n  \"Router\": {\n    \"default\": \"deepseek,deepseek-chat\"\n  }\n}\n```\n\n格式：`{provider-name},{model-name}`\n\n### Transformers（转换器）\n\n对请求/响应应用转换：\n\n```json\n{\n  \"transformers\": [\n    {\n      \"path\": \"/path/to/custom-transformer.js\",\n      \"options\": {\n        \"key\": \"value\"\n      }\n    }\n  ]\n}\n```\n\n### 环境变量\n\n在配置中使用环境变量：\n\n```json\n{\n  \"Providers\": [\n    {\n      \"name\": \"deepseek\",\n      \"api_base_url\": \"https://api.deepseek.com/chat/completions\",\n      \"api_key\": \"$DEEPSEEK_API_KEY\"\n    }\n  ]\n}\n```\n\n同时支持 `$VAR_NAME` 和 `${VAR_NAME}` 语法。\n\n## 完整示例\n\n```json\n{\n  \"PORT\": 8080,\n  \"APIKEY\": \"your-secret-key\",\n  \"PROXY_URL\": \"http://127.0.0.1:7890\",\n  \"LOG\": true,\n  \"LOG_LEVEL\": \"debug\",\n  \"API_TIMEOUT_MS\": 600000,\n  \"Providers\": [\n    {\n      \"name\": \"deepseek\",\n      \"api_base_url\": \"https://api.deepseek.com/chat/completions\",\n      \"api_key\": \"$DEEPSEEK_API_KEY\",\n      \"models\": [\"deepseek-chat\", \"deepseek-coder\"],\n      \"transformer\": {\n        \"use\": [\"deepseek\"]\n      }\n    },\n    {\n      \"name\": \"groq\",\n      \"api_base_url\": \"https://api.groq.com/openai/v1/chat/completions\",\n      \"api_key\": \"$GROQ_API_KEY\",\n      \"models\": [\"llama-3.3-70b-versatile\"]\n    }\n  ],\n  \"Router\": {\n    \"default\": \"deepseek,deepseek-chat\",\n    \"longContextThreshold\": 100000,\n    \"background\": \"groq,llama-3.3-70b-versatile\"\n  }\n}\n```\n\n## 编辑配置\n\n使用 CLI 编辑配置：\n\n```bash\nccr config edit\n```\n\n这将在您的默认编辑器中打开配置文件。\n\n## 重新加载配置\n\n编辑配置后，重启路由器：\n\n```bash\nccr restart\n```\n\n## 配置选项说明\n\n- **PORT**: 服务器端口号（默认：3456）\n- **APIKEY**: API 密钥，用于身份验证\n- **HOST**: 服务器监听地址（默认：127.0.0.1，如果配置了 Providers 且没有设置 APIKEY，则强制为 127.0.0.1）\n- **PROXY_URL**: 代理服务器地址\n- **LOG**: 是否启用日志（默认：true）\n- **LOG_LEVEL**: 日志级别（fatal/error/warn/info/debug/trace）\n- **API_TIMEOUT_MS**: API 请求超时时间（毫秒）\n- **NON_INTERACTIVE_MODE**: 非交互模式（用于 CI/CD 环境）\n\n## 下一步\n\n- [提供商配置](/zh/docs/config/providers) - 详细的提供商配置\n- [路由配置](/zh/docs/config/routing) - 配置路由规则\n- [转换器](/zh/docs/config/transformers) - 应用转换\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs.backup.20260101_205603/config/providers.md",
    "content": "---\nid: config/providers\ntitle: 提供商配置\nsidebar_position: 2\n---\n\n# 提供商配置\n\n配置 LLM 提供商的详细指南。\n\n## 支持的提供商\n\n### DeepSeek\n\n```json\n{\n  \"name\": \"deepseek\",\n  \"api_base_url\": \"https://api.deepseek.com/chat/completions\",\n  \"api_key\": \"your-api-key\",\n  \"models\": [\"deepseek-chat\", \"deepseek-coder\", \"deepseek-reasoner\"],\n  \"transformer\": {\n    \"use\": [\"deepseek\"]\n  }\n}\n```\n\n### Groq\n\n```json\n{\n  \"name\": \"groq\",\n  \"api_base_url\": \"https://api.groq.com/openai/v1/chat/completions\",\n  \"api_key\": \"your-api-key\",\n  \"models\": [\"llama-3.3-70b-versatile\"]\n}\n```\n\n### Gemini\n\n```json\n{\n  \"name\": \"gemini\",\n  \"api_base_url\": \"https://generativelanguage.googleapis.com/v1beta/models/\",\n  \"api_key\": \"your-api-key\",\n  \"models\": [\"gemini-2.5-flash\", \"gemini-2.5-pro\"],\n  \"transformer\": {\n    \"use\": [\"gemini\"]\n  }\n}\n```\n\n### OpenRouter\n\n```json\n{\n  \"name\": \"openrouter\",\n  \"api_base_url\": \"https://openrouter.ai/api/v1/chat/completions\",\n  \"api_key\": \"your-api-key\",\n  \"models\": [\n    \"anthropic/claude-3.5-sonnet\",\n    \"google/gemini-2.5-pro-preview\"\n  ],\n  \"transformer\": {\n    \"use\": [\"openrouter\"]\n  }\n}\n```\n\n### Ollama（本地模型）\n\n```json\n{\n  \"name\": \"ollama\",\n  \"api_base_url\": \"http://localhost:11434/v1/chat/completions\",\n  \"api_key\": \"ollama\",\n  \"models\": [\"qwen2.5-coder:latest\"]\n}\n```\n\n### 火山引擎\n\n```json\n{\n  \"name\": \"volcengine\",\n  \"api_base_url\": \"https://ark.cn-beijing.volces.com/api/v3/chat/completions\",\n  \"api_key\": \"your-api-key\",\n  \"models\": [\"deepseek-v3-250324\", \"deepseek-r1-250528\"],\n  \"transformer\": {\n    \"use\": [\"deepseek\"]\n  }\n}\n```\n\n### ModelScope\n\n```json\n{\n  \"name\": \"modelscope\",\n  \"api_base_url\": \"https://api-inference.modelscope.cn/v1/chat/completions\",\n  \"api_key\": \"\",\n  \"models\": [\n    \"Qwen/Qwen3-Coder-480B-A35B-Instruct\",\n    \"Qwen/Qwen3-235B-A22B-Thinking-2507\"\n  ],\n  \"transformer\": {\n    \"use\": [\n      [\"maxtoken\", { \"max_tokens\": 65536 }],\n      \"enhancetool\"\n    ],\n    \"Qwen/Qwen3-235B-A22B-Thinking-2507\": {\n      \"use\": [\"reasoning\"]\n    }\n  }\n}\n```\n\n### DashScope（阿里云）\n\n```json\n{\n  \"name\": \"dashscope\",\n  \"api_base_url\": \"https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions\",\n  \"api_key\": \"your-api-key\",\n  \"models\": [\"qwen3-coder-plus\"],\n  \"transformer\": {\n    \"use\": [\n      [\"maxtoken\", { \"max_tokens\": 65536 }],\n      \"enhancetool\"\n    ]\n  }\n}\n```\n\n## 提供商配置选项\n\n| 字段 | 类型 | 必填 | 说明 |\n|------|------|------|------|\n| `name` | string | 是 | 提供商的唯一标识符 |\n| `api_base_url` | string | 是 | API 基础 URL |\n| `api_key` | string | 是 | API 认证密钥 |\n| `models` | string[] | 否 | 可用模型列表 |\n| `transformer` | object | 否 | 应用的转换器配置 |\n\n## 模型选择\n\n在路由中选择模型时，使用以下格式：\n\n```\n{provider-name},{model-name}\n```\n\n例如：\n\n```\ndeepseek,deepseek-chat\n```\n\n## 使用环境变量\n\n您可以在配置中使用环境变量来保护 API 密钥：\n\n```json\n{\n  \"Providers\": [\n    {\n      \"name\": \"deepseek\",\n      \"api_base_url\": \"https://api.deepseek.com/chat/completions\",\n      \"api_key\": \"$DEEPSEEK_API_KEY\",\n      \"models\": [\"deepseek-chat\"]\n    }\n  ]\n}\n```\n\n支持 `$VAR_NAME` 和 `${VAR_NAME}` 两种语法。\n\n## 转换器配置\n\n转换器用于适配不同提供商的 API 差异。您可以在提供商级别或模型级别配置转换器：\n\n### 提供商级别转换器\n\n应用于提供商的所有模型：\n\n```json\n{\n  \"name\": \"openrouter\",\n  \"transformer\": {\n    \"use\": [\"openrouter\"]\n  }\n}\n```\n\n### 模型级别转换器\n\n应用于特定模型：\n\n```json\n{\n  \"name\": \"deepseek\",\n  \"transformer\": {\n    \"use\": [\"deepseek\"],\n    \"deepseek-chat\": {\n      \"use\": [\"tooluse\"]\n    }\n  }\n}\n```\n\n## 下一步\n\n- [路由配置](/zh/docs/config/routing) - 配置请求如何路由\n- [转换器](/zh/docs/config/transformers) - 对请求应用转换\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs.backup.20260101_205603/config/routing.md",
    "content": "---\nid: config/routing\ntitle: 路由配置\nsidebar_position: 3\n---\n\n# 路由配置\n\n配置如何将请求路由到不同的模型。\n\n## 默认路由\n\n为所有请求设置默认模型：\n\n```json\n{\n  \"Router\": {\n    \"default\": \"deepseek,deepseek-chat\"\n  }\n}\n```\n\n## 内置场景\n\n### 后台任务\n\n将后台任务路由到轻量级模型：\n\n```json\n{\n  \"Router\": {\n    \"background\": \"groq,llama-3.3-70b-versatile\"\n  }\n}\n```\n\n### 思考模式（计划模式）\n\n将思考密集型任务路由到更强大的模型：\n\n```json\n{\n  \"Router\": {\n    \"think\": \"deepseek,deepseek-reasoner\"\n  }\n}\n```\n\n### 长上下文\n\n路由长上下文请求：\n\n```json\n{\n  \"Router\": {\n    \"longContextThreshold\": 100000,\n    \"longContext\": \"gemini,gemini-2.5-pro\"\n  }\n}\n```\n\n### 网络搜索\n\n路由网络搜索任务：\n\n```json\n{\n  \"Router\": {\n    \"webSearch\": \"gemini,gemini-2.5-flash\"\n  }\n}\n```\n\n### 图像任务\n\n路由图像相关任务：\n\n```json\n{\n  \"Router\": {\n    \"image\": \"gemini,gemini-2.5-pro\"\n  }\n}\n```\n\n## 项目级路由\n\n在 `~/.claude/projects/<project-id>/claude-code-router.json` 中为每个项目配置路由：\n\n```json\n{\n  \"Router\": {\n    \"default\": \"groq,llama-3.3-70b-versatile\"\n  }\n}\n```\n\n项目级配置优先于全局配置。\n\n## 自定义路由器\n\n创建自定义 JavaScript 路由器函数：\n\n1. 创建路由器文件（例如 `custom-router.js`）：\n\n```javascript\nmodule.exports = async function(req, config) {\n  // 分析请求上下文\n  const userMessage = req.body.messages.find(m => m.role === 'user')?.content;\n\n  // 自定义路由逻辑\n  if (userMessage && userMessage.includes('解释代码')) {\n    return 'openrouter,anthropic/claude-3.5-sonnet';\n  }\n\n  // 返回 null 以使用默认路由\n  return null;\n};\n```\n\n2. 在 `config.json` 中设置 `CUSTOM_ROUTER_PATH`：\n\n```json\n{\n  \"CUSTOM_ROUTER_PATH\": \"/path/to/custom-router.js\"\n}\n```\n\n## Token 计数\n\n路由器使用 `tiktoken` (cl100k_base) 来估算请求 token 数量。这用于：\n\n- 确定请求是否超过 `longContextThreshold`\n- 基于 token 数量的自定义路由逻辑\n\n## 子代理路由\n\n使用特殊标签为子代理指定模型：\n\n```\n<CCR-SUBAGENT-MODEL>provider,model</CCR-SUBAGENT-MODEL>\n请帮我分析这段代码...\n```\n\n## 动态模型切换\n\n在 Claude Code 中使用 `/model` 命令动态切换模型：\n\n```\n/model provider_name,model_name\n```\n\n示例：`/model openrouter,anthropic/claude-3.5-sonnet`\n\n## 路由优先级\n\n1. 项目级配置\n2. 自定义路由器\n3. 内置场景路由\n4. 默认路由\n\n## 下一步\n\n- [转换器](/zh/docs/config/transformers) - 对请求应用转换\n- [自定义路由器](/zh/docs/advanced/custom-router) - 高级自定义路由\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs.backup.20260101_205603/config/transformers.md",
    "content": "---\nid: config/transformers\ntitle: 转换器\nsidebar_position: 4\n---\n\n# 转换器\n\n转换器用于适配不同提供商之间的 API 差异。\n\n## 内置转换器\n\n### anthropic\n\n将请求转换为兼容 Anthropic 风格的 API：\n\n```json\n{\n  \"transformer\": {\n    \"use\": [\"anthropic\"]\n  }\n}\n```\n\n如果只使用这一个转换器，它将直接透传请求和响应（您可以用来接入其他支持 Anthropic 端点的服务商）。\n\n### deepseek\n\n专门用于 DeepSeek API 的转换器：\n\n```json\n{\n  \"transformer\": {\n    \"use\": [\"deepseek\"]\n  }\n}\n```\n\n### gemini\n\n用于 Google Gemini API 的转换器：\n\n```json\n{\n  \"transformer\": {\n    \"use\": [\"gemini\"]\n  }\n}\n```\n\n### groq\n\n用于 Groq API 的转换器：\n\n```json\n{\n  \"transformer\": {\n    \"use\": [\"groq\"]\n  }\n}\n```\n\n### openrouter\n\n用于 OpenRouter API 的转换器：\n\n```json\n{\n  \"transformer\": {\n    \"use\": [\"openrouter\"]\n  }\n}\n```\n\nOpenRouter 转换器还支持 `provider` 路由参数，以指定 OpenRouter 应使用哪些底层提供商：\n\n```json\n{\n  \"transformer\": {\n    \"use\": [\"openrouter\"],\n    \"moonshotai/kimi-k2\": {\n      \"use\": [\n        [\"openrouter\", {\n          \"provider\": {\n            \"only\": [\"moonshotai/fp8\"]\n          }\n        }]\n      ]\n    }\n  }\n}\n```\n\n### maxtoken\n\n设置特定的 `max_tokens` 值：\n\n```json\n{\n  \"transformer\": {\n    \"use\": [\n      [\"maxtoken\", { \"max_tokens\": 65536 }]\n    ]\n  }\n}\n```\n\n### tooluse\n\n通过 `tool_choice` 参数优化某些模型的工具使用：\n\n```json\n{\n  \"transformer\": {\n    \"use\": [\"tooluse\"]\n  }\n}\n```\n\n### reasoning\n\n用于处理 `reasoning_content` 字段：\n\n```json\n{\n  \"transformer\": {\n    \"use\": [\"reasoning\"]\n  }\n}\n```\n\n### sampling\n\n用于处理采样信息字段，如 `temperature`、`top_p`、`top_k` 和 `repetition_penalty`：\n\n```json\n{\n  \"transformer\": {\n    \"use\": [\"sampling\"]\n  }\n}\n```\n\n### enhancetool\n\n对 LLM 返回的工具调用参数增加一层容错处理（注意：这会导致不再流式返回工具调用信息）：\n\n```json\n{\n  \"transformer\": {\n    \"use\": [\"enhancetool\"]\n  }\n}\n```\n\n### cleancache\n\n清除请求中的 `cache_control` 字段：\n\n```json\n{\n  \"transformer\": {\n    \"use\": [\"cleancache\"]\n  }\n}\n```\n\n### vertex-gemini\n\n处理使用 Vertex 鉴权的 Gemini API：\n\n```json\n{\n  \"transformer\": {\n    \"use\": [\"vertex-gemini\"]\n  }\n}\n```\n\n## 应用转换器\n\n### 全局应用\n\n应用于提供商的所有请求：\n\n```json\n{\n  \"Providers\": [\n    {\n      \"name\": \"deepseek\",\n      \"api_base_url\": \"https://api.deepseek.com/chat/completions\",\n      \"api_key\": \"your-api-key\",\n      \"transformer\": {\n        \"use\": [\"deepseek\"]\n      }\n    }\n  ]\n}\n```\n\n### 模型特定应用\n\n应用于特定模型：\n\n```json\n{\n  \"name\": \"deepseek\",\n  \"transformer\": {\n    \"use\": [\"deepseek\"],\n    \"deepseek-chat\": {\n      \"use\": [\"tooluse\"]\n    }\n  }\n}\n```\n\n### 传递选项\n\n某些转换器接受选项：\n\n```json\n{\n  \"transformer\": {\n    \"use\": [\n      [\"maxtoken\", { \"max_tokens\": 8192 }]\n    ]\n  }\n}\n```\n\n## 自定义转换器\n\n创建自定义转换器插件：\n\n1. 创建转换器文件：\n\n```javascript\nmodule.exports = {\n  name: 'my-transformer',\n  transformRequest: async (req, config) => {\n    // 修改请求\n    return req;\n  },\n  transformResponse: async (res, config) => {\n    // 修改响应\n    return res;\n  }\n};\n```\n\n2. 在配置中加载：\n\n```json\n{\n  \"transformers\": [\n    {\n      \"path\": \"/path/to/transformer.js\",\n      \"options\": {\n        \"key\": \"value\"\n      }\n    }\n  ]\n}\n```\n\n## 实验性转换器\n\n### gemini-cli（实验性）\n\n通过 Gemini CLI 对 Gemini 的非官方支持。\n\n### qwen-cli（实验性）\n\n通过 Qwen CLI 对 qwen3-coder-plus 的非官方支持。\n\n### rovo-cli（实验性）\n\n通过 Atlassian Rovo Dev CLI 对 GPT-5 的非官方支持。\n\n## 下一步\n\n- [高级主题](/zh/docs/advanced/custom-router) - 高级路由自定义\n- [Agent](/zh/docs/advanced/agents) - 使用 Agent 扩展功能\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs.backup.20260101_205603/current.json",
    "content": "{\n  \"version.label\": {\n    \"message\": \"Next\",\n    \"description\": \"The label for version current\"\n  },\n  \"sidebar.tutorialSidebar.category.Server\": {\n    \"message\": \"服务器\",\n    \"description\": \"The label for category 'Server' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.Server.link.generated-index.title\": {\n    \"message\": \"Claude Code Router 服务器\",\n    \"description\": \"The generated-index page title for category 'Server' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.Server.link.generated-index.description\": {\n    \"message\": \"部署和管理 Claude Code Router 服务器\",\n    \"description\": \"The generated-index page description for category 'Server' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.API Reference\": {\n    \"message\": \"API 参考\",\n    \"description\": \"The label for category 'API Reference' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.API Reference.link.generated-index.title\": {\n    \"message\": \"API 参考\",\n    \"description\": \"The generated-index page title for category 'API Reference' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.API Reference.link.generated-index.description\": {\n    \"message\": \"服务器 API 接口文档\",\n    \"description\": \"The generated-index page description for category 'API Reference' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.Configuration\": {\n    \"message\": \"配置\",\n    \"description\": \"The label for category 'Configuration' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.Configuration.link.generated-index.title\": {\n    \"message\": \"服务器配置\",\n    \"description\": \"The generated-index page title for category 'Configuration' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.Configuration.link.generated-index.description\": {\n    \"message\": \"服务器配置说明\",\n    \"description\": \"The generated-index page description for category 'Configuration' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.Advanced\": {\n    \"message\": \"高级\",\n    \"description\": \"The label for category 'Advanced' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.Advanced.link.generated-index.title\": {\n    \"message\": \"高级主题\",\n    \"description\": \"The generated-index page title for category 'Advanced' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.Advanced.link.generated-index.description\": {\n    \"message\": \"高级功能和自定义\",\n    \"description\": \"The generated-index page description for category 'Advanced' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.CLI\": {\n    \"message\": \"CLI\",\n    \"description\": \"The label for category 'CLI' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.CLI.link.generated-index.title\": {\n    \"message\": \"Claude Code Router CLI\",\n    \"description\": \"The generated-index page title for category 'CLI' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.CLI.link.generated-index.description\": {\n    \"message\": \"命令行工具使用指南\",\n    \"description\": \"The generated-index page description for category 'CLI' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.Commands\": {\n    \"message\": \"命令\",\n    \"description\": \"The label for category 'Commands' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.Commands.link.generated-index.title\": {\n    \"message\": \"CLI 命令\",\n    \"description\": \"The generated-index page title for category 'Commands' in sidebar 'tutorialSidebar'\"\n  },\n  \"sidebar.tutorialSidebar.category.Commands.link.generated-index.description\": {\n    \"message\": \"完整的命令参考\",\n    \"description\": \"The generated-index page description for category 'Commands' in sidebar 'tutorialSidebar'\"\n  }\n}\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs.backup.20260101_205603/installation.md",
    "content": "---\nid: installation\ntitle: 安装\nsidebar_position: 2\n---\n\n# 安装\n\n使用您喜欢的包管理器全局安装 Claude Code Router。\n\n## 前置要求\n\n- **Node.js**: >= 18.0.0\n- **pnpm**: >= 8.0.0（如果使用 pnpm）\n- 来自您偏好的 LLM 提供商的 API 密钥\n\n## 通过 npm 安装\n\n```bash\nnpm install -g @musistudio/claude-code-router\n```\n\n## 通过 pnpm 安装\n\n```bash\npnpm add -g @musistudio/claude-code-router\n```\n\n## 通过 Yarn 安装\n\n```bash\nyarn global add @musistudio/claude-code-router\n```\n\n## 验证安装\n\n安装完成后，验证 `ccr` 命令是否可用：\n\n```bash\nccr --version\n```\n\n您应该看到版本号显示。\n\n## 下一步\n\n安装完成后，前往 [快速开始](/zh/docs/quick-start) 了解如何配置和使用路由器。\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs.backup.20260101_205603/intro.md",
    "content": "---\nid: intro\ntitle: 欢迎使用 Claude Code Router\nsidebar_position: 1\nslug: /\n---\n\n# 欢迎使用 Claude Code Router\n\n[![npm version](https://badge.fury.io/js/%40musistudio%2Fclaude-code-router.svg)](https://www.npmjs.com/package/@musistudio/claude-code-router)\n![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)\n![Node Version](https://img.shields.io/node/v/@musistudio/claude-code-router.svg)\n\n**Claude Code Router** 是一个强大的工具，允许你在没有 Anthropic 账户的情况下使用 [Claude Code](https://claude.ai/code)，并将请求路由到其他 LLM 提供商。\n\n## 特性\n\n- **多提供商支持**: 路由到 DeepSeek、Gemini、Groq、OpenRouter 等\n- **智能路由**: 内置不同任务类型的场景（后台、思考、网络搜索、图像）\n- **项目级配置**: 每个项目自定义路由\n- **自定义路由函数**: 编写 JavaScript 定义自己的路由逻辑\n- **转换器系统**: 无缝适配不同提供商之间的 API 差异\n- **代理系统**: 可扩展的插件架构，实现自定义功能\n- **Web UI**: 内置管理界面，方便配置\n- **CLI 集成**: 与现有的 Claude Code 工作流无缝集成\n\n## 快速开始\n\n### 安装\n\n```bash\nnpm install -g @musistudio/claude-code-router\n# 或\npnpm add -g @musistudio/claude-code-router\n# 或\nyarn global add @musistudio/claude-code-router\n```\n\n### 基本使用\n\n```bash\n# 启动路由器服务器\nccr start\n\n# 配置 Claude Code 使用路由器\nexport ANTHROPIC_API_URL=\"http://localhost:8080/v1\"\nexport ANTHROPIC_API_KEY=\"your-api-key\"\n\n# 现在可以正常使用 Claude Code！\nclaude code\n```\n\n## 下一步\n\n- [安装指南](/docs/installation) - 详细安装说明\n- [快速开始](/docs/quick-start) - 5 分钟入门\n- [配置](/docs/config/basic) - 了解如何配置路由器\n- [CLI 参考](/docs/cli/start) - 完整的 CLI 命令参考\n\n## 架构\n\nClaude Code Router 由四个主要组件组成：\n\n- **CLI** (`@musistudio/claude-code-router`): 提供 `ccr` 命令的命令行工具\n- **Server** (`@CCR/server`): 处理 API 路由和转换的核心服务器\n- **Shared** (`@CCR/shared`): 共享常量和工具\n- **UI** (`@CCR/ui`): Web 管理界面（React + Vite）\n\n## 许可证\n\nMIT © [musistudio](https://github.com/musistudio)\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs.backup.20260101_205603/quick-start.md",
    "content": "---\nid: quick-start\ntitle: 快速开始\nsidebar_position: 3\n---\n\n# 快速开始\n\n5 分钟内启动并运行 Claude Code Router。\n\n## 1. 启动路由器\n\n```bash\nccr start\n```\n\n路由器默认将在 `http://localhost:8080` 上启动。\n\n## 2. 配置环境变量\n\n在您的 shell 中设置以下环境变量：\n\n```bash\nexport ANTHROPIC_API_URL=\"http://localhost:8080/v1\"\nexport ANTHROPIC_API_KEY=\"your-provider-api-key\"\n```\n\n或者使用 `ccr activate` 命令获取环境变量：\n\n```bash\neval \"$(ccr activate)\"\n```\n\n## 3. 使用 Claude Code\n\n现在您可以正常使用 Claude Code：\n\n```bash\nclaude code\n```\n\n您的请求将通过 Claude Code Router 路由到您配置的提供商。\n\n## 4. 配置提供商（可选）\n\n要配置多个提供商或自定义路由，使用：\n\n```bash\nccr model\n```\n\n这将打开一个交互式菜单来选择和配置模型。\n\n或者直接编辑配置文件：\n\n```bash\n# 在默认编辑器中打开配置\nccr config edit\n```\n\n配置文件示例 (`~/.claude-code-router/config.json`)：\n\n```json\n{\n  \"Providers\": [\n    {\n      \"name\": \"deepseek\",\n      \"api_base_url\": \"https://api.deepseek.com/chat/completions\",\n      \"api_key\": \"your-deepseek-api-key\",\n      \"models\": [\"deepseek-chat\", \"deepseek-coder\"]\n    }\n  ],\n  \"Router\": {\n    \"default\": \"deepseek,deepseek-chat\"\n  }\n}\n```\n\n## 下一步\n\n- [基础配置](/zh/docs/config/basic) - 了解配置选项\n- [路由配置](/zh/docs/config/routing) - 配置智能路由规则\n- [CLI 命令](/zh/docs/cli/start) - 探索所有 CLI 命令\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs.backup.20260101_205603/server/api/config-api.md",
    "content": "# 配置 API\n\n## GET /api/config\n\n获取当前服务器配置。\n\n### 请求示例\n\n```bash\ncurl http://localhost:3456/api/config \\\n  -H \"x-api-key: your-api-key\"\n```\n\n### 响应示例\n\n```json\n{\n  \"HOST\": \"0.0.0.0\",\n  \"PORT\": 3456,\n  \"APIKEY\": \"sk-xxxxx\",\n  \"Providers\": [\n    {\n      \"name\": \"openai\",\n      \"baseUrl\": \"https://api.openai.com/v1\",\n      \"apiKey\": \"sk-...\",\n      \"models\": [\"gpt-4\", \"gpt-3.5-turbo\"]\n    }\n  ],\n  \"Router\": {\n    \"default\": \"openai,gpt-4\"\n  },\n  \"transformers\": [\n    \"anthropic\"\n  ]\n}\n```\n\n## POST /api/config\n\n更新服务器配置。更新后会自动备份旧配置。\n\n### 请求示例\n\n```bash\ncurl -X POST http://localhost:3456/api/config \\\n  -H \"x-api-key: your-api-key\" \\\n  -H \"content-type: application/json\" \\\n  -d '{\n    \"HOST\": \"0.0.0.0\",\n    \"PORT\": 3456,\n    \"Providers\": [\n      {\n        \"name\": \"openai\",\n        \"baseUrl\": \"https://api.openai.com/v1\",\n        \"apiKey\": \"$OPENAI_API_KEY\",\n        \"models\": [\"gpt-4\"]\n      }\n    ],\n    \"Router\": {\n      \"default\": \"openai,gpt-4\"\n    }\n  }'\n```\n\n### 配置对象结构\n\n#### 基础配置\n\n| 字段 | 类型 | 必需 | 说明 |\n|------|------|------|------|\n| `HOST` | string | 否 | 监听地址（默认 127.0.0.1） |\n| `PORT` | integer | 否 | 监听端口（默认 3456） |\n| `APIKEY` | string | 否 | API 密钥 |\n| `LOG` | boolean | 否 | 是否启用日志（默认 true） |\n| `LOG_LEVEL` | string | 否 | 日志级别（debug/info/warn/error） |\n\n#### Providers 配置\n\n```json\n{\n  \"Providers\": [\n    {\n      \"name\": \"provider-name\",\n      \"baseUrl\": \"https://api.example.com/v1\",\n      \"apiKey\": \"your-api-key\",\n      \"models\": [\"model-1\", \"model-2\"]\n    }\n  ]\n}\n```\n\n| 字段 | 类型 | 必需 | 说明 |\n|------|------|------|------|\n| `name` | string | 是 | 提供商名称 |\n| `baseUrl` | string | 是 | API 基础 URL |\n| `apiKey` | string | 是 | API 密钥 |\n| `models` | array | 是 | 支持的模型列表 |\n\n#### Router 配置\n\n```json\n{\n  \"Router\": {\n    \"default\": \"provider,model\",\n    \"longContextThreshold\": 100000,\n    \"routes\": {\n      \"background\": \"lightweight-model\",\n      \"think\": \"powerful-model\",\n      \"longContext\": \"long-context-model\",\n      \"webSearch\": \"search-model\",\n      \"image\": \"vision-model\"\n    }\n  }\n}\n```\n\n#### Transformers 配置\n\n```json\n{\n  \"transformers\": [\n    {\n      \"name\": \"anthropic\",\n      \"provider\": \"provider-name\",\n      \"models\": [\"model-1\"],\n      \"options\": {}\n    }\n  ]\n}\n```\n\n### 响应示例\n\n成功：\n\n```json\n{\n  \"success\": true,\n  \"message\": \"Config saved successfully\"\n}\n```\n\n### 配置备份\n\n每次更新配置时，旧配置会自动备份到：\n\n```\n~/.claude-code-router/config.backup.{timestamp}.json\n```\n\n保留最近 3 个备份。\n\n## GET /api/transformers\n\n获取服务器加载的所有转换器列表。\n\n### 请求示例\n\n```bash\ncurl http://localhost:3456/api/transformers \\\n  -H \"x-api-key: your-api-key\"\n```\n\n### 响应示例\n\n```json\n{\n  \"transformers\": [\n    {\n      \"name\": \"anthropic\",\n      \"endpoint\": null\n    },\n    {\n      \"name\": \"openai\",\n      \"endpoint\": null\n    },\n    {\n      \"name\": \"gemini\",\n      \"endpoint\": \"https://generativelanguage.googleapis.com\"\n    }\n  ]\n}\n```\n\n### 转换器列表\n\n内置转换器：\n\n- `anthropic` - Anthropic Claude 格式\n- `openai` - OpenAI 格式\n- `deepseek` - DeepSeek 格式\n- `gemini` - Google Gemini 格式\n- `openrouter` - OpenRouter 格式\n- `groq` - Groq 格式\n- `maxtoken` - 调整 max_tokens 参数\n- `tooluse` - 工具使用转换\n- `reasoning` - 推理模式转换\n- `enhancetool` - 增强工具功能\n\n## 环境变量插值\n\n配置支持环境变量插值：\n\n```json\n{\n  \"Providers\": [\n    {\n      \"apiKey\": \"$OPENAI_API_KEY\"\n    }\n  ]\n}\n```\n\n或使用 `${VAR_NAME}` 格式：\n\n```json\n{\n  \"baseUrl\": \"${API_BASE_URL}\"\n}\n```\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs.backup.20260101_205603/server/api/logs-api.md",
    "content": "# 日志 API\n\n## GET /api/logs/files\n\n获取所有可用的日志文件列表。\n\n### 请求示例\n\n```bash\ncurl http://localhost:3456/api/logs/files \\\n  -H \"x-api-key: your-api-key\"\n```\n\n### 响应示例\n\n```json\n[\n  {\n    \"name\": \"ccr-20241226143022.log\",\n    \"path\": \"/home/user/.claude-code-router/logs/ccr-20241226143022.log\",\n    \"size\": 1024000,\n    \"lastModified\": \"2024-12-26T14:30:22.000Z\"\n  },\n  {\n    \"name\": \"ccr-20241226143021.log\",\n    \"path\": \"/home/user/.claude-code-router/logs/ccr-20241226143021.log\",\n    \"size\": 980000,\n    \"lastModified\": \"2024-12-26T14:30:21.000Z\"\n  }\n]\n```\n\n### 字段说明\n\n| 字段 | 类型 | 说明 |\n|------|------|------|\n| `name` | string | 文件名 |\n| `path` | string | 完整文件路径 |\n| `size` | integer | 文件大小（字节） |\n| `lastModified` | string | 最后修改时间（ISO 8601） |\n\n文件按修改时间倒序排列。\n\n## GET /api/logs\n\n获取指定日志文件的内容。\n\n### 查询参数\n\n| 参数 | 类型 | 必需 | 说明 |\n|------|------|------|------|\n| `file` | string | 否 | 日志文件路径（默认使用 app.log） |\n\n### 请求示例（获取默认日志）\n\n```bash\ncurl \"http://localhost:3456/api/logs\" \\\n  -H \"x-api-key: your-api-key\"\n```\n\n### 请求示例（获取指定文件）\n\n```bash\ncurl \"http://localhost:3456/api/logs?file=/home/user/.claude-code-router/logs/ccr-20241226143022.log\" \\\n  -H \"x-api-key: your-api-key\"\n```\n\n### 响应示例\n\n```json\n[\n  \"{\\\"level\\\":30,\\\"time\\\":1703550622000,\\\"pid\\\":12345,\\\"hostname\\\":\\\"server\\\",\\\"msg\\\":\\\"Incoming request\\\",\\\"req\\\":{\\\"id\\\":1,\\\"method\\\":\\\"POST\\\",\\\"url\\\":\\\"/v1/messages\\\",\\\"remoteAddress\\\":\\\"127.0.0.1\\\"}}\",\n  \"{\\\"level\\\":30,\\\"time\\\":1703550622500,\\\"pid\\\":12345,\\\"hostname\\\":\\\"server\\\",\\\"msg\\\":\\\"Request completed\\\",\\\"res\\\":{\\\"statusCode\\\":200,\\\"responseTime\\\":500}}\",\n  \"...\"\n]\n```\n\n返回的是日志行数组，每行是一个 JSON 字符串。\n\n### 日志格式\n\n日志使用 Pino 格式：\n\n```json\n{\n  \"level\": 30,\n  \"time\": 1703550622000,\n  \"pid\": 12345,\n  \"hostname\": \"server\",\n  \"msg\": \"Incoming request\",\n  \"req\": {\n    \"id\": 1,\n    \"method\": \"POST\",\n    \"url\": \"/v1/messages\",\n    \"remoteAddress\": \"127.0.0.1\"\n  }\n}\n```\n\n### 日志级别\n\n| 级别 | 值 | 说明 |\n|------|------|------|\n| `trace` | 10 | 最详细的日志 |\n| `debug` | 20 | 调试信息 |\n| `info` | 30 | 一般信息 |\n| `warn` | 40 | 警告信息 |\n| `error` | 50 | 错误信息 |\n| `fatal` | 60 | 致命错误 |\n\n## DELETE /api/logs\n\n清除指定日志文件的内容。\n\n### 查询参数\n\n| 参数 | 类型 | 必需 | 说明 |\n|------|------|------|------|\n| `file` | string | 否 | 日志文件路径（默认使用 app.log） |\n\n### 请求示例（清除默认日志）\n\n```bash\ncurl -X DELETE \"http://localhost:3456/api/logs\" \\\n  -H \"x-api-key: your-api-key\"\n```\n\n### 请求示例（清除指定文件）\n\n```bash\ncurl -X DELETE \"http://localhost:3456/api/logs?file=/home/user/.claude-code-router/logs/ccr-20241226143022.log\" \\\n  -H \"x-api-key: your-api-key\"\n```\n\n### 响应示例\n\n```json\n{\n  \"success\": true,\n  \"message\": \"Logs cleared successfully\"\n}\n```\n\n## 日志位置\n\n### 服务器日志\n\n位置：`~/.claude-code-router/logs/`\n\n文件命名：`ccr-{YYYYMMDD}{HH}{MM}{SS}.log`\n\n内容：HTTP 请求、API 调用、服务器事件\n\n### 应用日志\n\n位置：`~/.claude-code-router/claude-code-router.log`\n\n内容：路由决策、业务逻辑事件\n\n## 日志轮转\n\n服务器日志使用 rotating-file-stream 自动轮转：\n\n- **maxFiles**: 3 - 保留最近 3 个日志文件\n- **interval**: 1d - 每天轮转\n- **maxSize**: 50M - 单个文件最大 50MB\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs.backup.20260101_205603/server/api/messages-api.md",
    "content": "# 消息 API\n\n## POST /v1/messages\n\n发送消息到 LLM，兼容 Anthropic Claude API 格式。\n\n### 请求格式\n\n```bash\ncurl -X POST http://localhost:3456/v1/messages \\\n  -H \"x-api-key: your-api-key\" \\\n  -H \"content-type: application/json\" \\\n  -d '{\n    \"model\": \"claude-3-5-sonnet-20241022\",\n    \"max_tokens\": 1024,\n    \"messages\": [\n      {\n        \"role\": \"user\",\n        \"content\": \"Hello, Claude!\"\n      }\n    ]\n  }'\n```\n\n### 请求参数\n\n| 参数 | 类型 | 必需 | 说明 |\n|------|------|------|------|\n| `model` | string | 是 | 模型名称（会被路由到实际提供商） |\n| `messages` | array | 是 | 消息数组 |\n| `max_tokens` | integer | 是 | 最大生成 Token 数 |\n| `system` | string | 否 | 系统提示词 |\n| `tools` | array | 否 | 可用工具列表 |\n| `stream` | boolean | 否 | 是否使用流式响应（默认 false） |\n| `temperature` | number | 否 | 温度参数（0-1） |\n\n### 消息对象格式\n\n```json\n{\n  \"role\": \"user|assistant\",\n  \"content\": \"string | array\"\n}\n```\n\n### 响应格式（非流式）\n\n```json\n{\n  \"id\": \"msg_xxx\",\n  \"type\": \"message\",\n  \"role\": \"assistant\",\n  \"content\": [\n    {\n      \"type\": \"text\",\n      \"text\": \"Hello! How can I help you today?\"\n    }\n  ],\n  \"model\": \"claude-3-5-sonnet-20241022\",\n  \"stop_reason\": \"end_turn\",\n  \"usage\": {\n    \"input_tokens\": 10,\n    \"output_tokens\": 20\n  }\n}\n```\n\n### 流式响应\n\n设置 `stream: true` 启用流式响应：\n\n```json\n{\n  \"model\": \"claude-3-5-sonnet-20241022\",\n  \"max_tokens\": 1024,\n  \"messages\": [...],\n  \"stream\": true\n}\n```\n\n流式响应事件类型：\n\n- `message_start` - 消息开始\n- `content_block_start` - 内容块开始\n- `content_block_delta` - 内容增量\n- `content_block_stop` - 内容块结束\n- `message_delta` - 消息元数据（usage）\n- `message_stop` - 消息结束\n\n### 工具使用\n\n支持函数调用（Tool Use）：\n\n```json\n{\n  \"model\": \"claude-3-5-sonnet-20241022\",\n  \"max_tokens\": 1024,\n  \"messages\": [\n    {\n      \"role\": \"user\",\n      \"content\": \"What's the weather like?\"\n    }\n  ],\n  \"tools\": [\n    {\n      \"name\": \"get_weather\",\n      \"description\": \"Get the current weather\",\n      \"input_schema\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"location\": {\n            \"type\": \"string\",\n            \"description\": \"City name\"\n          }\n        },\n        \"required\": [\"location\"]\n      }\n    }\n  ]\n}\n```\n\n### 多模态支持\n\n支持图片输入：\n\n```json\n{\n  \"role\": \"user\",\n  \"content\": [\n    {\n      \"type\": \"image\",\n      \"source\": {\n        \"type\": \"base64\",\n        \"media_type\": \"image/png\",\n        \"data\": \"iVBORw0KGgo...\"\n      }\n    },\n    {\n      \"type\": \"text\",\n      \"text\": \"Describe this image\"\n    }\n  ]\n}\n```\n\n## POST /v1/messages/count_tokens\n\n计算消息的 Token 数量。\n\n### 请求格式\n\n```bash\ncurl -X POST http://localhost:3456/v1/messages/count_tokens \\\n  -H \"x-api-key: your-api-key\" \\\n  -H \"content-type: application/json\" \\\n  -d '{\n    \"model\": \"claude-3-5-sonnet-20241022\",\n    \"messages\": [\n      {\n        \"role\": \"user\",\n        \"content\": \"Hello!\"\n      }\n    ],\n    \"tools\": [],\n    \"system\": \"You are a helpful assistant.\"\n  }'\n```\n\n### 请求参数\n\n| 参数 | 类型 | 必需 | 说明 |\n|------|------|------|------|\n| `model` | string | 是 | 模型名称 |\n| `messages` | array | 是 | 消息数组 |\n| `tools` | array | 否 | 工具列表 |\n| `system` | string | 否 | 系统提示词 |\n\n### 响应格式\n\n```json\n{\n  \"input_tokens\": 42\n}\n```\n\n## 错误响应\n\n### 400 Bad Request\n\n```json\n{\n  \"error\": {\n    \"type\": \"invalid_request_error\",\n    \"message\": \"messages is required\"\n  }\n}\n```\n\n### 401 Unauthorized\n\n```json\n{\n  \"error\": {\n    \"type\": \"authentication_error\",\n    \"message\": \"Invalid API key\"\n  }\n}\n```\n\n### 500 Internal Server Error\n\n```json\n{\n  \"error\": {\n    \"type\": \"api_error\",\n    \"message\": \"Failed to connect to provider\"\n  }\n}\n```\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs.backup.20260101_205603/server/api/overview.md",
    "content": "# API 概览\n\nClaude Code Router Server 提供了完整的 HTTP API，支持：\n\n- **消息 API**：兼容 Anthropic Claude API 的消息接口\n- **配置 API**：读取和更新服务器配置\n- **日志 API**：查看和管理服务日志\n- **工具 API**：计算 Token 数量\n\n## 基础信息\n\n**Base URL**: `http://localhost:3456`\n\n**认证方式**: API Key（通过 `x-api-key` 请求头）\n\n```bash\ncurl -H \"x-api-key: your-api-key\" http://localhost:3456/api/config\n```\n\n## API 端点列表\n\n### 消息相关\n\n| 端点 | 方法 | 描述 |\n|------|------|------|\n| `/v1/messages` | POST | 发送消息（兼容 Anthropic API） |\n| `/v1/messages/count_tokens` | POST | 计算消息的 Token 数量 |\n\n### 配置管理\n\n| 端点 | 方法 | 描述 |\n|------|------|------|\n| `/api/config` | GET | 获取当前配置 |\n| `/api/config` | POST | 更新配置 |\n| `/api/transformers` | GET | 获取可用的转换器列表 |\n\n### 日志管理\n\n| 端点 | 方法 | 描述 |\n|------|------|------|\n| `/api/logs/files` | GET | 获取日志文件列表 |\n| `/api/logs` | GET | 获取日志内容 |\n| `/api/logs` | DELETE | 清除日志 |\n\n### 服务管理\n\n| 端点 | 方法 | 描述 |\n|------|------|------|\n| `/api/restart` | POST | 重启服务 |\n| `/ui` | GET | Web 管理界面 |\n| `/ui/` | GET | Web 管理界面（重定向） |\n\n## 认证\n\n### API Key 认证\n\n在请求头中添加 API Key：\n\n```bash\ncurl -X POST http://localhost:3456/v1/messages \\\n  -H \"x-api-key: your-api-key\" \\\n  -H \"content-type: application/json\" \\\n  -d '...'\n```\n\n## 流式响应\n\n消息 API 支持流式响应（Server-Sent Events）：\n\n```bash\ncurl -X POST http://localhost:3456/v1/messages \\\n  -H \"x-api-key: your-api-key\" \\\n  -H \"content-type: application/json\" \\\n  -d '{\"stream\": true, ...}'\n```\n\n流式响应格式：\n\n```\nevent: message_start\ndata: {\"type\":\"message_start\",\"message\":{...}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"delta\":{\"type\":\"text_delta\",\"text\":\"Hello\"}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n```\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs.backup.20260101_205603/server/deployment.md",
    "content": "# Server 部署\n\nClaude Code Router Server 支持多种部署方式，从本地开发到生产环境。\n\n## Docker 部署（推荐）\n\n### 使用 Docker Hub 镜像\n\n```bash\ndocker run -d \\\n  --name claude-code-router \\\n  -p 3456:3456 \\\n  -v ~/.claude-code-router:/app/.claude-code-router \\\n  musistudio/claude-code-router:latest\n```\n\n### 使用 Docker Compose\n\n创建 `docker-compose.yml`：\n\n```yaml\nversion: '3.8'\nservices:\n  claude-code-router:\n    image: musistudio/claude-code-router:latest\n    container_name: claude-code-router\n    ports:\n      - \"3456:3456\"\n    volumes:\n      - ./config:/app/.claude-code-router\n    environment:\n      - LOG_LEVEL=info\n      - HOST=0.0.0.0\n      - PORT=3456\n    restart: unless-stopped\n```\n\n启动服务：\n\n```bash\ndocker-compose up -d\n```\n\n### 自定义构建\n\n从源码构建 Docker 镜像：\n\n```bash\ngit clone https://github.com/musistudio/claude-code-router.git\ncd claude-code-router\ndocker build -t claude-code-router:latest .\n```\n\n## 配置文件挂载\n\n将配置文件挂载到容器中：\n\n```bash\ndocker run -d \\\n  --name claude-code-router \\\n  -p 3456:3456 \\\n  -v $(pwd)/config.json:/app/.claude-code-router/config.json \\\n  musistudio/claude-code-router:latest\n```\n\n配置文件示例：\n\n```json5\n{\n  // 服务器配置\n  \"HOST\": \"0.0.0.0\",\n  \"PORT\": 3456,\n  \"APIKEY\": \"your-api-key-here\",\n\n  // 日志配置\n  \"LOG\": true,\n  \"LOG_LEVEL\": \"info\",\n\n  // LLM 提供商配置\n  \"Providers\": [\n    {\n      \"name\": \"openai\",\n      \"baseUrl\": \"https://api.openai.com/v1\",\n      \"apiKey\": \"$OPENAI_API_KEY\",\n      \"models\": [\"gpt-4\", \"gpt-3.5-turbo\"]\n    }\n  ],\n\n  // 路由配置\n  \"Router\": {\n    \"default\": \"openai,gpt-4\"\n  }\n}\n```\n\n## 环境变量\n\n支持通过环境变量覆盖配置：\n\n| 变量名 | 说明 | 默认值 |\n|--------|------|--------|\n| `HOST` | 监听地址 | `127.0.0.1` |\n| `PORT` | 监听端口 | `3456` |\n| `APIKEY` | API 密钥 | - |\n| `LOG_LEVEL` | 日志级别 | `debug` |\n| `LOG` | 是否启用日志 | `true` |\n\n## 生产环境建议\n\n### 1. 使用反向代理\n\n使用 Nginx 作为反向代理：\n\n```nginx\nserver {\n    listen 80;\n    server_name your-domain.com;\n\n    location / {\n        proxy_pass http://localhost:3456;\n        proxy_http_version 1.1;\n        proxy_set_header Upgrade $http_upgrade;\n        proxy_set_header Connection 'upgrade';\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_cache_bypass $http_upgrade;\n    }\n}\n```\n\n### 2. 配置 HTTPS\n\n使用 Let's Encrypt 获取免费证书：\n\n```bash\nsudo certbot --nginx -d your-domain.com\n```\n\n### 3. 日志管理\n\n配置日志轮转和持久化：\n\n```yaml\nversion: '3.8'\nservices:\n  claude-code-router:\n    image: musistudio/claude-code-router:latest\n    volumes:\n      - ./logs:/app/.claude-code-router/logs\n    environment:\n      - LOG_LEVEL=warn\n```\n\n### 4. 健康检查\n\n配置 Docker 健康检查：\n\n```yaml\nhealthcheck:\n  test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:3456/api/config\"]\n  interval: 30s\n  timeout: 10s\n  retries: 3\n```\n\n## 访问 Web UI\n\n部署完成后，访问 Web UI：\n\n```\nhttp://localhost:3456/ui/\n```\n\n通过 Web UI 可以：\n- 查看和管理配置\n- 监控日志\n- 查看服务状态\n\n## 二次开发\n\n如果需要基于 CCR Server 进行二次开发，请查看 [API 参考](/docs/category/api)。\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-plugin-content-docs.backup.20260101_205603/server/intro.md",
    "content": "# Server 简介\n\nClaude Code Router Server 是一个核心服务组件，负责将 Claude Code 的 API 请求路由到不同的 LLM 提供商。它提供了完整的 HTTP API，支持：\n\n- **API 请求路由**：将 Anthropic 格式的请求转换为各种提供商的 API 格式\n- **认证与授权**：支持 API Key 认证\n- **配置管理**：动态配置提供商、路由规则和转换器\n- **Web UI**：内置管理界面\n- **日志系统**：完整的请求日志记录\n\n## 架构概述\n\n```\n┌─────────────┐     ┌──────────────────┐     ┌──────────────┐\n│ Claude Code │────▶│ CCR Server       │────▶│ LLM Provider │\n│   Client    │     │  (Router +       │     │  (OpenAI/    │\n└─────────────┘     │   Transformer)   │     │   Gemini/etc)│\n                    └──────────────────┘     └──────────────┘\n                           │\n                           ├─ Web UI\n                           ├─ Config API\n                           └─ Logs API\n```\n\n## 核心功能\n\n### 1. 请求路由\n- 基于 Token 数量的智能路由\n- 项目级路由配置\n- 自定义路由函数\n- 场景化路由（background、think、longContext 等）\n\n### 2. 请求转换\n- 支持多种 LLM 提供商的 API 格式转换\n- 内置转换器：Anthropic、DeepSeek、Gemini、OpenRouter、Groq 等\n- 可扩展的转换器系统\n\n### 3. Agent 系统\n- 插件式的 Agent 架构\n- 内置图片处理 Agent\n- 自定义 Agent 支持\n\n### 4. 配置管理\n- JSON5 格式配置文件\n- 环境变量插值\n- 配置热更新（需重启服务）\n\n## 使用场景\n\n### 场景一：个人本地服务\n在本地运行服务，供个人 Claude Code 使用：\n\n```bash\nccr start\n```\n\n### 场景二：团队共享服务\n使用 Docker 部署，为团队成员提供共享服务：\n\n```bash\ndocker run -d -p 3456:3456 musistudio/claude-code-router\n```\n\n### 场景三：二次开发\n基于暴露的 API 构建自定义应用：\n\n```bash\nGET /api/config\nPOST /v1/messages\nGET /api/logs\n```\n\n## 下一步\n\n- [Docker 部署指南](/docs/server/deployment) - 学习如何部署服务\n- [API 参考](/docs/category/api) - 查看完整的 API 文档\n- [配置说明](/docs/category/server-config) - 了解服务器配置选项\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-theme-classic/footer.json",
    "content": "{\n  \"link.title.Documentation\": {\n    \"message\": \"文档\",\n    \"description\": \"The title of the footer links column with title=Documentation in the footer\"\n  },\n  \"link.title.Community\": {\n    \"message\": \"社区\",\n    \"description\": \"The title of the footer links column with title=Community in the footer\"\n  },\n  \"link.title.Resources\": {\n    \"message\": \"资源\",\n    \"description\": \"The title of the footer links column with title=Resources in the footer\"\n  },\n  \"link.item.label.Introduction\": {\n    \"message\": \"简介\",\n    \"description\": \"The label of footer link with label=Introduction linking to /docs/intro\"\n  },\n  \"link.item.label.Installation\": {\n    \"message\": \"安装\",\n    \"description\": \"The label of footer link with label=Installation linking to /docs/cli/installation\"\n  },\n  \"link.item.label.Quick Start\": {\n    \"message\": \"快速开始\",\n    \"description\": \"The label of footer link with label=Quick Start linking to /docs/cli/quick-start\"\n  },\n  \"link.item.label.GitHub\": {\n    \"message\": \"GitHub\",\n    \"description\": \"The label of footer link with label=GitHub linking to https://github.com/musistudio/claude-code-router\"\n  },\n  \"link.item.label.Issues\": {\n    \"message\": \"问题反馈\",\n    \"description\": \"The label of footer link with label=Issues linking to https://github.com/musistudio/claude-code-router/issues\"\n  },\n  \"link.item.label.Releases\": {\n    \"message\": \"发布版本\",\n    \"description\": \"The label of footer link with label=Releases linking to https://github.com/musistudio/claude-code-router/releases\"\n  },\n  \"link.item.label.Blog\": {\n    \"message\": \"博客\",\n    \"description\": \"The label of footer link with label=Blog linking to /blog\"\n  },\n  \"link.item.label.NPM Package\": {\n    \"message\": \"NPM 包\",\n    \"description\": \"The label of footer link with label=NPM Package linking to https://www.npmjs.com/package/@musistudio/claude-code-router\"\n  },\n  \"copyright\": {\n    \"message\": \"Copyright © 2025 Claude Code Router. 保留所有权利。\",\n    \"description\": \"The footer copyright\"\n  }\n}\n"
  },
  {
    "path": "docs/i18n/zh-CN/docusaurus-theme-classic/navbar.json",
    "content": "{\n  \"title\": {\n    \"message\": \"Claude Code Router\",\n    \"description\": \"The title in the navbar\"\n  },\n  \"logo.alt\": {\n    \"message\": \"Claude Code Router Logo\",\n    \"description\": \"The alt text of navbar logo\"\n  },\n  \"item.label.Documentation\": {\n    \"message\": \"文档\",\n    \"description\": \"Navbar item with label Documentation\"\n  },\n  \"item.label.Blog\": {\n    \"message\": \"博客\",\n    \"description\": \"Navbar item with label Blog\"\n  },\n  \"item.label.GitHub\": {\n    \"message\": \"GitHub\",\n    \"description\": \"Navbar item with label GitHub\"\n  }\n}\n"
  },
  {
    "path": "docs/package.json",
    "content": "{\n  \"name\": \"claude-code-router-docs\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"docusaurus\": \"docusaurus\",\n    \"start\": \"docusaurus start\",\n    \"build\": \"docusaurus build\",\n    \"swizzle\": \"docusaurus swizzle\",\n    \"deploy\": \"docusaurus deploy\",\n    \"clear\": \"docusaurus clear\",\n    \"serve\": \"docusaurus serve\",\n    \"write-translations\": \"docusaurus write-translations\",\n    \"write-heading-ids\": \"docusaurus write-heading-ids\"\n  },\n  \"dependencies\": {\n    \"@docusaurus/core\": \"^3.9.2\",\n    \"@docusaurus/preset-classic\": \"^3.9.2\",\n    \"@mdx-js/react\": \"^3.0.0\",\n    \"prism-react-renderer\": \"^2.4.1\",\n    \"react\": \"^18.3.1\",\n    \"react-dom\": \"^18.3.1\"\n  },\n  \"devDependencies\": {\n    \"@docusaurus/module-type-aliases\": \"^3.9.2\",\n    \"@docusaurus/types\": \"^3.9.2\",\n    \"@tailwindcss/typography\": \"^0.5.19\",\n    \"@types/react\": \"18\",\n    \"@types/react-dom\": \"18\",\n    \"autoprefixer\": \"^10.4.21\",\n    \"tailwindcss\": \"3\"\n  },\n  \"browserslist\": {\n    \"production\": [\n      \">0.5%\",\n      \"not dead\",\n      \"not op_mini all\"\n    ],\n    \"development\": [\n      \"last 1 chrome version\",\n      \"last 1 firefox version\",\n      \"last 1 safari version\"\n    ]\n  },\n  \"engines\": {\n    \"node\": \">=18.0\"\n  }\n}\n"
  },
  {
    "path": "docs/postcss.config.js",
    "content": "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n}\n"
  },
  {
    "path": "docs/sidebars.ts",
    "content": "import type { SidebarsConfig } from '@docusaurus/plugin-content-docs';\n\nconst sidebars: SidebarsConfig = {\n  tutorialSidebar: [\n    {\n      type: 'category',\n      label: 'CLI',\n      link: {\n        type: 'generated-index',\n        title: 'Claude Code Router CLI',\n        description: 'Command-line tool usage guide',\n        slug: 'category/cli',\n      },\n      items: [\n        'cli/intro',\n        'cli/installation',\n        'cli/quick-start',\n        {\n          type: 'category',\n          label: 'Commands',\n          link: {\n            type: 'generated-index',\n            title: 'CLI Commands',\n            description: 'Complete command reference',\n            slug: 'category/cli-commands',\n          },\n          items: [\n            'cli/commands/start',\n            'cli/commands/model',\n            'cli/commands/status',\n            'cli/commands/statusline',\n            'cli/commands/preset',\n            'cli/commands/other',\n          ],\n        },\n        {\n          type: 'category',\n          label: 'Configuration',\n          key: 'cli-configuration-category',\n          link: {\n            type: 'generated-index',\n            title: 'CLI Configuration',\n            description: 'CLI configuration guide',\n            slug: 'category/cli-config',\n          },\n          items: [\n            'cli/config/basic',\n            'cli/config/project-level',\n          ],\n        },\n      ],\n    },\n    {\n      type: 'category',\n      label: 'Server',\n      link: {\n        type: 'generated-index',\n        title: 'Claude Code Router Server',\n        description: 'Deploy and manage Claude Code Router server',\n        slug: 'category/server',\n      },\n      items: [\n        'server/intro',\n        'server/deployment',\n        {\n          type: 'category',\n          label: 'API Reference',\n          link: {\n            type: 'generated-index',\n            title: 'API Reference',\n            description: 'Server API documentation',\n            slug: 'category/api',\n          },\n          items: [\n            'server/api/overview',\n            'server/api/messages-api',\n            'server/api/config-api',\n            'server/api/logs-api',\n          ],\n        },\n        {\n          type: 'category',\n          label: 'Configuration',\n          key: 'server-configuration-category',\n          link: {\n            type: 'generated-index',\n            title: 'Server Configuration',\n            description: 'Server configuration guide',\n            slug: 'category/server-config',\n          },\n          items: [\n            'server/config/basic',\n            'server/config/providers',\n            'server/config/routing',\n            'server/config/transformers',\n          ],\n        },\n        {\n          type: 'category',\n          label: 'Advanced',\n          link: {\n            type: 'generated-index',\n            title: 'Advanced Topics',\n            description: 'Advanced features and customization',\n            slug: 'category/server-advanced',\n          },\n          items: [\n            'server/advanced/custom-router',\n          ],\n        },\n      ],\n    },\n    {\n      type: 'category',\n      label: 'Presets',\n      link: {\n        type: 'generated-index',\n        title: 'CCR Presets',\n        description: 'Predefined configurations for quick setup',\n        slug: 'category/presets',\n      },\n      items: ['presets/intro'],\n    },\n  ],\n};\n\nexport default sidebars;\n"
  },
  {
    "path": "docs/src/components/HomepageFeatures.module.css",
    "content": ".features {\n  display: flex;\n  align-items: center;\n  padding: 2rem 0;\n  width: 100%;\n}\n\n.featureSvg {\n  height: 160px;\n  width: 160px;\n  transition: transform 0.3s ease;\n}\n\n.feature:hover .featureSvg {\n  transform: scale(1.05);\n}\n\n.feature {\n  padding: 1.5rem;\n  border-radius: 12px;\n  background: var(--ifm-background-surface-color);\n  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);\n  height: 100%;\n  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\n  position: relative;\n  overflow: hidden;\n}\n\n.feature::before {\n  content: '';\n  position: absolute;\n  top: 0;\n  left: 0;\n  right: 0;\n  height: 3px;\n  background: linear-gradient(90deg, var(--ifm-color-primary), var(--ifm-color-primary-dark));\n  transform: scaleX(0);\n  transform-origin: left;\n  transition: transform 0.3s ease;\n}\n\n.feature:hover::before {\n  transform: scaleX(1);\n}\n\n.feature:hover {\n  box-shadow: 0 12px 24px rgba(0, 0, 0, 0.12);\n  transform: translateY(-4px);\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .feature,\n  .featureSvg,\n  .feature:hover,\n  .feature:hover .featureSvg {\n    transition: none;\n    transform: none;\n  }\n}\n"
  },
  {
    "path": "docs/src/components/HomepageFeatures.tsx",
    "content": "import Heading from '@theme/Heading';\nimport styles from './HomepageFeatures.module.css';\nimport type { ComponentType, ComponentProps, ReactNode } from 'react';\n\ntype FeatureItem = {\n  title: string;\n  Svg: ComponentType<ComponentProps<'svg'>>;\n  description: ReactNode;\n};\n\nconst FeatureList: FeatureItem[] = [\n  {\n    title: 'Easy to Use',\n    Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default,\n    description: (\n      <>\n        Install and configure Claude Code Router in minutes with our simple CLI.\n        Start routing your Claude Code requests to any LLM provider instantly.\n      </>\n    ),\n  },\n  {\n    title: 'Flexible Routing',\n    Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default,\n    description: (\n      <>\n        Configure custom routing logic based on project, context length, or task type.\n        Built-in scenarios for background tasks, thinking mode, web search, and more.\n      </>\n    ),\n  },\n  {\n    title: 'Provider Agnostic',\n    Svg: require('@site/static/img/undraw_docusaurus_react.svg').default,\n    description: (\n      <>\n        Support for multiple LLM providers including DeepSeek, Gemini, Groq, and OpenRouter.\n        Easy to extend with custom transformers for new providers.\n      </>\n    ),\n  },\n];\n\nfunction Feature({title, Svg, description}: FeatureItem) {\n  return (\n    <div className={styles.feature}>\n      <div className=\"text--center\">\n        <Svg className={styles.featureSvg} role=\"img\" />\n      </div>\n      <div className=\"text--center padding-horiz--md\">\n        <Heading as=\"h3\">{title}</Heading>\n        <p>{description}</p>\n      </div>\n    </div>\n  );\n}\n\nexport default function HomepageFeatures() {\n  return (\n    <section className={styles.features}>\n      <div className=\"container\">\n        <div className=\"row\">\n          {FeatureList.map((props, idx) => (\n            <div key={idx} className=\"col col--4\">\n              <Feature {...props} />\n            </div>\n          ))}\n        </div>\n      </div>\n    </section>\n  );\n}\n"
  },
  {
    "path": "docs/src/css/custom.css",
    "content": "/**\n * Any CSS included here will be global. The classic template\n * bundles Infima by default. Infima is a CSS framework designed to\n * work well for content-centric websites.\n */\n\n/* Tailwind CSS directives */\n@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n/* You can override the default Infima variables here. */\n:root {\n  --ifm-color-primary: #CC7C5E;\n  --ifm-color-primary-dark: #BC5C3E;\n  --ifm-color-primary-darker: #964630;\n  --ifm-color-primary-darkest: #7A3B2C;\n  --ifm-color-primary-light: #F28B68;\n  --ifm-color-primary-lighter: #F7AC92;\n  --ifm-color-primary-lightest: #FEF6F3;\n  --ifm-code-font-size: 95%;\n  --docusaurus-highlighted-code-line-bg: rgba(204, 124, 94, 0.1);\n\n  /* Custom animations */\n  --animation-duration-slow: 0.6s;\n  --animation-duration-normal: 0.3s;\n  --animation-duration-fast: 0.15s;\n}\n\n/* Apply Tailwind Typography to Docusaurus markdown content */\n@layer components {\n  /* Apply prose styles to article content (main markdown container in Docusaurus) */\n  article {\n    @apply prose prose-neutral max-w-none;\n    @apply prose-headings:font-bold prose-headings:text-neutral-900;\n    @apply prose-h1:text-3xl prose-h1:mt-8 prose-h1:mb-4;\n    @apply prose-h2:text-2xl prose-h2:mt-6 prose-h2:mb-3;\n    @apply prose-h3:text-xl prose-h3:mt-5 prose-h3:mb-2;\n    @apply prose-h4:text-lg prose-h4:mt-4 prose-h4:mb-2;\n    @apply prose-p:text-base prose-p:leading-7 prose-p:mb-4;\n    @apply prose-a:text-primary-600 prose-a:no-underline hover:prose-a:underline;\n    @apply prose-strong:text-neutral-900 prose-strong:font-semibold;\n    @apply prose-code:text-primary-600 prose-code:px-1 prose-code:py-0.5 prose-code:bg-neutral-100 prose-code:rounded;\n    @apply prose-pre:bg-neutral-900 prose-pre:text-neutral-100;\n    @apply prose-blockquote:border-l-4 prose-blockquote:border-primary-600 prose-blockquote:pl-4 prose-blockquote:italic;\n    @apply prose-ul:list-disc prose-ul:pl-6;\n    @apply prose-ol:list-decimal prose-ol:pl-6;\n  }\n\n  /* Handle nested markdown containers */\n  .markdown,\n  .theme-doc-markdown {\n    @apply prose prose-neutral;\n    @apply prose-headings:font-bold prose-headings:text-neutral-900;\n    @apply prose-h1:text-3xl prose-h1:mt-8 prose-h1:mb-4;\n    @apply prose-h2:text-2xl prose-h2:mt-6 prose-h2:mb-3;\n    @apply prose-h3:text-xl prose-h3:mt-5 prose-h3:mb-2;\n    @apply prose-h4:text-lg prose-h4:mt-4 prose-h4:mb-2;\n    @apply prose-p:text-base prose-p:leading-7 prose-p:mb-4;\n    @apply prose-a:text-primary-600 prose-a:no-underline hover:prose-a:underline;\n    @apply prose-strong:text-neutral-900 prose-strong:font-semibold;\n    @apply prose-code:text-primary-600 prose-code:px-1 prose-code:py-0.5 prose-code:bg-neutral-100 prose-code:rounded;\n    @apply prose-pre:bg-neutral-900 prose-pre:text-neutral-100;\n    @apply prose-blockquote:border-l-4 prose-blockquote:border-primary-600 prose-blockquote:pl-4 prose-blockquote:italic;\n    @apply prose-ul:list-disc prose-ul:pl-6;\n    @apply prose-ol:list-decimal prose-ol:pl-6;\n  }\n}\n\n/* Override Docusaurus default max-width - must be outside @layer to take precedence */\n.markdown,\n.theme-doc-markdown {\n  max-width: 56rem !important;\n  margin-left: auto !important;\n  margin-right: auto !important;\n  padding-left: 1rem;\n  padding-right: 1rem;\n}\n\n@media (min-width: 640px) {\n  .markdown,\n  .theme-doc-markdown {\n    padding-left: 1.5rem;\n    padding-right: 1.5rem;\n  }\n}\n\n@media (min-width: 1024px) {\n  .markdown,\n  .theme-doc-markdown {\n    padding-left: 2rem;\n    padding-right: 2rem;\n  }\n}\n\n/* Custom styles */\n.hero {\n  padding: 4rem 0;\n  text-align: center;\n}\n\n.hero__title {\n  font-size: 3rem;\n  font-weight: 700;\n  margin-bottom: 1.5rem;\n}\n\n.hero__subtitle {\n  font-size: 1.5rem;\n  color: var(--ifm-color-emphasis-600);\n  margin-bottom: 2rem;\n}\n\n.features {\n  display: grid;\n  grid-template-columns: repeat(3, 1fr);\n  gap: 2rem;\n  margin-top: 3rem;\n}\n\n.feature {\n  padding: 1.5rem;\n  border-radius: 8px;\n  background: var(--ifm-background-surface-color);\n  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);\n}\n\n.feature__title {\n  font-size: 1.25rem;\n  font-weight: 600;\n  margin-bottom: 0.75rem;\n}\n\n.feature__text {\n  color: var(--ifm-color-emphasis-600);\n}\n\n/* Smooth scroll behavior */\nhtml {\n  scroll-behavior: smooth;\n}\n\n/* Custom scrollbar for webkit browsers */\n::-webkit-scrollbar {\n  width: 10px;\n  height: 10px;\n}\n\n::-webkit-scrollbar-track {\n  background: var(--ifm-background-surface-color);\n}\n\n::-webkit-scrollbar-thumb {\n  background: var(--ifm-color-primary);\n  border-radius: 5px;\n}\n\n::-webkit-scrollbar-thumb:hover {\n  background: var(--ifm-color-primary-dark);\n}\n\n/* Enhanced animations */\n@keyframes fadeInUp {\n  from {\n    opacity: 0;\n    transform: translateY(20px);\n  }\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n\n@keyframes fadeIn {\n  from {\n    opacity: 0;\n  }\n  to {\n    opacity: 1;\n  }\n}\n\n@keyframes slideInLeft {\n  from {\n    opacity: 0;\n    transform: translateX(-30px);\n  }\n  to {\n    opacity: 1;\n    transform: translateX(0);\n  }\n}\n\n@keyframes scaleIn {\n  from {\n    opacity: 0;\n    transform: scale(0.9);\n  }\n  to {\n    opacity: 1;\n    transform: scale(1);\n  }\n}\n\n.animate-fade-in-up {\n  animation: fadeInUp var(--animation-duration-slow) ease-out forwards;\n  opacity: 0;\n}\n\n.animate-fade-in {\n  animation: fadeIn var(--animation-duration-slow) ease-out forwards;\n  opacity: 0;\n}\n\n.animate-slide-in-left {\n  animation: slideInLeft var(--animation-duration-slow) ease-out forwards;\n  opacity: 0;\n}\n\n.animate-scale-in {\n  animation: scaleIn var(--animation-duration-slow) ease-out forwards;\n  opacity: 0;\n}\n\n/* Hover effects for cards */\n.hover-lift {\n  transition: transform var(--animation-duration-normal) ease,\n              box-shadow var(--animation-duration-normal) ease;\n}\n\n.hover-lift:hover {\n  transform: translateY(-4px);\n  box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15);\n}\n\n/* Gradient text */\n.gradient-text {\n  background: linear-gradient(135deg, var(--ifm-color-primary) 0%, var(--ifm-color-primary-dark) 100%);\n  -webkit-background-clip: text;\n  -webkit-text-fill-color: transparent;\n  background-clip: text;\n}\n\n/* Glass morphism effect */\n.glass {\n  background: rgba(255, 255, 255, 0.1);\n  backdrop-filter: blur(10px);\n  -webkit-backdrop-filter: blur(10px);\n  border: 1px solid rgba(255, 255, 255, 0.2);\n}\n\n/* Focus styles for accessibility */\n:focus-visible {\n  outline: 2px solid var(--ifm-color-primary);\n  outline-offset: 2px;\n}\n\n/* Button hover effects */\nbutton,\na {\n  transition: all var(--animation-duration-fast) ease;\n}\n\n/* Prevent navbar items from wrapping */\n.navbar__item,\n.navbar__link {\n  white-space: nowrap;\n}\n\n/* Ensure navbar links with icons stay on same line */\n.navbar__link {\n  display: inline-flex;\n  align-items: center;\n  gap: 0.25rem;\n}\n.dropdown > .navbar__link:after{\n  top: 4px;\n}\n\n/* Prevent footer links from wrapping */\n.footer__link-item {\n  display: inline-flex;\n  align-items: center;\n  gap: 0.25rem;\n  white-space: nowrap;\n}\n\n/* Prevent \"Edit this page\" button from wrapping */\n.theme-edit-this-page {\n  display: inline-flex;\n  align-items: center;\n  gap: 0.25rem;\n  white-space: nowrap;\n}\n\n/* Breadcrumbs alignment */\n.breadcrumbs {\n  margin-top: 0;\n  display: flex;\n  align-items: center;\n  flex-wrap: wrap;\n}\n\n.breadcrumbs__item {\n  display: inline-flex;\n  align-items: center;\n  line-height: 1;\n}\n\n.breadcrumbs__link {\n  display: inline-flex;\n  align-items: center;\n  gap: 0.25rem;\n  line-height: 1;\n}\n\n.breadcrumbs__link svg {\n  display: inline-flex;\n  align-items: center;\n}\n\n.breadcrumbs__link span {\n  display: inline-flex;\n  align-items: center;\n}\n\n/* Breadcrumb separator alignment */\n.breadcrumbs__item::after,\n.breadcrumbs__item--separator {\n  display: inline-flex;\n  align-items: center;\n  margin: 0 0.5rem;\n}\n\n/* Responsive improvements */\n@media (max-width: 996px) {\n  .features {\n    grid-template-columns: repeat(2, 1fr);\n  }\n}\n\n@media (max-width: 768px) {\n  .features {\n    grid-template-columns: 1fr;\n  }\n\n  .hero__title {\n    font-size: 2rem;\n  }\n\n  /* Reduce animation on mobile for better performance */\n  .animate-fade-in-up,\n  .animate-fade-in,\n  .animate-slide-in-left,\n  .animate-scale-in {\n    animation-duration: 0.3s;\n  }\n}\n\n/* Print styles */\n@media print {\n  .hero,\n  .features {\n    page-break-inside: avoid;\n  }\n}\n"
  },
  {
    "path": "docs/src/css-modules.d.ts",
    "content": "declare module '*.module.css' {\n  const classes: { [key: string]: string };\n  export default classes;\n}\n\ndeclare module '*.module.scss' {\n  const classes: { [key: string]: string };\n  export default classes;\n}\n\ndeclare module '*.module.sass' {\n  const classes: { [key: string]: string };\n  export default classes;\n}\n"
  },
  {
    "path": "docs/src/docusaurus.d.ts",
    "content": "/// <reference types=\"@docusaurus/module-type-aliases\" />\n/// <reference types=\"@docusaurus/types\" />\n\n// Additional theme component type declarations\ndeclare module '@theme/Heading' {\n  import type {ReactNode, CSSProperties} from 'react';\n\n  export type Props = {\n    readonly as?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';\n    readonly className?: string;\n    readonly style?: CSSProperties;\n    readonly children?: ReactNode;\n  };\n\n  export default function Heading(props: Props): ReactNode;\n}\n"
  },
  {
    "path": "docs/src/pages/index.tsx",
    "content": "import Link from '@docusaurus/Link';\nimport useDocusaurusContext from '@docusaurus/useDocusaurusContext';\nimport Layout from '@theme/Layout';\nimport Heading from '@theme/Heading';\n\nfunction HomepageHeader() {\n  const {siteConfig, i18n} = useDocusaurusContext();\n  const currentLocale = i18n.currentLocale;\n\n  const content = {\n    en: {\n      title: 'Route Claude Code',\n      highlight: 'to Any LLM',\n      subtitle: 'Use Claude Code without an Anthropic account. Connect to DeepSeek, Gemini, Groq, and more.',\n      getStarted: 'Get Started',\n      github: 'View on GitHub',\n    },\n    'zh-CN': {\n      title: '将 Claude Code',\n      highlight: '路由到任何 LLM',\n      subtitle: '无需 Anthropic 账户即可使用 Claude Code。支持连接 DeepSeek、Gemini、Groq 等提供商。',\n      getStarted: '开始使用',\n      github: '查看 GitHub',\n    }\n  };\n\n  const t = content[currentLocale as keyof typeof content] || content.en;\n\n  return (\n    <header className=\"relative min-h-[90vh] flex items-center justify-center overflow-hidden bg-gradient-to-b from-gray-50 to-white\">\n      {/* Background Elements */}\n      <div\n        className=\"absolute inset-0\"\n        style={{\n          backgroundImage: `\n            linear-gradient(rgba(204, 124, 94, 0.03) 1px, transparent 1px),\n            linear-gradient(90deg, rgba(204, 124, 94, 0.03) 1px, transparent 1px)\n          `,\n          backgroundSize: '50px 50px'\n        }}\n      ></div>\n\n      {/* Hero Content */}\n      <div className=\"relative z-10 container mx-auto px-4 py-20\">\n        <div className=\"max-w-4xl mx-auto text-center\">\n          {/* Badge */}\n          <div className=\"animate-fade-in-up inline-flex items-center gap-2 px-4 py-2 bg-primary text-white rounded-full text-sm font-semibold mb-8\">\n            <span className=\"animate-pulse\">●</span>\n            <span>{currentLocale === 'zh-CN' ? '开源免费' : 'Open Source'}</span>\n          </div>\n\n          {/* Title */}\n          <Heading\n            as=\"h1\"\n            className=\"text-5xl sm:text-6xl md:text-7xl lg:text-8xl font-extrabold mb-6 leading-tight animate-fade-in-up\"\n            style={{ animationDelay: '0.1s' }}\n          >\n            <span className=\"text-gray-900\">Route </span>\n            <span className=\"text-primary\">Claude Code</span>\n            <br />\n            <span className=\"text-gray-900\">{t.highlight}</span>\n          </Heading>\n\n          {/* Subtitle */}\n          <p\n            className=\"text-lg sm:text-xl md:text-2xl text-gray-600 mb-10 leading-relaxed max-w-3xl mx-auto animate-fade-in-up\"\n            style={{ animationDelay: '0.2s' }}\n          >\n            {t.subtitle}\n          </p>\n\n          {/* CTA Buttons */}\n          <div\n            className=\"flex flex-col sm:flex-row gap-4 justify-center items-center mb-12 animate-fade-in-up\"\n            style={{ animationDelay: '0.3s' }}\n          >\n            <Link\n              className=\"px-6 sm:px-8 py-3 sm:py-4 bg-primary text-white hover:text-white rounded-lg text-base sm:text-lg font-semibold transition-all duration-300 shadow-lg hover:shadow-xl hover:scale-105\"\n              to={currentLocale === 'zh-CN' ? '/zh-CN/docs/cli/quick-start' : '/docs/cli/quick-start'}\n            >\n              {t.getStarted}\n            </Link>\n            <Link\n              className=\"px-6 sm:px-8 py-3 sm:py-4 bg-white hover:bg-gray-50 text-gray-900 border border-gray-300 rounded-lg text-base sm:text-lg font-semibold transition-all duration-300 hover:border-primary hover:scale-105\"\n              to=\"https://github.com/musistudio/claude-code-router\"\n            >\n              {t.github}\n            </Link>\n          </div>\n\n          {/* Install Command */}\n          <div\n            className=\"animate-fade-in-up max-w-3xl mx-auto mt-16\"\n            style={{ animationDelay: '0.4s' }}\n          >\n            <div\n              className=\"rounded-xl overflow-hidden\"\n              style={{\n                background: 'linear-gradient(145deg, #1e1e1e 0%, #0d0d0d 100%)',\n                boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.1)',\n              }}\n            >\n              {/* Terminal Header */}\n              <div\n                className=\"px-5 py-3 flex items-center justify-between\"\n                style={{\n                  background: 'linear-gradient(180deg, #2a2a2a 0%, #1f1f1f 100%)',\n                  borderBottom: '1px solid rgba(255, 255, 255, 0.1)',\n                }}\n              >\n                <div className=\"flex items-center gap-2\">\n                  <div className=\"flex gap-2\">\n                    <div className=\"w-3 h-3 rounded-full bg-red-500 hover:bg-red-400 transition-colors\"></div>\n                    <div className=\"w-3 h-3 rounded-full bg-yellow-500 hover:bg-yellow-400 transition-colors\"></div>\n                    <div className=\"w-3 h-3 rounded-full bg-green-500 hover:bg-green-400 transition-colors\"></div>\n                  </div>\n                  <span className=\"ml-4 text-xs text-gray-400 font-mono flex items-center gap-2\">\n                    <svg className=\"w-4 h-4\" fill=\"currentColor\" viewBox=\"0 0 20 20\">\n                      <path fillRule=\"evenodd\" d=\"M2 5a2 2 0 012-2h12a2 2 0 012 2v10a2 2 0 01-2 2H4a2 2 0 01-2-2V5zm3.293 1.293a1 1 0 011.414 0l3 3a1 1 0 010 1.414l-3 3a1 1 0 01-1.414-1.414L7.586 10 5.293 7.707a1 1 0 010-1.414zM11 12a1 1 0 100 2h3a1 1 0 100-2h-3z\" clipRule=\"evenodd\" />\n                    </svg>\n                    terminal\n                  </span>\n                </div>\n\n                {/* Copy Button */}\n                <button\n                  onClick={() => navigator.clipboard.writeText('npm install -g @musistudio/claude-code-router')}\n                  className=\"flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs font-mono transition-all duration-200 hover:bg-white/10\"\n                  style={{ color: '#9ca3af' }}\n                  title=\"Copy to clipboard\"\n                >\n                  <svg className=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n                    <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\" />\n                  </svg>\n                  <span>Copy</span>\n                </button>\n              </div>\n\n              {/* Terminal Body */}\n              <div className=\"p-8\" style={{ background: '#0d0d0d' }}>\n                <div className=\"font-mono text-base md:text-lg leading-relaxed text-left\">\n                  {/* Prompt */}\n                  <div className=\"flex items-start gap-3\">\n                    <span style={{ color: '#22c55e', fontWeight: '600' }}>$</span>\n                    <span className=\"flex-1\">\n                      <span style={{ color: '#60a5fa' }}>npm</span>\n                      <span style={{ color: '#f97583', marginLeft: '0.5rem' }}>install</span>\n                      <span style={{ color: '#fbbf24', marginLeft: '0.5rem' }}>-g</span>\n                      <span style={{ color: '#e5e7eb', marginLeft: '0.5rem' }}>\n                        @musistudio/claude-code-router\n                      </span>\n                    </span>\n                  </div>\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n\n      {/* Scroll Indicator */}\n      <div className=\"absolute bottom-8 left-1/2 -translate-x-1/2 animate-bounce\">\n        <svg className=\"w-6 h-6 text-primary\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n          <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M19 14l-7 7m0 0l-7-7m7 7V3\" />\n        </svg>\n      </div>\n    </header>\n  );\n}\n\nfunction FeatureSection() {\n  const {i18n} = useDocusaurusContext();\n  const currentLocale = i18n.currentLocale;\n\n  const content = {\n    en: {\n      title: 'Why Claude Code Router?',\n      features: [\n        {\n          emoji: '⚡',\n          title: 'Lightning Fast',\n          description: 'Get started in minutes with just one command. No complicated configuration required.'\n        },\n        {\n          emoji: '🎯',\n          title: 'Smart Routing',\n          description: 'Automatically route requests to the best model based on context length, task type, and custom rules.'\n        },\n        {\n          emoji: '🔌',\n          title: 'Multi-Provider',\n          description: 'Support for DeepSeek, Gemini, Groq, OpenRouter, and more. Easy to extend with custom transformers.'\n        },\n        {\n          emoji: '💰',\n          title: 'Cost Effective',\n          description: 'Use more affordable models for routine tasks while reserving Claude for complex scenarios.'\n        },\n        {\n          emoji: '🛠️',\n          title: 'Agent System',\n          description: 'Extendable agent architecture for custom tools and workflows. Built-in support for image tasks.'\n        },\n        {\n          emoji: '🔧',\n          title: 'Highly Customizable',\n          description: 'Configure routing per project, set up transformers, and fine-tune every aspect of your workflow.'\n        }\n      ]\n    },\n    'zh-CN': {\n      title: '为什么选择 Claude Code Router？',\n      features: [\n        {\n          emoji: '⚡',\n          title: '快速上手',\n          description: '只需一条命令即可开始使用，无需复杂配置。'\n        },\n        {\n          emoji: '🎯',\n          title: '智能路由',\n          description: '基于上下文长度、任务类型和自定义规则，自动将请求路由到最佳模型。'\n        },\n        {\n          emoji: '🔌',\n          title: '多提供商支持',\n          description: '支持 DeepSeek、Gemini、Groq、OpenRouter 等多个提供商，易于扩展。'\n        },\n        {\n          emoji: '💰',\n          title: '节省成本',\n          description: '常规任务使用更经济的模型，复杂场景再使用 Claude。'\n        },\n        {\n          emoji: '🛠️',\n          title: 'Agent 系统',\n          description: '可扩展的 agent 架构，支持自定义工具和工作流。内置图像任务支持。'\n        },\n        {\n          emoji: '🔧',\n          title: '高度可定制',\n          description: '按项目配置路由、设置转换器，微调工作流的每个细节。'\n        }\n      ]\n    }\n  };\n\n  const t = content[currentLocale as keyof typeof content] || content.en;\n\n  return (\n    <section className=\"py-16 sm:py-24 bg-white\">\n      <div className=\"container mx-auto px-4\">\n        <div className=\"text-center mb-12 sm:mb-16\">\n          <Heading as=\"h2\" className=\"text-3xl sm:text-4xl md:text-5xl font-bold text-gray-900 mb-4\">\n            {t.title}\n          </Heading>\n        </div>\n\n        <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 sm:gap-8 max-w-7xl mx-auto\">\n          {t.features.map((feature, idx) => (\n            <div\n              key={idx}\n              className=\"group p-6 sm:p-8 bg-white rounded-2xl border border-gray-200 hover:border-primary transition-all duration-300 hover:shadow-2xl hover:-translate-y-2\"\n              style={{ animationDelay: `${idx * 0.1}s` }}\n            >\n              <div className=\"text-4xl sm:text-5xl mb-4\">{feature.emoji}</div>\n              <Heading as=\"h3\" className=\"text-lg sm:text-xl font-bold text-gray-900 mb-3\">\n                {feature.title}\n              </Heading>\n              <p className=\"text-gray-600 leading-relaxed text-sm sm:text-base\">\n                {feature.description}\n              </p>\n            </div>\n          ))}\n        </div>\n      </div>\n    </section>\n  );\n}\n\nfunction CodeDemo() {\n  const {i18n} = useDocusaurusContext();\n  const currentLocale = i18n.currentLocale;\n\n  const content = {\n    en: {\n      title: 'Simple Configuration',\n      subtitle: 'Configure your providers and routing logic with a single JSON file.'\n    },\n    'zh-CN': {\n      title: '简单配置',\n      subtitle: '使用单个 JSON 文件配置提供商和路由逻辑。'\n    }\n  };\n\n  const t = content[currentLocale as keyof typeof content] || content.en;\n\n  return (\n    <section className=\"py-16 sm:py-24 bg-gray-50\">\n      <div className=\"container mx-auto px-4\">\n        <div className=\"text-center mb-12 sm:mb-16\">\n          <Heading as=\"h2\" className=\"text-3xl sm:text-4xl md:text-5xl font-bold text-gray-900 mb-4\">\n            {t.title}\n          </Heading>\n          <p className=\"text-lg sm:text-xl text-gray-600 max-w-2xl mx-auto\">\n            {t.subtitle}\n          </p>\n        </div>\n\n        <div className=\"max-w-4xl mx-auto\">\n          <div\n            className=\"rounded-2xl overflow-hidden shadow-2xl\"\n            style={{\n              background: '#1a1a1a',\n              border: '2px solid #374151'\n            }}\n          >\n            <div\n              className=\"px-6 py-4 flex items-center gap-2\"\n              style={{ background: '#2d2d2d', borderBottom: '1px solid #374151' }}\n            >\n              <div className=\"w-3 h-3 rounded-full bg-red-500\"></div>\n              <div className=\"w-3 h-3 rounded-full bg-yellow-500\"></div>\n              <div className=\"w-3 h-3 rounded-full bg-green-500\"></div>\n              <span className=\"ml-4 text-sm text-gray-300 font-mono\">config.json</span>\n            </div>\n            <pre className=\"p-8 overflow-x-auto text-sm md:text-base\" style={{ background: '#1a1a1a' }}>\n              <code className=\"font-mono\" style={{ lineHeight: '1.6' }}>\n                <span style={{ color: '#f97583' }}>{'{'}</span>\n                <span style={{ color: '#e5e7eb' }}>\n                  {'\\n  '}\n                  <span style={{ color: '#79c0ff' }}>\"Providers\"</span>\n                  <span style={{ color: '#f97583' }}>: </span>\n                  <span style={{ color: '#f97583' }}>[</span>\n                  {'\\n    '}\n                  <span style={{ color: '#f97583' }}>{'{'}</span>\n                  {'\\n      '}\n                  <span style={{ color: '#79c0ff' }}>\"NAME\"</span>\n                  <span style={{ color: '#f97583' }}>: </span>\n                  <span style={{ color: '#a5d6ff' }}>\"deepseek\"</span>\n                  <span style={{ color: '#f97583' }}>,</span>\n                  {'\\n      '}\n                  <span style={{ color: '#79c0ff' }}>\"HOST\"</span>\n                  <span style={{ color: '#f97583' }}>: </span>\n                  <span style={{ color: '#a5d6ff' }}>\"https://api.deepseek.com\"</span>\n                  <span style={{ color: '#f97583' }}>,</span>\n                  {'\\n      '}\n                  <span style={{ color: '#79c0ff' }}>\"APIKEY\"</span>\n                  <span style={{ color: '#f97583' }}>: </span>\n                  <span style={{ color: '#a5d6ff' }}>\"your-api-key\"</span>\n                  <span style={{ color: '#f97583' }}>,</span>\n                  {'\\n      '}\n                  <span style={{ color: '#79c0ff' }}>\"MODELS\"</span>\n                  <span style={{ color: '#f97583' }}>: </span>\n                  <span style={{ color: '#f97583' }}>[</span>\n                  <span style={{ color: '#a5d6ff' }}>\"deepseek-chat\"</span>\n                  <span style={{ color: '#f97583' }}>, </span>\n                  <span style={{ color: '#a5d6ff' }}>\"deepseek-coder\"</span>\n                  <span style={{ color: '#f97583' }}>]</span>\n                  {'\\n    '}\n                  <span style={{ color: '#f97583' }}>{'}'}</span>\n                  <span style={{ color: '#f97583' }}>,</span>\n                  {'\\n    '}\n                  <span style={{ color: '#f97583' }}>{'{'}</span>\n                  {'\\n      '}\n                  <span style={{ color: '#79c0ff' }}>\"NAME\"</span>\n                  <span style={{ color: '#f97583' }}>: </span>\n                  <span style={{ color: '#a5d6ff' }}>\"groq\"</span>\n                  <span style={{ color: '#f97583' }}>,</span>\n                  {'\\n      '}\n                  <span style={{ color: '#79c0ff' }}>\"HOST\"</span>\n                  <span style={{ color: '#f97583' }}>: </span>\n                  <span style={{ color: '#a5d6ff' }}>\"https://api.groq.com/openai/v1\"</span>\n                  <span style={{ color: '#f97583' }}>,</span>\n                  {'\\n      '}\n                  <span style={{ color: '#79c0ff' }}>\"APIKEY\"</span>\n                  <span style={{ color: '#f97583' }}>: </span>\n                  <span style={{ color: '#a5d6ff' }}>\"your-groq-key\"</span>\n                  <span style={{ color: '#f97583' }}>,</span>\n                  {'\\n      '}\n                  <span style={{ color: '#79c0ff' }}>\"MODELS\"</span>\n                  <span style={{ color: '#f97583' }}>: </span>\n                  <span style={{ color: '#f97583' }}>[</span>\n                  <span style={{ color: '#a5d6ff' }}>\"llama-3.3-70b-versatile\"</span>\n                  <span style={{ color: '#f97583' }}>]</span>\n                  {'\\n    '}\n                  <span style={{ color: '#f97583' }}>{'}'}</span>\n                  {'\\n  '}\n                  <span style={{ color: '#f97583' }}>]</span>\n                  <span style={{ color: '#f97583' }}>,</span>\n                  {'\\n  '}\n                  <span style={{ color: '#79c0ff' }}>\"Router\"</span>\n                  <span style={{ color: '#f97583' }}>: </span>\n                  <span style={{ color: '#f97583' }}>{'{'}</span>\n                  {'\\n    '}\n                  <span style={{ color: '#79c0ff' }}>\"default\"</span>\n                  <span style={{ color: '#f97583' }}>: </span>\n                  <span style={{ color: '#a5d6ff' }}>\"deepseek,deepseek-chat\"</span>\n                  <span style={{ color: '#f97583' }}>,</span>\n                  {'\\n    '}\n                  <span style={{ color: '#79c0ff' }}>\"background\"</span>\n                  <span style={{ color: '#f97583' }}>: </span>\n                  <span style={{ color: '#a5d6ff' }}>\"groq,llama-3.3-70b-versatile\"</span>\n                  {'\\n  '}\n                  <span style={{ color: '#f97583' }}>{'}'}</span>\n                  {'\\n'}\n                  <span style={{ color: '#f97583' }}>{'}'}</span>\n                </span>\n              </code>\n            </pre>\n          </div>\n        </div>\n      </div>\n    </section>\n  );\n}\n\nfunction UseCases() {\n  const {i18n} = useDocusaurusContext();\n  const currentLocale = i18n.currentLocale;\n\n  const content = {\n    en: {\n      title: 'Perfect For',\n      subtitle: 'See how Claude Code Router fits your workflow',\n      cases: [\n        {\n          icon: '💰',\n          title: 'Cost-Conscious Developers',\n          description: 'Reduce API costs by 10x while maintaining quality for most tasks'\n        },\n        {\n          icon: '🔒',\n          title: 'Privacy-Focused Teams',\n          description: 'Keep code local with self-hosted models while using Claude when needed'\n        },\n        {\n          icon: '🔄',\n          title: 'Multi-Model Workflows',\n          description: 'Use different models for different tasks without switching tools'\n        },\n        {\n          icon: '⚡',\n          title: 'Claude Code Power Users',\n          description: 'Extend Claude Code with custom providers and routing strategies'\n        }\n      ]\n    },\n    'zh-CN': {\n      title: '适用场景',\n      subtitle: '看看 Claude Code Router 如何融入您的工作流程',\n      cases: [\n        {\n          icon: '💰',\n          title: '注重成本的开发者',\n          description: '在大多数任务上保持质量的同时，将 API 成本降低 10 倍'\n        },\n        {\n          icon: '🔒',\n          title: '注重隐私的团队',\n          description: '使用自托管模型保持代码本地化，需要时再使用 Claude'\n        },\n        {\n          icon: '🔄',\n          title: '多模型工作流',\n          description: '为不同任务使用不同模型，无需切换工具'\n        },\n        {\n          icon: '⚡',\n          title: 'Claude Code 高级用户',\n          description: '使用自定义提供商和路由策略扩展 Claude Code'\n        }\n      ]\n    }\n  };\n\n  const t = content[currentLocale as keyof typeof content] || content.en;\n\n  return (\n    <section className=\"py-16 sm:py-24 bg-gradient-to-b from-white to-gray-50\">\n      <div className=\"container mx-auto px-4\">\n        <div className=\"text-center mb-12 sm:mb-16\">\n          <Heading as=\"h2\" className=\"text-3xl sm:text-4xl md:text-5xl font-bold text-gray-900 mb-4\">\n            {t.title}\n          </Heading>\n          <p className=\"text-lg sm:text-xl text-gray-600 max-w-2xl mx-auto\">\n            {t.subtitle}\n          </p>\n        </div>\n\n        <div className=\"grid grid-cols-1 md:grid-cols-2 gap-6 sm:gap-8 max-w-5xl mx-auto\">\n          {t.cases.map((useCase, idx) => (\n            <div\n              key={idx}\n              className=\"group relative p-6 sm:p-8 bg-white rounded-2xl border-2 border-gray-100 hover:border-primary transition-all duration-300 hover:shadow-2xl hover:-translate-y-1 overflow-hidden\"\n              style={{ animationDelay: `${idx * 0.1}s` }}\n            >\n              {/* Background decoration */}\n              <div\n                className=\"absolute top-0 right-0 w-32 h-32 rounded-full opacity-5 group-hover:opacity-10 transition-opacity\"\n                style={{\n                  background: 'linear-gradient(135deg, #CC7C5E 0%, transparent 70%)',\n                  transform: 'translate(30%, -30%)'\n                }}\n              ></div>\n\n              {/* Number badge */}\n              <div className=\"absolute top-4 sm:top-6 right-4 sm:right-6 w-8 h-8 sm:w-10 sm:h-10 flex items-center justify-center rounded-full text-base sm:text-lg font-bold transition-all duration-300 group-hover:scale-110\"\n                style={{\n                  background: 'linear-gradient(135deg, #CC7C5E 0%, #BC5C3E 100%)',\n                  color: 'white',\n                  boxShadow: '0 4px 12px rgba(204, 124, 94, 0.3)'\n                }}\n              >\n                {idx + 1}\n              </div>\n\n              {/* Icon */}\n              <div className=\"text-4xl sm:text-5xl mb-4 group-hover:scale-110 transition-transform duration-300\">\n                {useCase.icon}\n              </div>\n\n              {/* Content */}\n              <div>\n                <Heading as=\"h3\" className=\"text-lg sm:text-xl font-bold text-gray-900 mb-3 group-hover:text-primary transition-colors\">\n                  {useCase.title}\n                </Heading>\n                <p className=\"text-gray-600 leading-relaxed text-sm sm:text-base\">\n                  {useCase.description}\n                </p>\n              </div>\n\n              {/* Hover accent line */}\n              <div\n                className=\"absolute bottom-0 left-0 h-1 bg-gradient-to-r from-primary to-primary/70 transition-all duration-300 group-hover:w-full\"\n                style={{ width: '0%' }}\n              ></div>\n            </div>\n          ))}\n        </div>\n      </div>\n    </section>\n  );\n}\n\nfunction CTASection() {\n  const {i18n} = useDocusaurusContext();\n  const currentLocale = i18n.currentLocale;\n\n  const content = {\n    en: {\n      title: 'Ready to Get Started?',\n      subtitle: 'Join thousands of developers using Claude Code Router to build better software.',\n      getStarted: 'Get Started',\n      docs: 'Read the Docs',\n      github: 'Star on GitHub',\n      community: 'Join Community'\n    },\n    'zh-CN': {\n      title: '准备开始了吗？',\n      subtitle: '加入数千名使用 Claude Code Router 的开发者，构建更好的软件。',\n      getStarted: '开始使用',\n      docs: '阅读文档',\n      github: '在 GitHub 上点赞',\n      community: '加入社区'\n    }\n  };\n\n  const t = content[currentLocale as keyof typeof content] || content.en;\n\n  return (\n    <section className=\"relative py-20 sm:py-32 overflow-hidden\">\n      {/* Animated background */}\n      <div className=\"absolute inset-0\">\n        <div className=\"absolute inset-0 bg-gradient-to-br from-primary via-primary/90 to-primary/80\"></div>\n        {/* Pattern overlay */}\n        <div\n          className=\"absolute inset-0 opacity-10\"\n          style={{\n            backgroundImage: `\n              radial-gradient(circle at 25% 25%, white 1%, transparent 1%),\n              radial-gradient(circle at 75% 75%, white 1%, transparent 1%)\n            `,\n            backgroundSize: '40px 40px'\n          }}\n        ></div>\n        {/* Floating orbs */}\n        <div className=\"absolute top-20 left-10 w-64 h-64 bg-white/10 rounded-full blur-3xl animate-pulse\"></div>\n        <div className=\"absolute bottom-20 right-10 w-96 h-96 bg-white/5 rounded-full blur-3xl animate-pulse\" style={{ animationDelay: '1s' }}></div>\n      </div>\n\n      <div className=\"relative container mx-auto px-4\">\n        <div className=\"max-w-4xl mx-auto text-center\">\n          {/* Main title */}\n          <Heading as=\"h2\" className=\"text-4xl sm:text-5xl md:text-6xl font-bold text-white mb-6 leading-tight\">\n            {t.title}\n          </Heading>\n\n          {/* Subtitle */}\n          <p className=\"text-lg sm:text-xl md:text-2xl text-white/95 mb-10 sm:mb-12 max-w-3xl mx-auto leading-relaxed\">\n            {t.subtitle}\n          </p>\n\n          {/* Primary CTA buttons */}\n          <div className=\"flex flex-col sm:flex-row gap-4 justify-center mb-8 sm:mb-12\">\n            <Link\n              className=\"group px-8 sm:px-10 py-4 sm:py-5 bg-white text-primary hover:bg-gray-50 rounded-xl text-base sm:text-lg font-bold transition-all duration-300 shadow-2xl hover:shadow-white/20 hover:scale-105 flex items-center justify-center gap-2\"\n              to={currentLocale === 'zh-CN' ? '/zh-CN/docs/cli/quick-start' : '/docs/cli/quick-start'}\n            >\n              {t.getStarted}\n              <svg className=\"w-5 h-5 group-hover:translate-x-1 transition-transform\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n                <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M13 7l5 5m0 0l-5 5m5-5H6\" />\n              </svg>\n            </Link>\n            <Link\n              className=\"px-8 sm:px-10 py-4 sm:py-5 bg-white/10 backdrop-blur-sm text-white border-2 border-white/30 hover:bg-primary hover:border-primary hover:text-white rounded-xl text-base sm:text-lg font-bold transition-all duration-300 hover:scale-105 flex items-center justify-center gap-2\"\n              to={currentLocale === 'zh-CN' ? '/zh-CN/docs/category/cli' : '/docs/category/cli'}\n            >\n              {t.docs}\n              <svg className=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n                <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253\" />\n              </svg>\n            </Link>\n          </div>\n\n          {/* Secondary links */}\n          <div className=\"flex flex-col sm:flex-row gap-6 justify-center items-center text-white/80\">\n            <a\n              href=\"https://github.com/musistudio/claude-code-router\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              className=\"flex items-center gap-2 hover:text-white transition-colors group\"\n            >\n              <svg className=\"w-5 h-5 group-hover:scale-110 transition-transform\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\n                <path fillRule=\"evenodd\" d=\"M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z\" clipRule=\"evenodd\" />\n              </svg>\n              <span className=\"text-sm font-medium\">{t.github}</span>\n            </a>\n\n            <div className=\"hidden sm:block w-px h-4 bg-white/30\"></div>\n\n            <a\n              href=\"https://github.com/musistudio/claude-code-router/discussions\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              className=\"flex items-center gap-2 hover:text-white transition-colors group\"\n            >\n              <svg className=\"w-5 h-5 group-hover:scale-110 transition-transform\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n                <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z\" />\n              </svg>\n              <span className=\"text-sm font-medium\">{t.community}</span>\n            </a>\n          </div>\n        </div>\n      </div>\n    </section>\n  );\n}\n\nexport default function Home() {\n  return (\n    <Layout>\n      <HomepageHeader />\n      <main>\n        <FeatureSection />\n        <CodeDemo />\n        <UseCases />\n        <CTASection />\n      </main>\n    </Layout>\n  );\n}\n"
  },
  {
    "path": "docs/tailwind.config.js",
    "content": "/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n  content: [\n    './src/**/*.{js,jsx,ts,tsx}',\n  ],\n  darkMode: 'class',\n  theme: {\n    extend: {\n      colors: {\n        primary: {\n          DEFAULT: '#CC7C5E',\n          50: '#FEF6F3',\n          100: '#FDE8E0',\n          200: '#FBD2C1',\n          300: '#F7AC92',\n          400: '#F28B68',\n          500: '#CC7C5E',\n          600: '#BC5C3E',\n          700: '#964630',\n          800: '#7A3B2C',\n          900: '#643528',\n        },\n      },\n      typography: ({ theme }) => ({\n        DEFAULT: {\n          css: {\n            '--tw-prose-body': theme('colors.neutral.800'),\n            '--tw-prose-headings': theme('colors.neutral.900'),\n            '--tw-prose-links': theme('colors.primary.600'),\n            '--tw-prose-bold': theme('colors.neutral.900'),\n            '--tw-prose-counters': theme('colors.primary.600'),\n            '--tw-prose-bullets': theme('colors.primary.600'),\n            '--tw-prose-hr': theme('colors.neutral.300'),\n            '--tw-prose-quotes': theme('colors.neutral.900'),\n            '--tw-prose-quote-borders': theme('colors.primary.600'),\n            '--tw-prose-code': theme('colors.primary.600'),\n            '--tw-prose-pre-code': theme('colors.neutral.200'),\n            '--tw-prose-pre-bg': theme('colors.neutral.800'),\n            '--tw-prose-th-borders': theme('colors.neutral.300'),\n            '--tw-prose-td-borders': theme('colors.neutral.200'),\n          },\n        },\n      }),\n      animation: {\n        'fade-in-up': 'fadeInUp 0.6s ease-out',\n        'fade-in': 'fadeIn 0.6s ease-out',\n        'slide-in': 'slideIn 0.6s ease-out',\n      },\n      keyframes: {\n        fadeInUp: {\n          '0%': { opacity: '0', transform: 'translateY(20px)' },\n          '100%': { opacity: '1', transform: 'translateY(0)' },\n        },\n        fadeIn: {\n          '0%': { opacity: '0' },\n          '100%': { opacity: '1' },\n        },\n        slideIn: {\n          '0%': { transform: 'translateX(-100%)' },\n          '100%': { transform: 'translateX(0)' },\n        },\n      },\n    },\n  },\n  plugins: [\n    require('@tailwindcss/typography'),\n  ],\n}\n"
  },
  {
    "path": "docs/tsconfig.json",
    "content": "{\n  \"extends\": \"../tsconfig.base.json\",\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"jsx\": \"react-jsx\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Bundler\",\n    \"target\": \"ES2020\",\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"],\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": true\n  },\n  \"include\": [\"src\", \"docs\"],\n  \"exclude\": [\"node_modules\", \"build\"]\n}\n"
  },
  {
    "path": "examples/README.md",
    "content": "# Preset 示例说明\n\n本目录包含 CCR 预设配置的示例文件。\n\n## 示例文件\n\n### 1. `simple-preset-example.json` - 简单示例\n适合初学者，展示了基本的动态配置功能：\n- 密码输入（API Key）\n- 单选下拉框（选择模型）\n- 确认框（是否使用代理）\n- 条件显示（只有选择使用代理时才显示代理地址输入）\n\n**使用场景**：快速配置单个 Provider\n\n### 2. `preset-manifest-example.json` - 完整示例\n展示了所有高级功能：\n- 多种输入类型（password, select, confirm, number, multiselect）\n- 动态选项（从 Providers 配置中提取）\n- 复杂条件逻辑（when 条件）\n- 模板变量替换（{{variable}}）\n- 配置映射（configMappings）\n\n**使用场景**：生产环境的完整配置\n\n### 3. `dynamic-preset-example.json` - 多Provider示例\n展示了如何在多个 Provider 之间切换：\n- Provider 选择器\n- 根据选择的 Provider 动态显示对应的模型选项\n- 代理配置\n- 高级功能开关\n\n## 如何使用这些示例\n\n### 方法1：直接复制到预设目录\n\n```bash\n# 创建预设目录\nmkdir -p ~/.claude-code-router/presets/my-preset\n\n# 复制示例文件\ncp simple-preset-example.json ~/.claude-code-router/presets/my-preset/manifest.json\n\n# 应用预设\nccr my-preset\n```\n\n### 方法2：修改后使用\n\n1. 复制示例文件到本地\n2. 根据需要修改配置\n3. 使用 CLI 安装：\n\n```bash\nccr preset install ./simple-preset-example.json --name my-preset\n```\n\n## Schema 字段类型说明\n\n| 类型 | 说明 | 适用场景 |\n|------|------|----------|\n| `password` | 密码输入 | API Key、密钥等敏感信息 |\n| `input` | 单行文本 | Base URL、端点地址 |\n| `number` | 数字输入 | 超时时间、Token数量 |\n| `select` | 单选 | Provider选择、模型选择 |\n| `multiselect` | 多选 | 功能开关、标签选择 |\n| `confirm` | 确认框 | 是否启用某功能 |\n| `editor` | 多行文本 | 自定义配置、脚本 |\n\n## 条件运算符\n\n| 运算符 | 说明 | 示例 |\n|--------|------|------|\n| `eq` | 等于 | 当 provider == \"openai\" 时显示 |\n| `ne` | 不等于 | 当 mode != \"simple\" 时显示 |\n| `exists` | 字段存在 | 当 apiKey 有值时显示 |\n| `gt/lt` | 大于/小于 | 当 timeout > 30 时显示 |\n\n## 动态选项类型\n\n### static - 静态选项\n```json\n\"options\": {\n  \"type\": \"static\",\n  \"options\": [\n    {\"label\": \"选项1\", \"value\": \"value1\"},\n    {\"label\": \"选项2\", \"value\": \"value2\"}\n  ]\n}\n```\n\n### providers - 从 Providers 配置提取\n```json\n\"options\": {\n  \"type\": \"providers\"\n}\n```\n自动从 `Providers` 数组中提取 name 作为选项。\n\n### models - 从指定 Provider 的 models 提取\n```json\n\"options\": {\n  \"type\": \"models\",\n  \"providerField\": \"{{selectedProvider}}\"\n}\n```\n根据用户选择的 Provider，动态显示该 Provider 的 models。\n\n## 模板变量\n\n使用 `{{变量名}}` 语法在 template 中引用用户输入：\n\n```json\n\"template\": {\n  \"Providers\": [\n    {\n      \"name\": \"{{providerName}}\",\n      \"api_key\": \"{{apiKey}}\"\n    }\n  ]\n}\n```\n\n## 配置映射\n\n对于复杂的配置需求，使用 `configMappings` 精确控制值的位置：\n\n```json\n\"configMappings\": [\n  {\n    \"target\": \"Providers[0].api_key\",\n    \"value\": \"{{apiKey}}\"\n  },\n  {\n    \"target\": \"PROXY_URL\",\n    \"value\": \"{{proxyUrl}}\",\n    \"when\": {\n      \"field\": \"useProxy\",\n      \"operator\": \"eq\",\n      \"value\": true\n    }\n  }\n]\n```\n\n## 最佳实践\n\n1. **提供默认值**：为非必填项设置合理的 `defaultValue`\n2. **清晰的标签**：使用用户友好的 `label` 和 `prompt`\n3. **条件显示**：使用 `when` 避免显示无关选项\n4. **输入验证**：使用 `validator` 或 `min/max` 确保输入有效\n5. **分组配置**：相关字段使用相同的前缀（如 `proxy*`）\n6. **版本管理**：在 metadata 中记录版本和变更\n\n## 更多帮助\n\n- 查看完整文档：[Presets 配置指南](../docs/docs/server/advanced/presets.md)\n- 查看类型定义：[types.ts](../packages/shared/src/preset/types.ts)\n"
  },
  {
    "path": "examples/dynamic-preset-example.json",
    "content": "{\n  \"metadata\": {\n    \"name\": \"multi-provider-preset\",\n    \"version\": \"1.0.0\",\n    \"description\": \"示例预设：支持多provider选择和动态配置\",\n    \"author\": \"CCR Team\",\n    \"keywords\": [\"example\", \"multi-provider\", \"dynamic\"],\n    \"ccrVersion\": \"2.0.0\"\n  },\n  \"schema\": [\n    {\n      \"id\": \"providerChoice\",\n      \"type\": \"select\",\n      \"label\": \"选择Provider\",\n      \"prompt\": \"请选择要使用的LLM提供商\",\n      \"options\": {\n        \"type\": \"static\",\n        \"options\": [\n          {\n            \"label\": \"OpenAI\",\n            \"value\": \"openai\",\n            \"description\": \"使用OpenAI的GPT模型\"\n          },\n          {\n            \"label\": \"DeepSeek\",\n            \"value\": \"deepseek\",\n            \"description\": \"使用DeepSeek的高性价比模型\"\n          },\n          {\n            \"label\": \"Gemini\",\n            \"value\": \"gemini\",\n            \"description\": \"使用Google的Gemini模型\"\n          }\n        ]\n      },\n      \"required\": true,\n      \"defaultValue\": \"openai\"\n    },\n    {\n      \"id\": \"apiKey\",\n      \"type\": \"password\",\n      \"label\": \"API Key\",\n      \"prompt\": \"请输入您的API Key\",\n      \"placeholder\": \"sk-...\",\n      \"required\": true,\n      \"when\": {\n        \"field\": \"providerChoice\",\n        \"operator\": \"exists\"\n      }\n    },\n    {\n      \"id\": \"baseUrl\",\n      \"type\": \"input\",\n      \"label\": \"Base URL（可选）\",\n      \"prompt\": \"自定义API Base URL，留空使用默认值\",\n      \"required\": false,\n      \"when\": {\n        \"field\": \"providerChoice\",\n        \"operator\": \"exists\"\n      }\n    },\n    {\n      \"id\": \"modelChoice\",\n      \"type\": \"select\",\n      \"label\": \"选择模型\",\n      \"prompt\": \"请选择要使用的模型\",\n      \"options\": {\n        \"type\": \"static\",\n        \"options\": [\n          {\n            \"label\": \"GPT-4o\",\n            \"value\": \"gpt-4o\"\n          },\n          {\n            \"label\": \"GPT-4o-mini\",\n            \"value\": \"gpt-4o-mini\"\n          },\n          {\n            \"label\": \"GPT-3.5-turbo\",\n            \"value\": \"gpt-3.5-turbo\"\n          }\n        ]\n      },\n      \"required\": true,\n      \"when\": {\n        \"field\": \"providerChoice\",\n        \"operator\": \"eq\",\n        \"value\": \"openai\"\n      },\n      \"defaultValue\": \"gpt-4o\"\n    },\n    {\n      \"id\": \"deepseekModelChoice\",\n      \"type\": \"select\",\n      \"label\": \"选择模型\",\n      \"prompt\": \"请选择要使用的DeepSeek模型\",\n      \"options\": {\n        \"type\": \"static\",\n        \"options\": [\n          {\n            \"label\": \"DeepSeek-V3\",\n            \"value\": \"deepseek-v3\"\n          },\n          {\n            \"label\": \"DeepSeek-Chat\",\n            \"value\": \"deepseek-chat\"\n          }\n        ]\n      },\n      \"required\": true,\n      \"when\": {\n        \"field\": \"providerChoice\",\n        \"operator\": \"eq\",\n        \"value\": \"deepseek\"\n      },\n      \"defaultValue\": \"deepseek-v3\"\n    },\n    {\n      \"id\": \"useProxy\",\n      \"type\": \"confirm\",\n      \"label\": \"使用代理\",\n      \"prompt\": \"是否通过代理访问API？\",\n      \"defaultValue\": false\n    },\n    {\n      \"id\": \"proxyUrl\",\n      \"type\": \"input\",\n      \"label\": \"代理URL\",\n      \"prompt\": \"请输入代理地址（如：http://127.0.0.1:7890）\",\n      \"placeholder\": \"http://127.0.0.1:7890\",\n      \"required\": true,\n      \"when\": {\n        \"field\": \"useProxy\",\n        \"operator\": \"eq\",\n        \"value\": true\n      }\n    },\n    {\n      \"id\": \"maxTokens\",\n      \"type\": \"number\",\n      \"label\": \"最大Token数\",\n      \"prompt\": \"设置单次请求的最大token数量\",\n      \"min\": 100,\n      \"max\": 128000,\n      \"defaultValue\": 4096,\n      \"required\": false\n    },\n    {\n      \"id\": \"advancedSettings\",\n      \"type\": \"confirm\",\n      \"label\": \"高级设置\",\n      \"prompt\": \"是否配置高级选项？\",\n      \"defaultValue\": false\n    },\n    {\n      \"id\": \"temperature\",\n      \"type\": \"number\",\n      \"label\": \"Temperature\",\n      \"prompt\": \"控制生成随机性（0-2）\",\n      \"min\": 0,\n      \"max\": 2,\n      \"defaultValue\": 0.7,\n      \"required\": false,\n      \"when\": {\n        \"field\": \"advancedSettings\",\n        \"operator\": \"eq\",\n        \"value\": true\n      }\n    },\n    {\n      \"id\": \"features\",\n      \"type\": \"multiselect\",\n      \"label\": \"启用功能\",\n      \"prompt\": \"选择要启用的功能\",\n      \"options\": {\n        \"type\": \"static\",\n        \"options\": [\n          {\n            \"label\": \"流式输出\",\n            \"value\": \"stream\"\n          },\n          {\n            \"label\": \"工具调用\",\n            \"value\": \"tools\"\n          },\n          {\n            \"label\": \"长上下文支持\",\n            \"value\": \"longContext\"\n          }\n        ]\n      },\n      \"defaultValue\": [\"stream\"],\n      \"required\": false\n    }\n  ],\n  \"template\": {\n    \"Providers\": [\n      {\n        \"name\": \"{{providerChoice}}\",\n        \"api_base_url\": \"{{baseUrl}}\",\n        \"api_key\": \"{{apiKey}}\",\n        \"models\": [\"{{modelChoice}}\", \"{{deepseekModelChoice}}\"]\n      }\n    ],\n    \"Router\": {\n      \"default\": \"{{providerChoice}}/{{modelChoice}}{{deepseekModelChoice}}\"\n    },\n    \"PROXY_URL\": \"{{proxyUrl}}\",\n    \"API_TIMEOUT_MS\": 60000\n  },\n  \"configMappings\": [\n    {\n      \"target\": \"Providers[0].name\",\n      \"value\": \"{{providerChoice}}\"\n    },\n    {\n      \"target\": \"Providers[0].api_key\",\n      \"value\": \"{{apiKey}}\"\n    },\n    {\n      \"target\": \"Providers[0].api_base_url\",\n      \"value\": \"{{baseUrl}}\",\n      \"when\": {\n        \"field\": \"baseUrl\",\n        \"operator\": \"exists\"\n      }\n    },\n    {\n      \"target\": \"Router.default\",\n      \"value\": \"{{providerChoice}}/{{modelChoice}}{{deepseekModelChoice}}\"\n    },\n    {\n      \"target\": \"PROXY_URL\",\n      \"value\": \"{{proxyUrl}}\",\n      \"when\": {\n        \"field\": \"useProxy\",\n        \"operator\": \"eq\",\n        \"value\": true\n      }\n    },\n    {\n      \"target\": \"API_TIMEOUT_MS\",\n      \"value\": 60000\n    }\n  ]\n}\n"
  },
  {
    "path": "examples/preset-manifest-example.json",
    "content": "{\n  \"name\": \"multi-provider-example\",\n  \"version\": \"1.0.0\",\n  \"description\": \"多Provider配置示例 - 支持OpenAI和DeepSeek切换\",\n  \"author\": \"CCR Team\",\n  \"keywords\": [\"openai\", \"deepseek\", \"multi-provider\"],\n  \"ccrVersion\": \"2.0.0\",\n  \"Providers\": [\n    {\n      \"name\": \"openai\",\n      \"api_base_url\": \"https://api.openai.com/v1\",\n      \"models\": [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-3.5-turbo\"]\n    },\n    {\n      \"name\": \"deepseek\",\n      \"api_base_url\": \"https://api.deepseek.com\",\n      \"models\": [\"deepseek-v3\", \"deepseek-chat\"]\n    }\n  ],\n  \"schema\": [\n    {\n      \"id\": \"primaryProvider\",\n      \"type\": \"select\",\n      \"label\": \"主要Provider\",\n      \"prompt\": \"选择您主要使用的LLM提供商\",\n      \"options\": {\n        \"type\": \"providers\"\n      },\n      \"required\": true,\n      \"defaultValue\": \"openai\"\n    },\n    {\n      \"id\": \"apiKey\",\n      \"type\": \"password\",\n      \"label\": \"API Key\",\n      \"prompt\": \"请输入您的API Key（将从环境变量或安全存储中读取）\",\n      \"placeholder\": \"sk-...\",\n      \"required\": true,\n      \"when\": {\n        \"field\": \"primaryProvider\",\n        \"operator\": \"exists\"\n      }\n    },\n    {\n      \"id\": \"customBaseUrl\",\n      \"type\": \"input\",\n      \"label\": \"自定义Base URL\",\n      \"prompt\": \"如果使用代理或自定义端点，请输入Base URL（留空使用默认值）\",\n      \"placeholder\": \"https://api.openai.com/v1\",\n      \"required\": false,\n      \"when\": {\n        \"field\": \"primaryProvider\",\n        \"operator\": \"exists\"\n      }\n    },\n    {\n      \"id\": \"defaultModel\",\n      \"type\": \"select\",\n      \"label\": \"默认模型\",\n      \"prompt\": \"选择默认使用的模型\",\n      \"options\": {\n        \"type\": \"models\",\n        \"providerField\": \"{{primaryProvider}}\"\n      },\n      \"required\": true,\n      \"defaultValue\": \"gpt-4o\",\n      \"when\": {\n        \"field\": \"primaryProvider\",\n        \"operator\": \"eq\",\n        \"value\": \"openai\"\n      }\n    },\n    {\n      \"id\": \"backgroundModel\",\n      \"type\": \"select\",\n      \"label\": \"后台任务模型\",\n      \"prompt\": \"用于后台任务的轻量级模型\",\n      \"options\": {\n        \"type\": \"models\",\n        \"providerField\": \"{{primaryProvider}}\"\n      },\n      \"required\": false,\n      \"when\": {\n        \"field\": \"primaryProvider\",\n        \"operator\": \"exists\"\n      }\n    },\n    {\n      \"id\": \"enableProxy\",\n      \"type\": \"confirm\",\n      \"label\": \"启用代理\",\n      \"prompt\": \"是否通过代理访问API？\",\n      \"defaultValue\": false\n    },\n    {\n      \"id\": \"proxyUrl\",\n      \"type\": \"input\",\n      \"label\": \"代理地址\",\n      \"prompt\": \"输入代理服务器地址\",\n      \"placeholder\": \"http://127.0.0.1:7890\",\n      \"required\": true,\n      \"when\": {\n        \"field\": \"enableProxy\",\n        \"operator\": \"eq\",\n        \"value\": true\n      }\n    },\n    {\n      \"id\": \"maxTokens\",\n      \"type\": \"number\",\n      \"label\": \"最大Token数\",\n      \"prompt\": \"设置单次请求的最大token数量\",\n      \"min\": 100,\n      \"max\": 128000,\n      \"defaultValue\": 4096,\n      \"required\": false\n    },\n    {\n      \"id\": \"timeout\",\n      \"type\": \"number\",\n      \"label\": \"请求超时（秒）\",\n      \"prompt\": \"API请求超时时间\",\n      \"min\": 10,\n      \"max\": 300,\n      \"defaultValue\": 60,\n      \"required\": false\n    },\n    {\n      \"id\": \"enableFeatures\",\n      \"type\": \"multiselect\",\n      \"label\": \"启用功能\",\n      \"prompt\": \"选择要启用的功能\",\n      \"options\": {\n        \"type\": \"static\",\n        \"options\": [\n          {\n            \"label\": \"流式输出\",\n            \"value\": \"stream\",\n            \"description\": \"实时显示AI响应\"\n          },\n          {\n            \"label\": \"工具调用\",\n            \"value\": \"tools\",\n            \"description\": \"启用Function Calling功能\"\n          },\n          {\n            \"label\": \"长上下文\",\n            \"value\": \"longContext\",\n            \"description\": \"支持长文本处理\"\n          },\n          {\n            \"label\": \"思维链\",\n            \"value\": \"think\",\n            \"description\": \"在思考模式中使用\"\n          }\n        ]\n      },\n      \"defaultValue\": [\"stream\", \"tools\"],\n      \"required\": false\n    },\n    {\n      \"id\": \"advancedMode\",\n      \"type\": \"confirm\",\n      \"label\": \"高级模式\",\n      \"prompt\": \"启用高级配置选项？\",\n      \"defaultValue\": false\n    },\n    {\n      \"id\": \"temperature\",\n      \"type\": \"number\",\n      \"label\": \"Temperature\",\n      \"prompt\": \"控制生成随机性（0-2，值越高越随机）\",\n      \"min\": 0,\n      \"max\": 2,\n      \"defaultValue\": 0.7,\n      \"required\": false,\n      \"when\": {\n        \"field\": \"advancedMode\",\n        \"operator\": \"eq\",\n        \"value\": true\n      }\n    },\n    {\n      \"id\": \"logLevel\",\n      \"type\": \"select\",\n      \"label\": \"日志级别\",\n      \"prompt\": \"设置详细的日志级别\",\n      \"options\": {\n        \"type\": \"static\",\n        \"options\": [\n          {\n            \"label\": \"错误\",\n            \"value\": \"error\"\n          },\n          {\n            \"label\": \"警告\",\n            \"value\": \"warn\"\n          },\n          {\n            \"label\": \"信息\",\n            \"value\": \"info\"\n          },\n          {\n            \"label\": \"调试\",\n            \"value\": \"debug\"\n          }\n        ]\n      },\n      \"defaultValue\": \"info\",\n      \"required\": false,\n      \"when\": {\n        \"field\": \"advancedMode\",\n        \"operator\": \"eq\",\n        \"value\": true\n      }\n    }\n  ],\n  \"template\": {\n    \"Providers\": [\n      {\n        \"name\": \"{{primaryProvider}}\",\n        \"api_base_url\": \"{{customBaseUrl}}\",\n        \"api_key\": \"{{apiKey}}\",\n        \"models\": [\"{{defaultModel}}\", \"{{backgroundModel}}\"]\n      }\n    ],\n    \"Router\": {\n      \"default\": \"{{primaryProvider}}/{{defaultModel}}\",\n      \"background\": \"{{backgroundModel}}\",\n      \"think\": \"{{primaryProvider}}/{{defaultModel}}\"\n    },\n    \"PROXY_URL\": \"{{proxyUrl}}\",\n    \"API_TIMEOUT_MS\": 60000,\n    \"LOG_LEVEL\": \"info\"\n  },\n  \"configMappings\": [\n    {\n      \"target\": \"Providers[0].api_base_url\",\n      \"value\": \"{{customBaseUrl}}\",\n      \"when\": {\n        \"field\": \"customBaseUrl\",\n        \"operator\": \"exists\"\n      }\n    },\n    {\n      \"target\": \"PROXY_URL\",\n      \"value\": \"{{proxyUrl}}\",\n      \"when\": {\n        \"field\": \"enableProxy\",\n        \"operator\": \"eq\",\n        \"value\": true\n      }\n    },\n    {\n      \"target\": \"API_TIMEOUT_MS\",\n      \"value\": 60000\n    },\n    {\n      \"target\": \"LOG_LEVEL\",\n      \"value\": \"{{logLevel}}\",\n      \"when\": {\n        \"field\": \"advancedMode\",\n        \"operator\": \"eq\",\n        \"value\": true\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "examples/simple-preset-example.json",
    "content": "{\n  \"name\": \"simple-openai-preset\",\n  \"version\": \"1.0.0\",\n  \"description\": \"简单的OpenAI配置预设\",\n  \"author\": \"Your Name\",\n  \"keywords\": [\"openai\", \"simple\"],\n  \"schema\": [\n    {\n      \"id\": \"apiKey\",\n      \"type\": \"password\",\n      \"label\": \"OpenAI API Key\",\n      \"prompt\": \"请输入您的OpenAI API Key\",\n      \"placeholder\": \"sk-...\",\n      \"required\": true\n    },\n    {\n      \"id\": \"model\",\n      \"type\": \"select\",\n      \"label\": \"选择模型\",\n      \"prompt\": \"选择要使用的GPT模型\",\n      \"options\": {\n        \"type\": \"static\",\n        \"options\": [\n          {\n            \"label\": \"GPT-4o (推荐)\",\n            \"value\": \"gpt-4o\"\n          },\n          {\n            \"label\": \"GPT-4o-mini (快速)\",\n            \"value\": \"gpt-4o-mini\"\n          },\n          {\n            \"label\": \"GPT-3.5 Turbo (经济)\",\n            \"value\": \"gpt-3.5-turbo\"\n          }\n        ]\n      },\n      \"required\": true,\n      \"defaultValue\": \"gpt-4o\"\n    },\n    {\n      \"id\": \"useProxy\",\n      \"type\": \"confirm\",\n      \"label\": \"使用代理\",\n      \"prompt\": \"是否需要通过代理访问OpenAI API？\",\n      \"defaultValue\": false\n    },\n    {\n      \"id\": \"proxyUrl\",\n      \"type\": \"input\",\n      \"label\": \"代理地址\",\n      \"prompt\": \"请输入代理地址\",\n      \"placeholder\": \"http://127.0.0.1:7890\",\n      \"required\": true,\n      \"when\": {\n        \"field\": \"useProxy\",\n        \"operator\": \"eq\",\n        \"value\": true\n      }\n    }\n  ],\n  \"template\": {\n    \"Providers\": [\n      {\n        \"name\": \"openai\",\n        \"api_base_url\": \"https://api.openai.com/v1\",\n        \"api_key\": \"{{apiKey}}\",\n        \"models\": [\"{{model}}\"]\n      }\n    ],\n    \"Router\": {\n      \"default\": \"openai/{{model}}\"\n    },\n    \"PROXY_URL\": \"{{proxyUrl}}\"\n  },\n  \"configMappings\": [\n    {\n      \"target\": \"PROXY_URL\",\n      \"value\": \"{{proxyUrl}}\",\n      \"when\": {\n        \"field\": \"useProxy\",\n        \"operator\": \"eq\",\n        \"value\": true\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"@musistudio/claude-code-router\",\n  \"version\": \"2.0.0\",\n  \"description\": \"Use Claude Code without an Anthropics account and route it to another LLM provider\",\n  \"scripts\": {\n    \"build\": \"pnpm build:shared && pnpm build:core && pnpm build:server && pnpm build:cli && pnpm build:ui\",\n    \"build:core\": \"pnpm --filter @musistudio/llms build\",\n    \"build:shared\": \"pnpm --filter @CCR/shared build\",\n    \"build:cli\": \"pnpm --filter @CCR/cli build\",\n    \"build:server\": \"pnpm --filter @CCR/server build\",\n    \"build:ui\": \"pnpm --filter @CCR/ui build\",\n    \"build:docs\": \"pnpm --filter claude-code-router-docs build\",\n    \"release\": \"pnpm build && bash scripts/release.sh all\",\n    \"release:npm\": \"bash scripts/release.sh npm\",\n    \"release:docker\": \"bash scripts/release.sh docker\",\n    \"dev:cli\": \"pnpm --filter @CCR/cli dev\",\n    \"dev:server\": \"pnpm --filter @CCR/server dev\",\n    \"dev:ui\": \"pnpm --filter @CCR/ui dev\",\n    \"dev:core\": \"pnpm --filter @musistudio/llms dev\",\n    \"dev:docs\": \"pnpm --filter claude-code-router-docs start\",\n    \"serve:docs\": \"pnpm --filter claude-code-router-docs serve\"\n  },\n  \"bin\": {\n    \"ccr\": \"dist/cli.js\"\n  },\n  \"keywords\": [\n    \"claude\",\n    \"code\",\n    \"router\",\n    \"llm\",\n    \"anthropic\"\n  ],\n  \"author\": \"musistudio\",\n  \"license\": \"MIT\",\n  \"devDependencies\": {\n    \"@types/node\": \"^24.0.15\",\n    \"esbuild\": \"^0.25.1\",\n    \"shx\": \"^0.4.0\",\n    \"typescript\": \"^5.8.2\"\n  },\n  \"pnpm\": {\n    \"overrides\": {\n      \"@types/react\": \"18\"\n    }\n  },\n  \"engines\": {\n    \"node\": \">=20.0.0\",\n    \"pnpm\": \">=8.0.0\"\n  }\n}\n"
  },
  {
    "path": "packages/cli/package.json",
    "content": "{\n  \"name\": \"@CCR/cli\",\n  \"version\": \"2.0.0\",\n  \"description\": \"CLI for Claude Code Router\",\n  \"bin\": {\n    \"ccr\": \"dist/cli.js\"\n  },\n  \"scripts\": {\n    \"build\": \"node ../../scripts/build-cli.js\",\n    \"dev\": \"ts-node src/cli.ts\"\n  },\n  \"keywords\": [\n    \"claude\",\n    \"code\",\n    \"router\",\n    \"cli\"\n  ],\n  \"author\": \"musistudio\",\n  \"license\": \"MIT\",\n  \"devDependencies\": {\n    \"@CCR/server\": \"workspace:*\",\n    \"@CCR/shared\": \"workspace:*\",\n    \"@inquirer/prompts\": \"^5.0.0\",\n    \"@types/archiver\": \"^7.0.0\",\n    \"@types/node\": \"^24.0.15\",\n    \"adm-zip\": \"^0.5.16\",\n    \"archiver\": \"^7.0.1\",\n    \"esbuild\": \"^0.25.1\",\n    \"find-process\": \"^2.0.0\",\n    \"minimist\": \"^1.2.8\",\n    \"openurl\": \"^1.1.1\",\n    \"ts-node\": \"^10.9.2\",\n    \"typescript\": \"^5.8.2\"\n  }\n}\n"
  },
  {
    "path": "packages/cli/src/cli.ts",
    "content": "#!/usr/bin/env node\nimport { run, restartService } from \"./utils\";\nimport { showStatus } from \"./utils/status\";\nimport { executeCodeCommand, PresetConfig } from \"./utils/codeCommand\";\nimport {\n  cleanupPidFile,\n  isServiceRunning,\n  getServiceInfo,\n} from \"./utils/processCheck\";\nimport { runModelSelector } from \"./utils/modelSelector\";\nimport { activateCommand } from \"./utils/activateCommand\";\nimport { readConfigFile } from \"./utils\";\nimport { version } from \"../package.json\";\nimport { spawn, exec } from \"child_process\";\nimport {getPresetDir, loadConfigFromManifest, PID_FILE, readPresetFile, REFERENCE_COUNT_FILE} from \"@CCR/shared\";\nimport fs, { existsSync, readFileSync } from \"fs\";\nimport { join } from \"path\";\nimport { parseStatusLineData, StatusLineInput } from \"./utils/statusline\";\nimport {handlePresetCommand} from \"./utils/preset\";\nimport { handleInstallCommand } from \"./utils/installCommand\";\n\n\nconst command = process.argv[2];\n\n// Define all known commands\nconst KNOWN_COMMANDS = [\n  \"start\",\n  \"stop\",\n  \"restart\",\n  \"status\",\n  \"statusline\",\n  \"code\",\n  \"model\",\n  \"preset\",\n  \"install\",\n  \"activate\",\n  \"env\",\n  \"ui\",\n  \"-v\",\n  \"version\",\n  \"-h\",\n  \"help\",\n];\n\nconst HELP_TEXT = `\nUsage: ccr [command] [preset-name]\n\nCommands:\n  start         Start server\n  stop          Stop server\n  restart       Restart server\n  status        Show server status\n  statusline    Integrated statusline\n  code          Execute claude command\n  model         Interactive model selection and configuration\n  preset        Manage presets (export, install, list, delete)\n  install       Install preset from GitHub marketplace\n  activate      Output environment variables for shell integration\n  ui            Open the web UI in browser\n  -v, version   Show version information\n  -h, help      Show help information\n\nPresets:\n  Any preset directory in ~/.claude-code-router/presets/\n\nExamples:\n  ccr start\n  ccr code \"Write a Hello World\"\n  ccr my-preset \"Write a Hello World\"    # Use preset configuration\n  ccr model\n  ccr preset export my-config            # Export current config as preset\n  ccr preset install /path/to/preset     # Install a preset from directory\n  ccr preset list                        # List all presets\n  ccr install my-preset                  # Install preset from marketplace\n  eval \"$(ccr activate)\"  # Set environment variables globally\n  ccr ui\n`;\n\nasync function waitForService(\n  timeout = 10000,\n  initialDelay = 1000\n): Promise<boolean> {\n  // Wait for an initial period to let the service initialize\n  await new Promise((resolve) => setTimeout(resolve, initialDelay));\n\n  const startTime = Date.now();\n  while (Date.now() - startTime < timeout) {\n    const isRunning = isServiceRunning()\n    if (isRunning) {\n      // Wait for an additional short period to ensure service is fully ready\n      await new Promise((resolve) => setTimeout(resolve, 500));\n      return true;\n    }\n    await new Promise((resolve) => setTimeout(resolve, 100));\n  }\n  return false;\n}\n\nasync function main() {\n  const isRunning = isServiceRunning()\n\n  // If command is not a known command, check if it's a preset\n  if (command && !KNOWN_COMMANDS.includes(command)) {\n    const manifest = await readPresetFile(command);\n\n    if (manifest) {\n      // This is a preset, load its configuration\n      const presetDir = getPresetDir(command);\n      const config = loadConfigFromManifest(manifest, presetDir);\n\n      // Execute code command\n      const codeArgs = process.argv.slice(3); // Get remaining arguments\n\n      // Check noServer configuration\n      const shouldStartServer = config.noServer !== true;\n\n      // Build environment variable overrides\n      let envOverrides: Record<string, string> = {};\n\n      // Handle provider configuration (supports both old and new formats)\n      let provider: any = null;\n\n      // Old format: config.provider is the provider name\n      if (config.provider && typeof config.provider === 'string') {\n        const globalConfig = await readConfigFile();\n        provider = globalConfig.Providers?.find((p: any) => p.name === config.provider);\n      }\n      // New format: config.Providers is an array of providers\n      else if (config.Providers && config.Providers.length > 0) {\n        provider = config.Providers[0];\n      }\n\n      // If noServer is not true, use local server baseurl\n      if (shouldStartServer) {\n        const globalConfig = await readConfigFile();\n        const port = globalConfig.PORT || 3456;\n        envOverrides = {\n          ...envOverrides,\n          ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}/preset/${command}`,\n        };\n      } else if (provider) {\n        // Handle api_base_url, remove /v1/messages suffix\n        if (provider.api_base_url) {\n          let baseUrl = provider.api_base_url;\n          if (baseUrl.endsWith('/v1/messages')) {\n            baseUrl = baseUrl.slice(0, -'/v1/messages'.length);\n          } else if (baseUrl.endsWith('/')) {\n            baseUrl = baseUrl.slice(0, -1);\n          }\n          envOverrides = {\n            ...envOverrides,\n            ANTHROPIC_BASE_URL: baseUrl,\n          };\n        }\n\n        // Handle api_key\n        if (provider.api_key) {\n          envOverrides = {\n            ...envOverrides,\n            ANTHROPIC_AUTH_TOKEN: provider.api_key,\n          };\n        }\n      }\n\n      // Build PresetConfig\n      const presetConfig: PresetConfig = {\n        noServer: config.noServer,\n        claudeCodeSettings: config.claudeCodeSettings,\n        StatusLine: config.StatusLine\n      };\n\n      if (shouldStartServer && !isRunning) {\n        console.log(\"Service not running, starting service...\");\n        const cliPath = join(__dirname, \"cli.js\");\n        const startProcess = spawn(\"node\", [cliPath, \"start\"], {\n          detached: true,\n          stdio: \"ignore\",\n        });\n\n        startProcess.on(\"error\", (error) => {\n          console.error(\"Failed to start service:\", error.message);\n          process.exit(1);\n        });\n\n        startProcess.unref();\n\n        if (await waitForService()) {\n          executeCodeCommand(codeArgs, presetConfig, envOverrides, command);\n        } else {\n          console.error(\n            \"Service startup timeout, please manually run `ccr start` to start the service\"\n          );\n          process.exit(1);\n        }\n      } else {\n        // Service is already running or no need to start server\n        if (shouldStartServer && !isRunning) {\n          console.error(\"Service is not running. Please start it first with `ccr start`\");\n          process.exit(1);\n        }\n        executeCodeCommand(codeArgs, presetConfig, envOverrides, command);\n      }\n      return;\n    } else {\n      // Not a preset nor a known command\n      console.log(HELP_TEXT);\n      process.exit(1);\n    }\n  }\n\n  switch (command) {\n    case \"start\":\n      await run();\n      break;\n    case \"stop\":\n      try {\n        const pid = parseInt(readFileSync(PID_FILE, \"utf-8\"));\n        process.kill(pid);\n        cleanupPidFile();\n        if (existsSync(REFERENCE_COUNT_FILE)) {\n          try {\n            fs.unlinkSync(REFERENCE_COUNT_FILE);\n          } catch (e) {\n            // Ignore cleanup errors\n          }\n        }\n        console.log(\n          \"claude code router service has been successfully stopped.\"\n        );\n      } catch (e) {\n        console.log(\n          \"Failed to stop the service. It may have already been stopped.\"\n        );\n        cleanupPidFile();\n      }\n      break;\n    case \"status\":\n      await showStatus();\n      break;\n    case \"statusline\":\n      // Read JSON input from stdin\n      let inputData = \"\";\n      process.stdin.setEncoding(\"utf-8\");\n      process.stdin.on(\"readable\", () => {\n        let chunk;\n        while ((chunk = process.stdin.read()) !== null) {\n          inputData += chunk;\n        }\n      });\n\n      process.stdin.on(\"end\", async () => {\n        try {\n          const input: StatusLineInput = JSON.parse(inputData);\n          // Check if preset name is provided as argument\n          const presetName = process.argv[3];\n          const statusLine = await parseStatusLineData(input, presetName);\n          console.log(statusLine);\n        } catch (error) {\n          console.error(\"Error parsing status line data:\", error);\n          process.exit(1);\n        }\n      });\n      break;\n    // ADD THIS CASE\n    case \"model\":\n      await runModelSelector();\n      break;\n    case \"preset\":\n      await handlePresetCommand(process.argv.slice(3));\n      break;\n    case \"install\":\n      const presetName = process.argv[3];\n      await handleInstallCommand(presetName);\n      break;\n    case \"activate\":\n    case \"env\":\n      await activateCommand();\n      break;\n    case \"code\":\n      if (!isRunning) {\n        console.log(\"Service not running, starting service...\");\n        const cliPath = join(__dirname, \"cli.js\");\n        const startProcess = spawn(\"node\", [cliPath, \"start\"], {\n          detached: true,\n          stdio: \"ignore\",\n        });\n\n        startProcess.on(\"error\", (error) => {\n          console.error(\"Failed to start service:\", error.message);\n          process.exit(1);\n        });\n\n        startProcess.unref();\n\n        if (await waitForService()) {\n          const codeArgs = process.argv.slice(3);\n          executeCodeCommand(codeArgs);\n        } else {\n          console.error(\n            \"Service startup timeout, please manually run `ccr start` to start the service\"\n          );\n          process.exit(1);\n        }\n      } else {\n        const codeArgs = process.argv.slice(3);\n        executeCodeCommand(codeArgs);\n      }\n      break;\n    case \"ui\":\n      // Check if service is running\n      if (!isRunning) {\n        console.log(\"Service not running, starting service...\");\n        const cliPath = join(__dirname, \"cli.js\");\n        const startProcess = spawn(\"node\", [cliPath, \"start\"], {\n          detached: true,\n          stdio: \"ignore\",\n        });\n\n        startProcess.on(\"error\", (error) => {\n          console.error(\"Failed to start service:\", error.message);\n          process.exit(1);\n        });\n\n        startProcess.unref();\n\n        if (!(await waitForService())) {\n          // If service startup fails, try to start with default config\n          console.log(\n            \"Service startup timeout, trying to start with default configuration...\"\n          );\n          const {\n            initDir,\n            writeConfigFile,\n            backupConfigFile,\n          } = require(\"./utils\");\n\n          try {\n            // Initialize directories\n            await initDir();\n\n            // Backup existing config file if it exists\n            const backupPath = await backupConfigFile();\n            if (backupPath) {\n              console.log(\n                `Backed up existing configuration file to ${backupPath}`\n              );\n            }\n\n            // Create a minimal default config file\n            await writeConfigFile({\n              PORT: 3456,\n              Providers: [],\n              Router: {},\n            });\n            console.log(\n              \"Created minimal default configuration file at ~/.claude-code-router/config.json\"\n            );\n            console.log(\n              \"Please edit this file with your actual configuration.\"\n            );\n\n            // Try starting the service again\n            const restartProcess = spawn(\"node\", [cliPath, \"start\"], {\n              detached: true,\n              stdio: \"ignore\",\n            });\n\n            restartProcess.on(\"error\", (error) => {\n              console.error(\n                \"Failed to start service with default config:\",\n                error.message\n              );\n              process.exit(1);\n            });\n\n            restartProcess.unref();\n\n            if (!(await waitForService(15000))) {\n              // Wait a bit longer for the first start\n              console.error(\n                \"Service startup still failing. Please manually run `ccr start` to start the service and check the logs.\"\n              );\n              process.exit(1);\n            }\n          } catch (error: any) {\n            console.error(\n              \"Failed to create default configuration:\",\n              error.message\n            );\n            process.exit(1);\n          }\n        }\n      }\n\n      // Get service info and open UI\n      const serviceInfo = await getServiceInfo();\n\n      // Add temporary API key as URL parameter if successfully generated\n      const uiUrl = `${serviceInfo.endpoint}/ui/`;\n\n      console.log(`Opening UI at ${uiUrl}`);\n\n      // Open URL in browser based on platform\n      const platform = process.platform;\n      let openCommand = \"\";\n\n      if (platform === \"win32\") {\n        // Windows\n        openCommand = `start ${uiUrl}`;\n      } else if (platform === \"darwin\") {\n        // macOS\n        openCommand = `open ${uiUrl}`;\n      } else if (platform === \"linux\") {\n        // Linux\n        openCommand = `xdg-open ${uiUrl}`;\n      } else {\n        console.error(\"Unsupported platform for opening browser\");\n        process.exit(1);\n      }\n\n      exec(openCommand, (error) => {\n        if (error) {\n          console.error(\"Failed to open browser:\", error.message);\n          process.exit(1);\n        }\n      });\n      break;\n    case \"-v\":\n    case \"version\":\n      console.log(`claude-code-router version: ${version}`);\n      break;\n    case \"restart\":\n      await restartService();\n      break;\n    case \"-h\":\n    case \"help\":\n      console.log(HELP_TEXT);\n      break;\n    default:\n      console.log(HELP_TEXT);\n      process.exit(1);\n  }\n}\n\nmain().catch(console.error);\n"
  },
  {
    "path": "packages/cli/src/types/inquirer.d.ts",
    "content": "// Type declarations for @inquirer packages\ndeclare module '@inquirer/input' {\n  import { DistinctChoice } from '@inquirer/core';\n  interface PromptConfig {\n    message: string;\n    default?: string;\n  }\n  export default function prompt<T = string>(config: PromptConfig): Promise<T>;\n}\n\ndeclare module '@inquirer/confirm' {\n  interface PromptConfig {\n    message: string;\n    default?: boolean;\n  }\n  export default function prompt(config: PromptConfig): Promise<boolean>;\n}\n\ndeclare module '@inquirer/select' {\n  export default function prompt<T = string>(config: {\n    message: string;\n    choices: Array<{ name: string; value: T; description?: string }>;\n    default?: T;\n  }): Promise<T>;\n}\n\ndeclare module '@inquirer/password' {\n  interface PromptConfig {\n    message: string;\n    mask?: string;\n  }\n  export default function prompt(config: PromptConfig): Promise<string>;\n}\n\ndeclare module '@inquirer/checkbox' {\n  export default function prompt<T = string>(config: {\n    message: string;\n    choices: Array<{ name: string; value: T; checked?: boolean }>;\n  }): Promise<T[]>;\n}\n\ndeclare module '@inquirer/editor' {\n  interface PromptConfig {\n    message: string;\n    default?: string;\n  }\n  export default function prompt(config: PromptConfig): Promise<string>;\n}\n"
  },
  {
    "path": "packages/cli/src/types.d.ts",
    "content": "declare module 'shell-quote' {\n  export function quote(args: string[]): string;\n  export function parse(cmd: string): string[];\n}\n\ndeclare module 'minimist' {\n  interface Options {\n    string?: string[];\n    boolean?: string | string[];\n    alias?: Record<string, string | string[]>;\n    default?: Record<string, any>;\n    stopEarly?: boolean;\n    '--'?: boolean;\n    unknown?: (arg: string) => boolean;\n  }\n\n  interface ParsedArgs {\n    _: string[];\n    [key: string]: any;\n  }\n\n  function minimist(args?: string[], opts?: Options): ParsedArgs;\n  export = minimist;\n}\n\ndeclare module '@inquirer/prompts' {\n  export function select<T>(config: {\n    message: string;\n    choices: Array<{ name: string; value: T; description?: string }>;\n    pageSize?: number;\n  }): Promise<T>;\n  export function input(config: {\n    message: string;\n    default?: string;\n    validate?: (value: string) => boolean | string | Promise<boolean | string>;\n  }): Promise<string>;\n  export function confirm(config: {\n    message: string;\n    default?: boolean;\n  }): Promise<boolean>;\n}\n\ndeclare module 'find-process' {\n  export default function find(\n    type: 'pid' | 'name' | 'port',\n    value: string | number\n  ): Promise<Array<{ pid: number; name: string; ppid?: number; cmd?: string }>>;\n}\n\ndeclare module 'json5' {\n  export function parse(text: string): any;\n  export function stringify(value: any, replacer?: any, space?: string | number): string;\n}\n\ndeclare namespace NodeJS {\n  interface ProcessEnv {\n    CI?: string;\n    FORCE_COLOR?: string;\n    NODE_NO_READLINE?: string;\n    TERM?: string;\n    ANTHROPIC_SMALL_FAST_MODEL?: string;\n  }\n}\n\ninterface ClaudeSettingsFlag {\n  env: {\n    ANTHROPIC_AUTH_TOKEN?: any;\n    ANTHROPIC_API_KEY: string;\n    ANTHROPIC_BASE_URL: string;\n    NO_PROXY: string;\n    DISABLE_TELEMETRY: string;\n    DISABLE_COST_WARNINGS: string;\n    API_TIMEOUT_MS: string;\n    CLAUDE_CODE_USE_BEDROCK?: undefined;\n    [key: string]: any;\n  };\n  statusLine?: {\n    type: string;\n    command: string;\n    padding: number;\n  };\n}\n"
  },
  {
    "path": "packages/cli/src/utils/activateCommand.ts",
    "content": "import { createEnvVariables } from \"./createEnvVariables\";\n\n/**\n * Execute the env command\n */\nexport const activateCommand = async () => {\n  const envVars = await createEnvVariables();\n\n  // Output in shell-friendly format for eval\n  for (const [key, value] of Object.entries(envVars)) {\n    if (value === \"\") {\n      console.log(`export ${key}=\"\"`);\n    } else if (value === undefined) {\n      console.log(`unset ${key}`);\n    } else {\n      console.log(`export ${key}=\"${value}\"`);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/cli/src/utils/codeCommand.ts",
    "content": "import { spawn, type StdioOptions } from \"child_process\";\nimport {getSettingsPath, readConfigFile} from \".\";\nimport {\n  decrementReferenceCount,\n  incrementReferenceCount,\n  closeService,\n} from \"./processCheck\";\nimport { quote } from 'shell-quote';\nimport minimist from \"minimist\";\nimport { createEnvVariables } from \"./createEnvVariables\";\n\nexport interface PresetConfig {\n  noServer?: boolean;\n  claudeCodeSettings?: {\n    env?: Record<string, any>;\n    statusLine?: any;\n    [key: string]: any;\n  };\n  provider?: string;\n  router?: Record<string, any>;\n  StatusLine?: any;  // Preset's StatusLine configuration\n  [key: string]: any;\n}\n\nexport async function executeCodeCommand(\n  args: string[] = [],\n  presetConfig?: PresetConfig | null,\n  envOverrides?: Record<string, string>,\n  presetName?: string  // Preset name for statusline command\n) {\n  // Set environment variables using shared function\n  const config = await readConfigFile();\n  const env = await createEnvVariables();\n\n  // Apply environment variable overrides (from preset's provider configuration)\n  if (envOverrides) {\n    Object.assign(env, envOverrides);\n  }\n\n  // Build settingsFlag\n  let settingsFlag: ClaudeSettingsFlag = {\n    env: env as ClaudeSettingsFlag['env']\n  };\n\n  // Add statusLine configuration\n  // Priority: preset.StatusLine > global config.StatusLine\n  const statusLineConfig = presetConfig?.StatusLine || config?.StatusLine;\n\n  if (statusLineConfig?.enabled) {\n    // If using preset, pass preset name to statusline command\n    const statuslineCommand = presetName\n      ? `ccr statusline ${presetName}`\n      : \"ccr statusline\";\n\n    settingsFlag.statusLine = {\n      type: \"command\",\n      command: statuslineCommand,\n      padding: 0,\n    }\n  }\n\n  // Merge claudeCodeSettings from preset into settingsFlag\n  if (presetConfig?.claudeCodeSettings) {\n    settingsFlag = {\n      ...settingsFlag,\n      ...presetConfig.claudeCodeSettings,\n      // Deep merge env\n      env: {\n        ...settingsFlag.env,\n        ...presetConfig.claudeCodeSettings.env,\n      } as ClaudeSettingsFlag['env']\n    };\n  }\n\n  // Non-interactive mode for automation environments\n  if (config.NON_INTERACTIVE_MODE) {\n    settingsFlag.env = {\n      ...settingsFlag.env,\n      CI: \"true\",\n      FORCE_COLOR: \"0\",\n      NODE_NO_READLINE: \"1\",\n      TERM: \"dumb\"\n    }\n  }\n\n  const settingsFile = await getSettingsPath(`${JSON.stringify(settingsFlag)}`)\n\n  args.push('--settings', settingsFile);\n\n  // Increment reference count when command starts\n  incrementReferenceCount();\n\n  // Execute claude command\n  const claudePath = config?.CLAUDE_PATH || process.env.CLAUDE_PATH || \"claude\";\n\n  const joinedArgs = args.length > 0 ? quote(args) : \"\";\n\n  const stdioConfig: StdioOptions = config.NON_INTERACTIVE_MODE\n    ? [\"pipe\", \"inherit\", \"inherit\"] // Pipe stdin for non-interactive\n    : \"inherit\"; // Default inherited behavior\n\n  const argsObj = minimist(args)\n  const argsArr = []\n  for (const [argsObjKey, argsObjValue] of Object.entries(argsObj)) {\n    if (argsObjKey !== '_' && argsObj[argsObjKey]) {\n      const prefix = argsObjKey.length === 1 ? '-' : '--';\n      // For boolean flags, don't append the value\n      if (argsObjValue === true) {\n        argsArr.push(`${prefix}${argsObjKey}`);\n      } else {\n        argsArr.push(`${prefix}${argsObjKey} ${JSON.stringify(argsObjValue)}`);\n      }\n    }\n  }\n  const claudeProcess = spawn(\n    claudePath,\n    argsArr,\n    {\n      env: {\n        ...process.env,\n      },\n      stdio: stdioConfig,\n      shell: true,\n    }\n  );\n\n  // Close stdin for non-interactive mode\n  if (config.NON_INTERACTIVE_MODE) {\n    claudeProcess.stdin?.end();\n  }\n\n  claudeProcess.on(\"error\", (error) => {\n    console.error(\"Failed to start claude command:\", error.message);\n    console.log(\n      \"Make sure Claude Code is installed: npm install -g @anthropic-ai/claude-code\"\n    );\n    decrementReferenceCount();\n    process.exit(1);\n  });\n\n  claudeProcess.on(\"close\", (code) => {\n    decrementReferenceCount();\n    closeService();\n    process.exit(code || 0);\n  });\n}\n"
  },
  {
    "path": "packages/cli/src/utils/createEnvVariables.ts",
    "content": "import { readConfigFile } from \".\";\n\n/**\n * Get environment variables for Agent SDK/Claude Code integration\n * This function is shared between `ccr env` and `ccr code` commands\n */\nexport const createEnvVariables = async (): Promise<Record<string, string | undefined>> => {\n  const config = await readConfigFile();\n  const port = config.PORT || 3456;\n  const apiKey = config.APIKEY || \"test\";\n\n  return {\n    ANTHROPIC_AUTH_TOKEN: apiKey,\n    ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}`,\n    NO_PROXY: \"127.0.0.1\",\n    DISABLE_TELEMETRY: \"true\",\n    DISABLE_COST_WARNINGS: \"true\",\n    API_TIMEOUT_MS: String(config.API_TIMEOUT_MS ?? 600000),\n    // Reset CLAUDE_CODE_USE_BEDROCK when running with ccr\n    CLAUDE_CODE_USE_BEDROCK: undefined,\n  };\n}\n"
  },
  {
    "path": "packages/cli/src/utils/index.ts",
    "content": "import fs from \"node:fs/promises\";\nimport readline from \"node:readline\";\nimport JSON5 from \"json5\";\nimport path from \"node:path\";\nimport { createHash } from \"node:crypto\";\nimport os from \"node:os\";\nimport {\n  CONFIG_FILE,\n  HOME_DIR, PID_FILE,\n  PLUGINS_DIR,\n  PRESETS_DIR,\n  REFERENCE_COUNT_FILE,\n  readPresetFile,\n} from \"@CCR/shared\";\nimport { getServer } from \"@CCR/server\";\nimport { writeFileSync, existsSync, readFileSync, mkdirSync } from \"fs\";\nimport { checkForUpdates, performUpdate } from \"./update\";\nimport { version } from \"../../package.json\";\nimport { spawn } from \"child_process\";\nimport {cleanupPidFile, isServiceRunning} from \"./processCheck\";\n\n// Function to interpolate environment variables in config values\nconst interpolateEnvVars = (obj: any): any => {\n  if (typeof obj === \"string\") {\n    // Replace $VAR_NAME or ${VAR_NAME} with environment variable values\n    return obj.replace(/\\$\\{([^}]+)\\}|\\$([A-Z_][A-Z0-9_]*)/g, (match, braced, unbraced) => {\n      const varName = braced || unbraced;\n      return process.env[varName] || match; // Keep original if env var doesn't exist\n    });\n  } else if (Array.isArray(obj)) {\n    return obj.map(interpolateEnvVars);\n  } else if (obj !== null && typeof obj === \"object\") {\n    const result: any = {};\n    for (const [key, value] of Object.entries(obj)) {\n      result[key] = interpolateEnvVars(value);\n    }\n    return result;\n  }\n  return obj;\n};\n\nconst ensureDir = async (dir_path: string) => {\n  try {\n    await fs.access(dir_path);\n  } catch {\n    await fs.mkdir(dir_path, { recursive: true });\n  }\n};\n\nexport const initDir = async () => {\n  await ensureDir(HOME_DIR);\n  await ensureDir(PLUGINS_DIR);\n  await ensureDir(PRESETS_DIR);\n  await ensureDir(path.join(HOME_DIR, \"logs\"));\n};\n\nconst createReadline = () => {\n  return readline.createInterface({\n    input: process.stdin,\n    output: process.stdout,\n  });\n};\n\nconst question = (query: string): Promise<string> => {\n  return new Promise((resolve) => {\n    const rl = createReadline();\n    rl.question(query, (answer) => {\n      rl.close();\n      resolve(answer);\n    });\n  });\n};\n\nconst confirm = async (query: string): Promise<boolean> => {\n  const answer = await question(query);\n  return answer.toLowerCase() !== \"n\";\n};\n\nexport const readConfigFile = async () => {\n  try {\n    const config = await fs.readFile(CONFIG_FILE, \"utf-8\");\n    try {\n      // Try to parse with JSON5 first (which also supports standard JSON)\n      const parsedConfig = JSON5.parse(config);\n      // Interpolate environment variables in the parsed config\n      return interpolateEnvVars(parsedConfig);\n    } catch (parseError) {\n      console.error(`Failed to parse config file at ${CONFIG_FILE}`);\n      console.error(\"Error details:\", (parseError as Error).message);\n      console.error(\"Please check your config file syntax.\");\n      process.exit(1);\n    }\n  } catch (readError: any) {\n    if (readError.code === \"ENOENT\") {\n      // Config file doesn't exist, prompt user for initial setup\n      try {\n        // Initialize directories\n        await initDir();\n\n        // Backup existing config file if it exists\n        const backupPath = await backupConfigFile();\n        if (backupPath) {\n          console.log(\n              `Backed up existing configuration file to ${backupPath}`\n          );\n        }\n        const config = {\n          PORT: 3456,\n          Providers: [],\n          Router: {},\n        }\n        // Create a minimal default config file\n        await writeConfigFile(config);\n        console.log(\n            \"Created minimal default configuration file at ~/.claude-code-router/config.json\"\n        );\n        console.log(\n            \"Please edit this file with your actual configuration.\"\n        );\n        return config\n      } catch (error: any) {\n        console.error(\n            \"Failed to create default configuration:\",\n            error.message\n        );\n        process.exit(1);\n      }\n    } else {\n      console.error(`Failed to read config file at ${CONFIG_FILE}`);\n      console.error(\"Error details:\", readError.message);\n      process.exit(1);\n    }\n  }\n};\n\nexport const backupConfigFile = async () => {\n  try {\n    if (await fs.access(CONFIG_FILE).then(() => true).catch(() => false)) {\n      const timestamp = new Date().toISOString().replace(/[:.]/g, '-');\n      const backupPath = `${CONFIG_FILE}.${timestamp}.bak`;\n      await fs.copyFile(CONFIG_FILE, backupPath);\n\n      // Clean up old backups, keeping only the 3 most recent\n      try {\n        const configDir = path.dirname(CONFIG_FILE);\n        const configFileName = path.basename(CONFIG_FILE);\n        const files = await fs.readdir(configDir);\n\n        // Find all backup files for this config\n        const backupFiles = files\n          .filter(file => file.startsWith(configFileName) && file.endsWith('.bak'))\n          .sort()\n          .reverse(); // Sort in descending order (newest first)\n\n        // Delete all but the 3 most recent backups\n        if (backupFiles.length > 3) {\n          for (let i = 3; i < backupFiles.length; i++) {\n            const oldBackupPath = path.join(configDir, backupFiles[i]);\n            await fs.unlink(oldBackupPath);\n          }\n        }\n      } catch (cleanupError) {\n        console.warn(\"Failed to clean up old backups:\", cleanupError);\n      }\n\n      return backupPath;\n    }\n  } catch (error) {\n    console.error(\"Failed to backup config file:\", error);\n  }\n  return null;\n};\n\nexport const writeConfigFile = async (config: any) => {\n  await ensureDir(HOME_DIR);\n  const configWithComment = `${JSON.stringify(config, null, 2)}`;\n  await fs.writeFile(CONFIG_FILE, configWithComment);\n};\n\nexport const initConfig = async () => {\n  const config = await readConfigFile();\n  Object.assign(process.env, config);\n  return config;\n};\n\nexport const run = async (args: string[] = []) => {\n  const isRunning = isServiceRunning()\n  if (isRunning) {\n    console.log('claude-code-router server is running');\n    return;\n  }\n  const server = await getServer();\n  const app = server.app;\n  // Save the PID of the background process\n  writeFileSync(PID_FILE, process.pid.toString());\n\n  app.post('/api/update/perform', async () => {\n    return await performUpdate();\n  })\n\n  app.get('/api/update/check', async () => {\n    return await checkForUpdates(version);\n  })\n\n  app.post(\"/api/restart\", async () => {\n    setTimeout(async () => {\n      spawn(\"ccr\", [\"restart\"], {\n        detached: true,\n        stdio: \"ignore\",\n      }).unref();\n    }, 100);\n\n    return { success: true, message: \"Service restart initiated\" }\n  });\n\n  // await server.start() to ensure it starts successfully and keep process alive\n  await server.start();\n}\n\nexport const restartService = async () => {\n  // Stop the service if it's running\n  try {\n    const pid = parseInt(readFileSync(PID_FILE, \"utf-8\"));\n    process.kill(pid);\n    cleanupPidFile();\n    if (existsSync(REFERENCE_COUNT_FILE)) {\n      try {\n        await fs.unlink(REFERENCE_COUNT_FILE);\n      } catch (e) {\n        // Ignore cleanup errors\n      }\n    }\n    console.log(\"claude code router service has been stopped.\");\n  } catch (e) {\n    console.log(\"Service was not running or failed to stop.\");\n    cleanupPidFile();\n  }\n\n  // Start the service again in the background\n  console.log(\"Starting claude code router service...\");\n  const cliPath = path.join(__dirname, \"cli.js\");\n  const startProcess = spawn(\"node\", [cliPath, \"start\"], {\n    detached: true,\n    stdio: \"ignore\",\n  });\n\n  startProcess.on(\"error\", (error) => {\n    console.error(\"Failed to start service:\", error);\n    throw error;\n  });\n\n  startProcess.unref();\n  console.log(\"✅ Service started successfully in the background.\");\n};\n\n\n/**\n * Get a temporary path for the settings file\n * Hash the content and return the file path if it already exists in temp directory,\n * otherwise create a new file with the content\n * @param content Settings content string\n * @returns Full path to the temporary file\n */\nexport const getSettingsPath = async (content: string): Promise<string> => {\n  // Hash the content using SHA256 algorithm\n  const hash = createHash('sha256').update(content, 'utf-8').digest('hex');\n\n  // Create claude-code-router directory in system temp folder\n  const tempDir = path.join(os.tmpdir(), 'claude-code-router');\n  const fileName = `ccr-settings-${hash}.json`;\n  const tempFilePath = path.join(tempDir, fileName);\n\n  // Ensure the directory exists\n  try {\n    await fs.access(tempDir);\n  } catch {\n    await fs.mkdir(tempDir, { recursive: true });\n  }\n\n  // Check if the file already exists\n  try {\n    await fs.access(tempFilePath);\n    return tempFilePath;\n  } catch {\n    // File doesn't exist, create and write content\n    await fs.writeFile(tempFilePath, content, 'utf-8');\n    return tempFilePath;\n  }\n}\n"
  },
  {
    "path": "packages/cli/src/utils/installCommand.ts",
    "content": "/**\n * Install preset from GitHub marketplace\n * ccr install {presetname}\n */\n\nimport { installPresetFromMarket } from './preset/install-github';\nimport { applyPresetCli } from './preset/install';\n\n// ANSI color codes\nconst RESET = \"\\x1B[0m\";\nconst GREEN = \"\\x1B[32m\";\nconst YELLOW = \"\\x1B[33m\";\nconst BOLDGREEN = \"\\x1B[1m\\x1B[32m\";\nconst BOLDYELLOW = \"\\x1B[1m\\x1B[33m\";\nconst BOLDCYAN = \"\\x1B[1m\\x1B[36m\";\nconst DIM = \"\\x1B[2m\";\n\n/**\n * Install preset from marketplace by preset name\n * @param presetName Preset name (must exist in marketplace)\n */\nexport async function handleInstallCommand(presetName: string): Promise<void> {\n  try {\n    if (!presetName) {\n      console.error(`\\n${BOLDYELLOW}Error:${RESET} Preset name is required\\n`);\n      console.error('Usage: ccr install <preset-name>\\n');\n      console.error('Examples:');\n      console.error('  ccr install my-preset');\n      console.error('  ccr install awesome-preset\\n');\n      console.error(`${DIM}Note: Preset must exist in the official marketplace.${RESET}\\n`);\n      process.exit(1);\n    }\n\n    console.log(`${BOLDCYAN}Installing preset:${RESET} ${presetName}\\n`);\n\n    // Install preset (download and extract)\n    const { name: installedName, preset } = await installPresetFromMarket(presetName);\n\n    if (installedName && preset) {\n      // Apply preset configuration (interactive setup)\n      await applyPresetCli(installedName, preset);\n\n      console.log(`\\n${BOLDGREEN}✓ Preset installation completed!${RESET}\\n`);\n    }\n\n  } catch (error: any) {\n    console.error(`\\n${BOLDYELLOW}Failed to install preset:${RESET} ${error.message}\\n`);\n    process.exit(1);\n  }\n}\n"
  },
  {
    "path": "packages/cli/src/utils/modelSelector.ts",
    "content": "import * as fs from 'fs';\nimport * as path from 'path';\nimport { select, input, confirm } from '@inquirer/prompts';\n\n// ANSI color codes\nconst RESET = \"\\x1B[0m\";\nconst DIM = \"\\x1B[2m\";\nconst BOLDGREEN = \"\\x1B[1m\\x1B[32m\";\nconst CYAN = \"\\x1B[36m\";\nconst BOLDCYAN = \"\\x1B[1m\\x1B[36m\";\nconst GREEN = \"\\x1B[32m\";\nconst YELLOW = \"\\x1B[33m\";\nconst BOLDYELLOW = \"\\x1B[1m\\x1B[33m\";\n\ninterface TransformerConfig {\n  use: Array<string | [string, any]>;\n  [key: string]: any;\n}\n\ninterface Provider {\n  name: string;\n  api_base_url: string;\n  api_key: string;\n  models: string[];\n  transformer?: TransformerConfig;\n}\n\ninterface RouterConfig {\n  default: string;\n  background?: string;\n  think?: string;\n  longContext?: string;\n  longContextThreshold?: number;\n  webSearch?: string;\n  image?: string;\n  [key: string]: string | number | undefined;\n}\n\ninterface Config {\n  Providers: Provider[];\n  Router: RouterConfig;\n  [key: string]: any;\n}\n\ninterface ModelResult {\n  providerName: string;\n  modelName: string;\n  modelType: string;\n}\n\nconst AVAILABLE_TRANSFORMERS = [\n  'anthropic',\n  'deepseek',\n  'gemini',\n  'openrouter',\n  'groq',\n  'maxtoken',\n  'tooluse',\n  'gemini-cli',\n  'reasoning',\n  'sampling',\n  'enhancetool',\n  'cleancache',\n  'vertex-gemini',\n  'chutes-glm',\n  'qwen-cli',\n  'rovo-cli'\n];\n\nfunction getConfigPath(): string {\n  const configDir = path.join(process.env.HOME || process.env.USERPROFILE || '', '.claude-code-router');\n  const configPath = path.join(configDir, 'config.json');\n  \n  if (!fs.existsSync(configPath)) {\n    throw new Error(`config.json not found at ${configPath}`);\n  }\n  \n  return configPath;\n}\n\nfunction loadConfig(): Config {\n  const configPath = getConfigPath();\n  return JSON.parse(fs.readFileSync(configPath, 'utf-8'));\n}\n\nfunction saveConfig(config: Config): void {\n  const configPath = getConfigPath();\n  fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');\n  console.log(`${GREEN}✓ config.json updated successfully${RESET}\\n`);\n}\n\nfunction getAllModels(config: Config) {\n  const models: any[] = [];\n  for (const provider of config.Providers) {\n    for (const model of provider.models) {\n      models.push({\n        name: `${BOLDCYAN}${provider.name}${RESET} → ${CYAN} ${model}`,\n        value: `${provider.name},${model}`,\n        description: `\\n${BOLDCYAN}Provider:${RESET} ${provider.name}`,\n        provider: provider.name,\n        model: model\n      });\n    }\n  }\n  return models;\n}\n\nfunction displayCurrentConfig(config: Config): void {\n  console.log(`\\n${BOLDCYAN}═══════════════════════════════════════════════${RESET}`);\n  console.log(`${BOLDCYAN}           Current Configuration${RESET}`);\n  console.log(`${BOLDCYAN}═══════════════════════════════════════════════${RESET}\\n`);\n  \n  const formatModel = (routerValue?: string | number) => {\n    if (!routerValue || typeof routerValue === 'number') {\n      return `${DIM}Not configured${RESET}`;\n    }\n    const [provider, model] = routerValue.split(',');\n    return `${YELLOW}${provider}${RESET} | ${model}\\n  ${DIM}- ${routerValue}${RESET}`;\n  };\n  \n  console.log(`${BOLDCYAN}Default Model:${RESET}`);\n  console.log(`  ${formatModel(config.Router.default)}\\n`);\n  \n  if (config.Router.background) {\n    console.log(`${BOLDCYAN}Background Model:${RESET}`);\n    console.log(`  ${formatModel(config.Router.background)}\\n`);\n  }\n  \n  if (config.Router.think) {\n    console.log(`${BOLDCYAN}Think Model:${RESET}`);\n    console.log(`  ${formatModel(config.Router.think)}\\n`);\n  }\n  \n  if (config.Router.longContext) {\n    console.log(`${BOLDCYAN}Long Context Model:${RESET}`);\n    console.log(`  ${formatModel(config.Router.longContext)}\\n`);\n  }\n  \n  if (config.Router.webSearch) {\n    console.log(`${BOLDCYAN}Web Search Model:${RESET}`);\n    console.log(`  ${formatModel(config.Router.webSearch)}\\n`);\n  }\n  \n  if (config.Router.image) {\n    console.log(`${BOLDCYAN}Image Model:${RESET}`);\n    console.log(`  ${formatModel(config.Router.image)}\\n`);\n  }\n  \n  console.log(`\\n${BOLDCYAN}═══════════════════════════════════════════════${RESET}`);\n  console.log(`${BOLDCYAN}           Add/Update Model${RESET}`);\n  console.log(`${BOLDCYAN}═══════════════════════════════════════════════${RESET}\\n`);\n}\n\nasync function selectModelType() {\n  return await select({\n    message: `${BOLDYELLOW}Which model configuration do you want to update?${RESET}`,\n    choices: [\n      { name: 'Default Model', value: 'default' },\n      { name: 'Background Model', value: 'background' },\n      { name: 'Think Model', value: 'think' },\n      { name: 'Long Context Model', value: 'longContext' },\n      { name: 'Web Search Model', value: 'webSearch' },\n      { name: 'Image Model', value: 'image' },\n      { name: `${BOLDGREEN}+ Add New Model${RESET}`, value: 'addModel' }\n    ]\n  });\n}\n\nasync function selectModel(config: Config, modelType: string) {\n  const models = getAllModels(config);\n  \n  return await select({\n    message: `\\n${BOLDYELLOW}Select a model for ${modelType}:${RESET}`,\n    choices: models,\n    pageSize: 15\n  });\n}\n\nasync function configureTransformers(): Promise<TransformerConfig | undefined> {\n  const useTransformers = await confirm({\n    message: `\\n${BOLDYELLOW}Add transformer configuration?${RESET}`,\n    default: false\n  });\n  \n  if (!useTransformers) {\n    return undefined;\n  }\n  \n  const transformers: Array<string | [string, any]> = [];\n  let addMore = true;\n  \n  while (addMore) {\n    const transformer = await select({\n      message: `\\n${BOLDYELLOW}Select a transformer:${RESET}`,\n      choices: AVAILABLE_TRANSFORMERS.map(t => ({ name: t, value: t })),\n      pageSize: 15\n    }) as string;\n    \n    // Check if transformer needs options\n    if (transformer === 'maxtoken') {\n      const maxTokens = await input({\n        message: `\\n${BOLDYELLOW}Max tokens:${RESET}`,\n        default: '30000',\n        validate: (value: string) => {\n          const num = parseInt(value);\n          if (isNaN(num) || num <= 0) {\n            return 'Please enter a valid positive number';\n          }\n          return true;\n        }\n      });\n      transformers.push(['maxtoken', { max_tokens: parseInt(maxTokens) }]);\n    } else if (transformer === 'openrouter') {\n      const addProvider = await confirm({\n        message: `\\n${BOLDYELLOW}Add provider routing options?${RESET}`,\n        default: false\n      });\n      \n      if (addProvider) {\n        const providerInput = await input({\n          message: 'Provider (e.g., moonshotai/fp8):',\n          validate: (value: string) => value.trim() !== '' || 'Provider cannot be empty'\n        });\n        transformers.push(['openrouter', { provider: { only: [providerInput] } }]);\n      } else {\n        transformers.push(transformer);\n      }\n    } else {\n      transformers.push(transformer);\n    }\n    \n    addMore = await confirm({\n      message: `\\n${BOLDYELLOW}Add another transformer?${RESET}`,\n      default: false\n    });\n  }\n  \n  return { use: transformers };\n}\n\nasync function addNewModel(config: Config): Promise<ModelResult | null> {\n  const providerChoices = config.Providers.map(p => ({\n    name: p.name,\n    value: p.name\n  }));\n  \n  providerChoices.push({ name: `${BOLDGREEN}+ Add New Provider${RESET}`, value: '__new__' });\n  \n  const selectedProvider = await select({\n    message: `\\n${BOLDYELLOW}Select provider for the new model:${RESET}`,\n    choices: providerChoices\n  }) as string;\n  \n  if (selectedProvider === '__new__') {\n    return await addNewProvider(config);\n  } else {\n    return await addModelToExistingProvider(config, selectedProvider);\n  }\n}\n\nasync function addModelToExistingProvider(config: Config, providerName: string): Promise<ModelResult | null> {\n  const modelName = await input({\n    message: `\\n${BOLDYELLOW}Enter the model name:${RESET}`,\n    validate: (value: string) => {\n      if (!value.trim()) {\n        return 'Model name cannot be empty';\n      }\n      return true;\n    }\n  });\n  \n  const provider = config.Providers.find(p => p.name === providerName);\n  \n  if (!provider) {\n    console.log(`${YELLOW}Provider not found${RESET}`);\n    return null;\n  }\n  \n  if (provider.models.includes(modelName)) {\n    console.log(`${YELLOW}Model already exists in provider${RESET}`);\n    return null;\n  }\n  \n  provider.models.push(modelName);\n  \n  // Ask about model-specific transformers\n  const addModelTransformer = await confirm({\n    message: `\\n${BOLDYELLOW}Add model-specific transformer configuration?${RESET}`,\n    default: false\n  });\n  \n  if (addModelTransformer) {\n    const transformerConfig = await configureTransformers();\n    if (transformerConfig && provider.transformer) {\n      provider.transformer[modelName] = transformerConfig;\n    }\n  }\n  \n  saveConfig(config);\n  \n  console.log(`${GREEN}✓ Model \"${modelName}\" added to provider \"${providerName}\"${RESET}`);\n  \n  const setAsDefault = await confirm({\n    message: `\\n${BOLDYELLOW}Do you want to set this model in router configuration?${RESET}`,\n    default: false\n  });\n  \n  if (setAsDefault) {\n    const modelType = await select({\n      message: `\\n${BOLDYELLOW}Select configuration type:${RESET}`,\n      choices: [\n        { name: 'Default Model', value: 'default' },\n        { name: 'Background Model', value: 'background' },\n        { name: 'Think Model', value: 'think' },\n        { name: 'Long Context Model', value: 'longContext' },\n        { name: 'Web Search Model', value: 'webSearch' },\n        { name: 'Image Model', value: 'image' }\n      ]\n    }) as string;\n    \n    return { providerName, modelName, modelType };\n  }\n  \n  return null;\n}\n\nasync function addNewProvider(config: Config): Promise<ModelResult | null> {\n  console.log(`\\n${BOLDCYAN}Adding New Provider${RESET}\\n`);\n  \n  const providerName = await input({\n    message: `${BOLDYELLOW}Provider name:${RESET}`,\n    validate: (value: string) => {\n      if (!value.trim()) {\n        return 'Provider name cannot be empty';\n      }\n      if (config.Providers.some(p => p.name === value)) {\n        return 'Provider already exists';\n      }\n      return true;\n    }\n  });\n  \n  const apiBaseUrl = await input({\n    message: `\\n${BOLDYELLOW}API base URL:${RESET}`,\n    validate: (value: string) => {\n      if (!value.trim()) {\n        return 'API base URL cannot be empty';\n      }\n      try {\n        new URL(value);\n        return true;\n      } catch {\n        return 'Please enter a valid URL';\n      }\n    }\n  });\n  \n  const apiKey = await input({\n    message: `\\n${BOLDYELLOW}API key:${RESET}`,\n    validate: (value: string) => {\n      if (!value.trim()) {\n        return 'API key cannot be empty';\n      }\n      return true;\n    }\n  });\n  \n  const modelsInput = await input({\n    message: `\\n${BOLDYELLOW}Model names (comma-separated):${RESET}`,\n    validate: (value: string) => {\n      if (!value.trim()) {\n        return 'At least one model name is required';\n      }\n      return true;\n    }\n  });\n  \n  const models = modelsInput.split(',').map(m => m.trim()).filter(m => m);\n  \n  const newProvider: Provider = {\n    name: providerName,\n    api_base_url: apiBaseUrl,\n    api_key: apiKey,\n    models: models\n  };\n  \n  // Global transformer configuration\n  const transformerConfig = await configureTransformers();\n  if (transformerConfig) {\n    newProvider.transformer = transformerConfig;\n  }\n  \n  config.Providers.push(newProvider);\n  saveConfig(config);\n  \n  console.log(`${GREEN}\\n✓ Provider \"${providerName}\" added successfully${RESET}`);\n  \n  const setAsDefault = await confirm({\n    message: `\\n${BOLDYELLOW}Do you want to set one of these models in router configuration?${RESET}`,\n    default: false\n  });\n  \n  if (setAsDefault && models.length > 0) {\n    let selectedModel = models[0];\n    \n    if (models.length > 1) {\n      selectedModel = await select({\n        message: `\\n${BOLDYELLOW}Select which model to configure:${RESET}`,\n        choices: models.map(m => ({ name: m, value: m }))\n      }) as string;\n    }\n    \n    const modelType = await select({\n      message: `\\n${BOLDYELLOW}Select configuration type:${RESET}`,\n      choices: [\n        { name: 'Default Model', value: 'default' },\n        { name: 'Background Model', value: 'background' },\n        { name: 'Think Model', value: 'think' },\n        { name: 'Long Context Model', value: 'longContext' },\n        { name: 'Web Search Model', value: 'webSearch' },\n        { name: 'Image Model', value: 'image' }\n      ]\n    }) as string;\n    \n    return { providerName, modelName: selectedModel, modelType };\n  }\n  \n  return null;\n}\n\nexport async function runModelSelector(): Promise<void> {\n  console.clear();\n  \n  try {\n    let config = loadConfig();\n    displayCurrentConfig(config);\n    \n    const action = await selectModelType() as string;\n    \n    if (action === 'addModel') {\n      const result = await addNewModel(config);\n      \n      if (result) {\n        config = loadConfig();\n        config.Router[result.modelType] = `${result.providerName},${result.modelName}`;\n        saveConfig(config);\n        console.log(`${GREEN}✓ ${result.modelType} set to ${result.providerName},${result.modelName}${RESET}`);\n      }\n    } else {\n      const selectedModel = await selectModel(config, action) as string;\n      config.Router[action] = selectedModel;\n      saveConfig(config);\n      \n      console.log(`${GREEN}✓ ${action} model updated to: ${selectedModel}${RESET}`);\n    }\n    \n    displayCurrentConfig(config);\n  } catch (error: any) {\n    console.error(`${YELLOW}Error:${RESET}`, error.message);\n    process.exit(1);\n  }\n}"
  },
  {
    "path": "packages/cli/src/utils/preset/commands.ts",
    "content": "/**\n * Preset command handler CLI layer\n * Handles CLI interactions, core logic is in the shared package\n */\n\nimport * as fs from 'fs/promises';\nimport * as path from 'path';\nimport JSON5 from 'json5';\nimport { exportPresetCli } from './export';\nimport { installPresetCli, loadPreset } from './install';\nimport { HOME_DIR } from '@CCR/shared';\n\n// ANSI color codes\nconst RESET = \"\\x1B[0m\";\nconst GREEN = \"\\x1B[32m\";\nconst YELLOW = \"\\x1B[33m\";\nconst BOLDCYAN = \"\\x1B[1m\\x1B[36m\";\nconst BOLDYELLOW = \"\\x1B[1m\\x1B[33m\";\nconst DIM = \"\\x1B[2m\";\n\n/**\n * List local presets\n */\nasync function listPresets(): Promise<void> {\n  const presetsDir = path.join(HOME_DIR, 'presets');\n\n  try {\n    await fs.access(presetsDir);\n  } catch {\n    console.log('\\nNo presets directory found.');\n    console.log(`\\nCreate your first preset with: ${GREEN}ccr preset export <name>${RESET}\\n`);\n    return;\n  }\n\n  const entries = await fs.readdir(presetsDir, { withFileTypes: true });\n  const presetDirs = entries.filter(e => e.isDirectory() && !e.name.startsWith('.')).map(e => e.name);\n\n  if (presetDirs.length === 0) {\n    console.log('\\nNo presets found.');\n    console.log(`\\nInstall a preset with: ${GREEN}ccr preset install <file>${RESET}\\n`);\n    return;\n  }\n\n  console.log(`\\n${BOLDCYAN}Available presets:${RESET}\\n`);\n\n  for (const dirName of presetDirs) {\n    const presetDir = path.join(presetsDir, dirName);\n    try {\n      const manifestPath = path.join(presetDir, 'manifest.json');\n      const content = await fs.readFile(manifestPath, 'utf-8');\n      const manifest = JSON5.parse(content);\n\n      // Extract metadata fields from manifest\n      const { Providers, Router, PORT, HOST, API_TIMEOUT_MS, PROXY_URL, LOG, LOG_LEVEL, StatusLine, NON_INTERACTIVE_MODE, ...metadata } = manifest;\n\n      const name = metadata.name || dirName;\n      const description = metadata.description || '';\n      const author = metadata.author || '';\n      const version = metadata.version;\n\n      // Display preset name\n      if (version) {\n        console.log(`${GREEN}•${RESET} ${BOLDCYAN}${name}${RESET} (v${version})`);\n      } else {\n        console.log(`${GREEN}•${RESET} ${BOLDCYAN}${name}${RESET}`);\n      }\n\n      // Display description\n      if (description) {\n        console.log(`  ${description}`);\n      }\n\n      // Display author\n      if (author) {\n        console.log(`  ${DIM}by ${author}${RESET}`);\n      }\n\n      console.log('');\n    } catch (error) {\n      console.log(`${YELLOW}•${RESET} ${dirName}`);\n      console.log(`  ${DIM}(Error reading preset)${RESET}\\n`);\n    }\n  }\n}\n\n/**\n * Delete preset\n */\nasync function deletePreset(name: string): Promise<void> {\n  const presetsDir = path.join(HOME_DIR, 'presets');\n\n  // Validate preset name (prevent path traversal)\n  if (!name || name.includes('..') || name.includes('/') || name.includes('\\\\')) {\n    console.error(`\\n${YELLOW}Error:${RESET} Invalid preset name.\\n`);\n    process.exit(1);\n  }\n\n  const presetDir = path.join(presetsDir, name);\n\n  try {\n    // Recursively delete entire directory\n    await fs.rm(presetDir, { recursive: true, force: true });\n    console.log(`\\n${GREEN}✓${RESET} Preset \"${name}\" deleted.\\n`);\n  } catch (error: any) {\n    if (error.code === 'ENOENT') {\n      console.error(`\\n${YELLOW}Error:${RESET} Preset \"${name}\" not found.\\n`);\n    } else {\n      console.error(`\\n${YELLOW}Error:${RESET} ${error.message}\\n`);\n    }\n    process.exit(1);\n  }\n}\n\n/**\n * Show preset information\n */\nasync function showPresetInfo(name: string): Promise<void> {\n  try {\n    const preset = await loadPreset(name);\n\n    const config = preset.config;\n    const metadata = preset.metadata;\n\n    console.log(`\\n${BOLDCYAN}═══════════════════════════════════════════════${RESET}`);\n    if (metadata?.name) {\n      console.log(`${BOLDCYAN}Preset: ${RESET}${metadata.name}`);\n    } else {\n      console.log(`${BOLDCYAN}Preset: ${RESET}${name}`);\n    }\n    console.log(`${BOLDCYAN}═══════════════════════════════════════════════${RESET}\\n`);\n\n    if (metadata?.version) console.log(`${BOLDCYAN}Version:${RESET} ${metadata.version}`);\n    if (metadata?.description) console.log(`${BOLDCYAN}Description:${RESET} ${metadata.description}`);\n    if (metadata?.author) console.log(`${BOLDCYAN}Author:${RESET} ${metadata.author}`);\n    const keywords = metadata?.keywords;\n    if (keywords && keywords.length > 0) {\n      console.log(`${BOLDCYAN}Keywords:${RESET} ${keywords.join(', ')}`);\n    }\n\n    console.log(`\\n${BOLDCYAN}Configuration:${RESET}`);\n    if (config.Providers) {\n      console.log(`  Providers: ${config.Providers.length}`);\n    }\n    if (config.Router) {\n      console.log(`  Router rules: ${Object.keys(config.Router).length}`);\n    }\n    if (config.provider) {\n      console.log(`  Provider: ${config.provider}`);\n    }\n\n    if (preset.schema && preset.schema.length > 0) {\n      console.log(`\\n${BOLDYELLOW}Required inputs:${RESET}`);\n      for (const input of preset.schema) {\n        const label = input.label || input.id;\n        const prompt = input.prompt || '';\n        console.log(`  - ${label}${prompt ? ` ${DIM}(${prompt})${RESET}` : ''}`);\n      }\n    }\n\n    console.log('');\n  } catch (error: any) {\n    console.error(`\\n${YELLOW}Error:${RESET} ${error.message}\\n`);\n    process.exit(1);\n  }\n}\n\n/**\n * Handle preset commands\n */\nexport async function handlePresetCommand(args: string[]): Promise<void> {\n  const subCommand = args[0];\n\n  switch (subCommand) {\n    case 'export':\n      const presetName = args[1];\n      if (!presetName) {\n        console.error('\\nError: Preset name is required\\n');\n        console.error('Usage: ccr preset export <name> [--output <path>] [--description <text>] [--author <name>] [--tags <tags>]\\n');\n        process.exit(1);\n      }\n\n      // Parse options\n      const options: any = {};\n      for (let i = 2; i < args.length; i++) {\n        if (args[i] === '--output' && args[i + 1]) {\n          options.output = args[++i];\n        } else if (args[i] === '--description' && args[i + 1]) {\n          options.description = args[++i];\n        } else if (args[i] === '--author' && args[i + 1]) {\n          options.author = args[++i];\n        } else if (args[i] === '--tags' && args[i + 1]) {\n          options.tags = args[++i];\n        } else if (args[i] === '--include-sensitive') {\n          options.includeSensitive = true;\n        }\n      }\n\n      await exportPresetCli(presetName, options);\n      break;\n\n    case 'install':\n      const source = args[1];\n      if (!source) {\n        console.error('\\nError: Preset source is required\\n');\n        console.error('Usage: ccr preset install <file | url | name>\\n');\n        process.exit(1);\n      }\n\n      await installPresetCli(source, {});\n      break;\n\n    case 'list':\n      await listPresets();\n      break;\n\n    case 'delete':\n    case 'rm':\n    case 'remove':\n      const deleteName = args[1];\n      if (!deleteName) {\n        console.error('\\nError: Preset name is required\\n');\n        console.error('Usage: ccr preset delete <name>\\n');\n        process.exit(1);\n      }\n      await deletePreset(deleteName);\n      break;\n\n    case 'info':\n      const infoName = args[1];\n      if (!infoName) {\n        console.error('\\nError: Preset name is required\\n');\n        console.error('Usage: ccr preset info <name>\\n');\n        process.exit(1);\n      }\n      await showPresetInfo(infoName);\n      break;\n\n    default:\n      console.error(`\\nError: Unknown preset command \"${subCommand}\"\\n`);\n      console.error('Available commands:');\n      console.error('  ccr preset export <name>      Export current configuration as a preset');\n      console.error('  ccr preset install <source>   Install a preset from file, URL, or registry');\n      console.error('  ccr preset list              List installed presets');\n      console.error('  ccr preset info <name>        Show preset information');\n      console.error('  ccr preset delete <name>      Delete a preset\\n');\n      process.exit(1);\n  }\n}\n"
  },
  {
    "path": "packages/cli/src/utils/preset/export.ts",
    "content": "/**\n * Preset export functionality CLI layer\n * Handles CLI interactions, core logic is in shared package\n */\n\nimport { input } from '@inquirer/prompts';\nimport { readConfigFile } from '../index';\nimport { exportPreset as exportPresetCore, ExportOptions } from '@CCR/shared';\n\n// ANSI color codes\nconst RESET = \"\\x1B[0m\";\nconst GREEN = \"\\x1B[32m\";\nconst BOLDGREEN = \"\\x1B[1m\\x1B[32m\";\nconst YELLOW = \"\\x1B[33m\";\nconst BOLDCYAN = \"\\x1B[1m\\x1B[36m\";\n\n/**\n * Export preset configuration (CLI version, with interaction)\n * @param presetName Preset name\n * @param options Export options\n */\nexport async function exportPresetCli(\n  presetName: string,\n  options: ExportOptions = {}\n): Promise<void> {\n  try {\n    console.log(`\\n${BOLDCYAN}═══════════════════════════════════════════════${RESET}`);\n    console.log(`${BOLDCYAN}              Preset Export${RESET}`);\n    console.log(`${BOLDCYAN}═══════════════════════════════════════════════${RESET}\\n`);\n\n    // 1. Read current configuration\n    const config = await readConfigFile();\n\n    // 2. Interactively ask for metadata if not provided via command line\n    if (!options.description) {\n      try {\n        options.description = await input({\n          message: 'Description (optional):',\n          default: '',\n        });\n      } catch {\n        // User cancelled, use default value\n        options.description = '';\n      }\n    }\n\n    if (!options.author) {\n      try {\n        options.author = await input({\n          message: 'Author (optional):',\n          default: '',\n        });\n      } catch {\n        options.author = '';\n      }\n    }\n\n    if (!options.tags) {\n      try {\n        const keywordsInput = await input({\n          message: 'Keywords (comma-separated, optional):',\n          default: '',\n        });\n        options.tags = keywordsInput || '';\n      } catch {\n        options.tags = '';\n      }\n    }\n\n    // 3. Call core export functionality\n    const result = await exportPresetCore(presetName, config, options);\n\n    // 4. Display summary\n    console.log(`\\n${BOLDGREEN}✓ Preset exported successfully${RESET}\\n`);\n    console.log(`${BOLDCYAN}Location:${RESET} ${result.presetDir}\\n`);\n    console.log(`${BOLDCYAN}Summary:${RESET}`);\n    console.log(`  - Providers: ${result.sanitizedConfig.Providers?.length || 0}`);\n    console.log(`  - Router rules: ${Object.keys(result.sanitizedConfig.Router || {}).length}`);\n    if (!options.includeSensitive) {\n      console.log(`  - Sensitive fields sanitized: ${YELLOW}${result.sanitizedCount}${RESET}`);\n    }\n\n    // Display metadata\n    if (result.metadata.description) {\n      console.log(`\\n${BOLDCYAN}Description:${RESET} ${result.metadata.description}`);\n    }\n    if (result.metadata.author) {\n      console.log(`${BOLDCYAN}Author:${RESET} ${result.metadata.author}`);\n    }\n    if (result.metadata.keywords && result.metadata.keywords.length > 0) {\n      console.log(`${BOLDCYAN}Keywords:${RESET} ${result.metadata.keywords.join(', ')}`);\n    }\n\n    // Display sharing tips\n    console.log(`\\n${BOLDCYAN}To share this preset:${RESET}`);\n    console.log(`  1. Share the directory: ${result.presetDir}`);\n    console.log(`  2. Upload to GitHub Gist or your repository`);\n    console.log(`  3. Others can install with: ${GREEN}ccr preset install <directory>${RESET}\\n`);\n\n  } catch (error: any) {\n    console.error(`\\n${YELLOW}Error exporting preset:${RESET} ${error.message}`);\n    throw error;\n  }\n}\n"
  },
  {
    "path": "packages/cli/src/utils/preset/index.ts",
    "content": "/**\n * 预设功能 CLI 层\n * 导出所有预设相关的功能和类型\n */\n\n// 从 shared 包重新导出类型和核心功能\nexport * from '@CCR/shared';\n\n// 导出 CLI 特定的功能（带交互）\nexport { exportPresetCli } from './export';\nexport { installPresetCli, applyPresetCli } from './install';\nexport { handlePresetCommand } from './commands';\n"
  },
  {
    "path": "packages/cli/src/utils/preset/install-github.ts",
    "content": "/**\n * Install preset from GitHub marketplace by preset name\n */\n\nimport * as fs from 'fs/promises';\nimport {\n  findMarketPresetByName,\n  getPresetDir,\n  readManifestFromDir,\n  saveManifest,\n  isPresetInstalled,\n  downloadPresetToTemp,\n  extractPreset,\n  manifestToPresetFile,\n  type PresetFile,\n} from '@CCR/shared';\nimport AdmZip from 'adm-zip';\n\n// ANSI color codes\nconst RESET = \"\\x1B[0m\";\nconst GREEN = \"\\x1B[32m\";\nconst BOLDCYAN = \"\\x1B[1m\\x1B[36m\";\nconst BOLDYELLOW = \"\\x1B[1m\\x1B[33m\";\n\n/**\n * Parse GitHub repository URL or name\n * Supports:\n * - owner/repo (short format)\n * - github.com/owner/repo\n * - https://github.com/owner/repo\n * - https://github.com/owner/repo.git\n * - git@github.com:owner/repo.git\n */\nfunction parseGitHubRepo(input: string): { owner: string; repoName: string } | null {\n  const match = input.match(/(?:github\\.com[:/]|^)([^/]+)\\/([^/\\s#]+?)(?:\\.git)?$/);\n  if (!match) {\n    return null;\n  }\n\n  const [, owner, repoName] = match;\n  return { owner, repoName };\n}\n\n/**\n * Load preset from ZIP file\n */\nasync function loadPresetFromZip(zipFile: string): Promise<PresetFile> {\n  const zip = new AdmZip(zipFile);\n\n  // First try to find manifest.json in root directory\n  let entry = zip.getEntry('manifest.json');\n\n  // If not in root, try to find in subdirectories (handle GitHub repo archive structure)\n  if (!entry) {\n    const entries = zip.getEntries();\n    // Find any manifest.json file\n    entry = entries.find(e => e.entryName.includes('manifest.json')) || null;\n  }\n\n  if (!entry) {\n    throw new Error('Invalid preset file: manifest.json not found');\n  }\n\n  const manifest = JSON.parse(entry.getData().toString('utf-8'));\n  return manifestToPresetFile(manifest);\n}\n\n/**\n * Install preset from marketplace by preset name\n * @param presetName Preset name (must exist in marketplace)\n * @returns Object containing installed preset name and PresetFile\n */\nexport async function installPresetFromMarket(presetName: string): Promise<{ name: string; preset: PresetFile }> {\n  // Check if preset is in the marketplace\n  console.log(`${BOLDCYAN}Checking marketplace...${RESET}`);\n\n  const marketPreset = await findMarketPresetByName(presetName);\n\n  if (!marketPreset) {\n    throw new Error(\n      `Preset '${presetName}' not found in marketplace. ` +\n      `Please check the available presets at: https://github.com/claude-code-router/presets`\n    );\n  }\n\n  console.log(`${GREEN}✓${RESET} Found in marketplace\\n`);\n\n  // Get repository from market preset\n  if (!marketPreset.repo) {\n    throw new Error(`Preset '${presetName}' does not have repository information`);\n  }\n\n  // Parse GitHub repository URL\n  const githubRepo = parseGitHubRepo(marketPreset.repo);\n  if (!githubRepo) {\n    throw new Error(`Invalid repository format: ${marketPreset.repo}`);\n  }\n\n  const { owner, repoName } = githubRepo;\n\n  // Use preset name from market (or the preset's id)\n  const installedPresetName = marketPreset.name || presetName;\n\n  // Check if already installed BEFORE downloading\n  if (await isPresetInstalled(installedPresetName)) {\n    throw new Error(\n      `Preset '${installedPresetName}' is already installed.\\n` +\n      `To delete and reinstall, use: ccr preset delete ${installedPresetName}\\n` +\n      `To reconfigure without deleting, use: ccr preset install ${installedPresetName}`\n    );\n  }\n\n  // Download GitHub repository ZIP file\n  console.log(`${BOLDCYAN}Downloading preset...${RESET}`);\n\n  const downloadUrl = `https://github.com/${owner}/${repoName}/archive/refs/heads/main.zip`;\n  const tempFile = await downloadPresetToTemp(downloadUrl);\n\n  console.log(`${GREEN}✓${RESET} Downloaded\\n`);\n\n  try {\n    // Load preset to validate structure\n    console.log(`${BOLDCYAN}Validating preset...${RESET}`);\n    const preset = await loadPresetFromZip(tempFile);\n    console.log(`${GREEN}✓${RESET} Valid\\n`);\n\n    // Double-check if already installed (in case of race condition)\n    if (await isPresetInstalled(installedPresetName)) {\n      throw new Error(\n        `Preset '${installedPresetName}' was installed while downloading. ` +\n        `Please try again.`\n      );\n    }\n\n    // Extract to target directory\n    console.log(`${BOLDCYAN}Installing preset...${RESET}`);\n    const targetDir = getPresetDir(installedPresetName);\n    await extractPreset(tempFile, targetDir);\n    console.log(`${GREEN}✓${RESET} Installed\\n`);\n\n    // Read manifest and add repo information\n    const manifest = await readManifestFromDir(targetDir);\n\n    // Add repo information to manifest\n    manifest.repository = marketPreset.repository;\n    if (marketPreset.url) {\n      manifest.source = marketPreset.url;\n    }\n\n    // Save updated manifest\n    await saveManifest(installedPresetName, manifest);\n\n    // Return preset name and PresetFile for further configuration\n    return { name: installedPresetName, preset };\n  } finally {\n    // Clean up temp file\n    try {\n      await fs.unlink(tempFile);\n    } catch {\n      // Ignore cleanup errors\n    }\n  }\n}\n"
  },
  {
    "path": "packages/cli/src/utils/preset/install.ts",
    "content": "/**\n * Preset installation functionality CLI layer\n * Handles CLI interactions, core logic is in the shared package\n */\n\nimport * as fs from 'fs/promises';\nimport * as path from 'path';\nimport {\n  loadPreset as loadPresetShared,\n  validatePreset,\n  MergeStrategy,\n  getPresetDir,\n  readManifestFromDir,\n  manifestToPresetFile,\n  saveManifest,\n  isPresetInstalled,\n  ManifestFile,\n  PresetFile,\n  UserInputValues,\n} from '@CCR/shared';\nimport { collectUserInputs } from '../prompt/schema-input';\n\n// Re-export loadPreset\nexport { loadPresetShared as loadPreset };\n\n// ANSI color codes\nconst RESET = \"\\x1B[0m\";\nconst GREEN = \"\\x1B[32m\";\nconst BOLDGREEN = \"\\x1B[1m\\x1B[32m\";\nconst YELLOW = \"\\x1B[33m\";\nconst BOLDYELLOW = \"\\x1B[1m\\x1B[33m\";\nconst BOLDCYAN = \"\\x1B[1m\\x1B[36m\";\nconst DIM = \"\\x1B[2m\";\n\n/**\n * Apply preset to configuration\n * @param presetName Preset name\n * @param preset Preset object\n */\nexport async function applyPresetCli(\n  presetName: string,\n  preset: PresetFile\n): Promise<void> {\n  try {\n    console.log(`${BOLDCYAN}Loading preset...${RESET} ${GREEN}✓${RESET}`);\n\n    // Validate preset\n    const validation = await validatePreset(preset);\n    if (validation.warnings.length > 0) {\n      console.log(`\\n${YELLOW}Warnings:${RESET}`);\n      for (const warning of validation.warnings) {\n        console.log(`  ${DIM}⚠${RESET} ${warning}`);\n      }\n    }\n\n    if (!validation.valid) {\n      console.log(`\\n${YELLOW}Validation errors:${RESET}`);\n      for (const error of validation.errors) {\n        console.log(`${YELLOW}✗${RESET} ${error}`);\n      }\n      throw new Error('Invalid preset file');\n    }\n\n    console.log(`${BOLDCYAN}Validating preset...${RESET} ${GREEN}✓${RESET}`);\n\n    // Check if configuration is required\n    if (preset.schema && preset.schema.length > 0) {\n      console.log(`\\n${BOLDCYAN}Configuration required:${RESET} ${preset.schema.length} field(s)\\n`);\n    } else {\n      console.log(`\\n${DIM}No configuration required for this preset${RESET}\\n`);\n    }\n\n    // Collect user inputs\n    let userInputs: UserInputValues = {};\n\n    // Use schema system\n    if (preset.schema && preset.schema.length > 0) {\n      userInputs = await collectUserInputs(preset.schema, preset.config);\n    }\n\n    // Read existing manifest to preserve fields like repository, source, etc.\n    const presetDir = getPresetDir(presetName);\n    let existingManifest: ManifestFile | null = null;\n    \n    try {\n      existingManifest = await readManifestFromDir(presetDir);\n    } catch {\n      // Manifest doesn't exist yet, this is a new installation\n    }\n\n    // Build manifest, preserve existing fields\n    const manifest: ManifestFile = {\n      name: presetName,\n      version: preset.metadata?.version || '1.0.0',\n      ...(preset.metadata || {}),\n      ...preset.config,  // Keep original config (may contain placeholders)\n    };\n\n    // Preserve fields from existing manifest (repository, source, etc.)\n    if (existingManifest) {\n      if (existingManifest.repository) {\n        manifest.repository = existingManifest.repository;\n      }\n      if (existingManifest.source) {\n        manifest.source = existingManifest.source;\n      }\n      if (existingManifest.sourceType) {\n        manifest.sourceType = existingManifest.sourceType;\n      }\n      if (existingManifest.checksum) {\n        manifest.checksum = existingManifest.checksum;\n      }\n    }\n\n    // Save schema (if exists)\n    if (preset.schema) {\n      manifest.schema = preset.schema;\n    }\n\n    // Save other configurations\n    if (preset.template) {\n      manifest.template = preset.template;\n    }\n    if (preset.configMappings) {\n      manifest.configMappings = preset.configMappings;\n    }\n\n    // Save user-filled values to userValues\n    if (Object.keys(userInputs).length > 0) {\n      manifest.userValues = userInputs;\n    }\n\n    // Save to manifest.json in extracted directory\n    await saveManifest(presetName, manifest);\n\n    // Display summary\n    console.log(`\\n${BOLDGREEN}✓ Preset configured successfully!${RESET}\\n`);\n    console.log(`${BOLDCYAN}Preset directory:${RESET} ${presetDir}`);\n    console.log(`${BOLDCYAN}Inputs configured:${RESET} ${Object.keys(userInputs).length}`);\n\n    if (preset.metadata?.description) {\n      console.log(`\\n${BOLDCYAN}Description:${RESET} ${preset.metadata.description}`);\n    }\n\n    if (preset.metadata?.author) {\n      console.log(`${BOLDCYAN}Author:${RESET} ${preset.metadata.author}`);\n    }\n\n    const keywords = (preset.metadata as any).keywords;\n    if (keywords && keywords.length > 0) {\n      console.log(`${BOLDCYAN}Keywords:${RESET} ${keywords.join(', ')}`);\n    }\n\n    console.log(`\\n${GREEN}Use this preset:${RESET} ccr ${presetName} \"your prompt\"`);\n    console.log(`${DIM}Note: Configuration is stored in the manifest file${RESET}\\n`);\n\n  } catch (error: any) {\n    console.error(`\\n${YELLOW}Error applying preset:${RESET} ${error.message}`);\n    throw error;\n  }\n}\n\n/**\n * Install preset (main entry point)\n */\nexport async function installPresetCli(\n  source: string,\n  options: {\n    strategy?: MergeStrategy;\n    name?: string;\n  } = {}\n): Promise<void> {\n  try {\n    // Determine preset name\n    let presetName = options.name;\n    let sourceDir: string | undefined;\n    let isReconfigure = false; // Whether to reconfigure installed preset\n\n    // Determine source type and get directory path\n    if (source.startsWith('http://') || source.startsWith('https://')) {\n      // URL installation not supported\n      throw new Error('URL installation is not supported. Please download the preset directory and install from local path.');\n    } else if (source.includes('/') || source.includes('\\\\')) {\n      // Directory path\n      if (!presetName) {\n        presetName = path.basename(source);\n      }\n      // Verify directory exists\n      try {\n        const stats = await fs.stat(source);\n        if (!stats.isDirectory()) {\n          throw new Error(`Source is not a directory: ${source}`);\n        }\n      } catch {\n        throw new Error(`Preset directory not found: ${source}`);\n      }\n      sourceDir = source;\n\n      // Check if preset with this name already exists BEFORE installing\n      if (await isPresetInstalled(presetName)) {\n        throw new Error(`Preset '${presetName}' is already installed. To reconfigure, use: ccr preset install ${presetName}\\nTo delete and reinstall, use: ccr preset delete ${presetName}`);\n      }\n    } else {\n      // Preset name (without path)\n      presetName = source;\n\n      // Check if already installed (directory exists)\n      if (await isPresetInstalled(source)) {\n        // Already installed, reconfigure\n        isReconfigure = true;\n      } else {\n        // Not found, error\n        throw new Error(`Preset '${source}' not found. Please provide a valid preset directory path.`);\n      }\n    }\n\n    if (isReconfigure) {\n      // Reconfigure installed preset\n      console.log(`${BOLDCYAN}Reconfiguring preset:${RESET} ${presetName}\\n`);\n\n      const presetDir = getPresetDir(presetName);\n      const manifest = await readManifestFromDir(presetDir);\n      const preset = manifestToPresetFile(manifest);\n\n      // Apply preset (will ask for sensitive info)\n      await applyPresetCli(presetName, preset);\n    } else {\n      // New installation: read from source directory\n      if (!sourceDir) {\n        throw new Error('Source directory is required for installation');\n      }\n\n      console.log(`${BOLDCYAN}Reading preset from:${RESET} ${sourceDir}`);\n      console.log(`${GREEN}✓${RESET} Read successfully\\n`);\n\n      // Read manifest from source directory\n      const manifest = await readManifestFromDir(sourceDir);\n      const preset = manifestToPresetFile(manifest);\n\n      // Apply preset (ask user info, etc.)\n      await applyPresetCli(presetName, preset);\n    }\n\n  } catch (error: any) {\n    console.error(`\\n${YELLOW}Failed to install preset:${RESET} ${error.message}`);\n    process.exit(1);\n  }\n}\n"
  },
  {
    "path": "packages/cli/src/utils/processCheck.ts",
    "content": "import { existsSync, readFileSync, writeFileSync } from 'fs';\nimport { PID_FILE, REFERENCE_COUNT_FILE } from '@CCR/shared';\nimport { readConfigFile } from '.';\nimport find from 'find-process';\nimport { execSync } from 'child_process'; // 引入 execSync 来执行命令行\n\nexport async function isProcessRunning(pid: number): Promise<boolean> {\n    try {\n        const processes = await find('pid', pid);\n        return processes.length > 0;\n    } catch (error) {\n        return false;\n    }\n}\n\nexport function incrementReferenceCount() {\n    let count = 0;\n    if (existsSync(REFERENCE_COUNT_FILE)) {\n        count = parseInt(readFileSync(REFERENCE_COUNT_FILE, 'utf-8')) || 0;\n    }\n    count++;\n    writeFileSync(REFERENCE_COUNT_FILE, count.toString());\n}\n\nexport function decrementReferenceCount() {\n    let count = 0;\n    if (existsSync(REFERENCE_COUNT_FILE)) {\n        count = parseInt(readFileSync(REFERENCE_COUNT_FILE, 'utf-8')) || 0;\n    }\n    count = Math.max(0, count - 1);\n    writeFileSync(REFERENCE_COUNT_FILE, count.toString());\n}\n\nexport function getReferenceCount(): number {\n    if (!existsSync(REFERENCE_COUNT_FILE)) {\n        return 0;\n    }\n    return parseInt(readFileSync(REFERENCE_COUNT_FILE, 'utf-8')) || 0;\n}\n\nexport function isServiceRunning(): boolean {\n    if (!existsSync(PID_FILE)) {\n        return false;\n    }\n\n    let pid: number;\n    try {\n        const pidStr = readFileSync(PID_FILE, 'utf-8');\n        pid = parseInt(pidStr, 10);\n        if (isNaN(pid)) {\n            // PID 文件内容无效\n            cleanupPidFile();\n            return false;\n        }\n    } catch (e) {\n        // 读取文件失败\n        return false;\n    }\n\n    try {\n        if (process.platform === 'win32') {\n            // --- Windows 平台逻辑 ---\n            // 使用 tasklist 命令并通过 PID 过滤器查找进程\n            // stdio: 'pipe' 压制命令的输出，防止其显示在控制台\n            const command = `tasklist /FI \"PID eq ${pid}\"`;\n            const output = execSync(command, { stdio: 'pipe' }).toString();\n\n            // 如果输出中包含了 PID，说明进程存在\n            // tasklist 找不到进程时会返回 \"INFO: No tasks are running...\"\n            // 所以一个简单的包含检查就足够了\n            if (output.includes(pid.toString())) {\n                return true;\n            } else {\n                // 理论上如果 tasklist 成功执行但没找到，这里不会被命中\n                // 但作为保险，我们仍然认为进程不存在\n                cleanupPidFile();\n                return false;\n            }\n\n        } else {\n            // --- Linux, macOS 等其他平台逻辑 ---\n            // 使用信号 0 来检查进程是否存在，这不会真的杀死进程\n            process.kill(pid, 0);\n            return true; // 如果没有抛出异常，说明进程存在\n        }\n    } catch (e) {\n        // 捕获到异常，说明进程不存在 (无论是 kill 还是 execSync 失败)\n        // 清理掉无效的 PID 文件\n        cleanupPidFile();\n        return false;\n    }\n}\n\nexport function savePid(pid: number) {\n    writeFileSync(PID_FILE, pid.toString());\n}\n\nexport function cleanupPidFile() {\n    if (existsSync(PID_FILE)) {\n        try {\n            const fs = require('fs');\n            fs.unlinkSync(PID_FILE);\n        } catch (e) {\n            // Ignore cleanup errors\n        }\n    }\n}\n\nexport function getServicePid(): number | null {\n    if (!existsSync(PID_FILE)) {\n        return null;\n    }\n\n    try {\n        const pid = parseInt(readFileSync(PID_FILE, 'utf-8'));\n        return isNaN(pid) ? null : pid;\n    } catch (e) {\n        return null;\n    }\n}\n\nexport async function getServiceInfo() {\n    const pid = getServicePid();\n    const running = await isServiceRunning();\n    const config = await readConfigFile();\n    const port = config.PORT || 3456;\n\n    return {\n        running,\n        pid,\n        port,\n        endpoint: `http://127.0.0.1:${port}`,\n        pidFile: PID_FILE,\n        referenceCount: getReferenceCount()\n    };\n}\n\nexport async function closeService() {\n    // Check reference count\n    const referenceCount = getReferenceCount();\n\n    // Only stop the service if reference count is 0\n    if (referenceCount === 0) {\n        const pid = getServicePid();\n        if (pid && await isServiceRunning()) {\n            try {\n                // Kill the service process\n                process.kill(pid, 'SIGTERM');\n            } catch (e) {\n                // Ignore kill errors\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "packages/cli/src/utils/prompt/schema-input.ts",
    "content": "/**\n * Dynamic configuration CLI interaction handler\n * Handles user interactions for various input types\n */\n\nimport {\n  RequiredInput,\n  InputType,\n  UserInputValues,\n  PresetConfigSection,\n  shouldShowField,\n  resolveOptions,\n  validateInput,\n  getDefaultValue,\n  sortFieldsByDependencies,\n  getAffectedFields,\n} from '@CCR/shared';\nimport input from '@inquirer/input';\nimport confirm from '@inquirer/confirm';\nimport select from '@inquirer/select';\nimport password from '@inquirer/password';\nimport checkbox from '@inquirer/checkbox';\nimport editor from '@inquirer/editor';\n\n// ANSI color codes\nexport const COLORS = {\n  RESET: \"\\x1B[0m\",\n  GREEN: \"\\x1B[32m\",\n  YELLOW: \"\\x1B[33m\",\n  BOLDYELLOW: \"\\x1B[1m\\x1B[33m\",\n  BOLDCYAN: \"\\x1B[1m\\x1B[36m\",\n  DIM: \"\\x1B[2m\",\n  BOLDGREEN: \"\\x1B[1m\\x1B[32m\",\n};\n\n/**\n * Collect user input (supports dynamic configuration)\n */\nexport async function collectUserInputs(\n  schema: RequiredInput[],\n  presetConfig: PresetConfigSection,\n  existingValues?: UserInputValues\n): Promise<UserInputValues> {\n  // Sort by dependencies\n  const sortedFields = sortFieldsByDependencies(schema);\n\n  // Initialize values\n  const values: UserInputValues = { ...existingValues };\n\n  // Collect all inputs\n  for (const field of sortedFields) {\n    // Check if this field should be displayed\n    if (!shouldShowField(field, values)) {\n      // Skip and clear the field value (if it existed before)\n      delete values[field.id];\n      continue;\n    }\n\n    // Skip if value already exists and not initial collection\n    if (existingValues && field.id in existingValues) {\n      continue;\n    }\n\n    // Get input value\n    const value = await promptField(field, presetConfig, values);\n\n    // Validate\n    const validation = validateInput(field, value);\n    if (!validation.valid) {\n      console.error(`${COLORS.YELLOW}Error:${COLORS.RESET} ${validation.error}`);\n      // Throw error for required fields\n      if (field.required !== false) {\n        throw new Error(validation.error);\n      }\n    }\n\n    values[field.id] = value;\n    console.log('');\n  }\n\n  return values;\n}\n\n/**\n * Recollect affected fields (when a field value changes)\n */\nexport async function recollectAffectedFields(\n  changedFieldId: string,\n  schema: RequiredInput[],\n  presetConfig: PresetConfigSection,\n  currentValues: UserInputValues\n): Promise<UserInputValues> {\n  const affectedFields = getAffectedFields(changedFieldId, schema);\n  const sortedFields = sortFieldsByDependencies(schema);\n\n  const values = { ...currentValues };\n\n  // Recollect input for affected fields\n  for (const fieldId of affectedFields) {\n    const field = sortedFields.find(f => f.id === fieldId);\n    if (!field) {\n      continue;\n    }\n\n    // Check if should be displayed\n    if (!shouldShowField(field, values)) {\n      delete values[field.id];\n      continue;\n    }\n\n    // Recollect input\n    const value = await promptField(field, presetConfig, values);\n    values[field.id] = value;\n\n    // Cascade update: if this field change affects other fields\n    const newAffected = getAffectedFields(field.id, schema);\n    for (const newAffectedId of newAffected) {\n      if (!affectedFields.has(newAffectedId)) {\n        affectedFields.add(newAffectedId);\n      }\n    }\n  }\n\n  return values;\n}\n\n/**\n * Prompt for a single field\n */\nasync function promptField(\n  field: RequiredInput,\n  presetConfig: PresetConfigSection,\n  currentValues: UserInputValues\n): Promise<any> {\n  const label = field.label || field.id;\n  const message = field.prompt || `${label}:`;\n\n  switch (field.type) {\n    case InputType.PASSWORD:\n      return await password({\n        message,\n        mask: '*',\n      });\n\n    case InputType.INPUT:\n      return await input({\n        message,\n        default: field.defaultValue,\n      });\n\n    case InputType.NUMBER:\n      const numStr = await input({\n        message,\n        default: String(field.defaultValue ?? 0),\n      });\n      return Number(numStr);\n\n    case InputType.CONFIRM:\n      return await confirm({\n        message,\n        default: field.defaultValue ?? false,\n      });\n\n    case InputType.SELECT: {\n      const options = resolveOptions(field, presetConfig, currentValues);\n      if (options.length === 0) {\n        console.warn(`${COLORS.YELLOW}Warning:${COLORS.RESET} No options available for ${label}`);\n        return field.defaultValue;\n      }\n\n      return await select({\n        message,\n        choices: options.map(opt => ({\n          name: opt.label,\n          value: opt.value,\n          description: opt.description,\n          disabled: opt.disabled,\n        })),\n        default: field.defaultValue,\n      });\n    }\n\n    case InputType.MULTISELECT: {\n      const options = resolveOptions(field, presetConfig, currentValues);\n      if (options.length === 0) {\n        console.warn(`${COLORS.YELLOW}Warning:${COLORS.RESET} No options available for ${label}`);\n        return field.defaultValue ?? [];\n      }\n\n      // @inquirer/prompts doesn't have multi-select, use checkbox\n      return await checkbox({\n        message,\n        choices: options.map(opt => ({\n          name: opt.label,\n          value: opt.value,\n          checked: Array.isArray(field.defaultValue) && field.defaultValue.includes(opt.value),\n        })),\n      });\n    }\n\n    case InputType.EDITOR: {\n      return await editor({\n        message,\n        default: field.defaultValue,\n      });\n    }\n\n    default:\n      // Use input by default\n      return await input({\n        message,\n        default: field.defaultValue,\n      });\n  }\n}\n\n/**\n * Collect sensitive information (legacy compatible)\n */\nexport async function collectSensitiveInputs(\n  schema: RequiredInput[],\n  presetConfig: PresetConfigSection,\n  existingValues?: UserInputValues\n): Promise<UserInputValues> {\n  console.log(`\\n${COLORS.BOLDYELLOW}This preset requires additional information:${COLORS.RESET}\\n`);\n\n  const values = await collectUserInputs(schema, presetConfig, existingValues);\n\n  // Display summary\n  console.log(`${COLORS.GREEN}✓${COLORS.RESET} All required information collected\\n`);\n\n  return values;\n}\n"
  },
  {
    "path": "packages/cli/src/utils/status.ts",
    "content": "import { getServiceInfo } from './processCheck';\n\nexport async function showStatus() {\n    const info = await getServiceInfo();\n    \n    console.log('\\n📊 Claude Code Router Status');\n    console.log('═'.repeat(40));\n    \n    if (info.running) {\n        console.log('✅ Status: Running');\n        console.log(`🆔 Process ID: ${info.pid}`);\n        console.log(`🌐 Port: ${info.port}`);\n        console.log(`📡 API Endpoint: ${info.endpoint}`);\n        console.log(`📄 PID File: ${info.pidFile}`);\n        console.log('');\n        console.log('🚀 Ready to use! Run the following commands:');\n        console.log('   ccr code    # Start coding with Claude');\n        console.log('   ccr stop   # Stop the service');\n    } else {\n        console.log('❌ Status: Not Running');\n        console.log('');\n        console.log('💡 To start the service:');\n        console.log('   ccr start');\n    }\n    \n    console.log('');\n}\n"
  },
  {
    "path": "packages/cli/src/utils/statusline.ts",
    "content": "import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { execSync } from \"child_process\";\nimport { tmpdir } from \"node:os\";\nimport { CONFIG_FILE, HOME_DIR, readPresetFile, getPresetDir, loadConfigFromManifest } from \"@CCR/shared\";\nimport JSON5 from \"json5\";\n\nexport interface StatusLineModuleConfig {\n    type: string;\n    icon?: string;\n    text: string;\n    color?: string;\n    background?: string;\n    scriptPath?: string;\n    options?: Record<string, any>;\n}\n\nexport interface StatusLineThemeConfig {\n    modules: StatusLineModuleConfig[];\n}\n\nexport interface StatusLineInput {\n    hook_event_name: string;\n    session_id: string;\n    transcript_path: string;\n    cwd: string;\n    model: {\n        id: string;\n        display_name: string;\n    };\n    workspace: {\n        current_dir: string;\n        project_dir: string;\n    };\n    version?: string;\n    output_style?: {\n        name: string;\n    };\n    cost?: {\n        total_cost_usd: number;\n        total_duration_ms: number;\n        total_api_duration_ms: number;\n        total_lines_added: number;\n        total_lines_removed: number;\n    };\n    context_window?: {\n        total_input_tokens: number;\n        total_output_tokens: number;\n        context_window_size: number;\n        current_usage: {\n            input_tokens: number;\n            output_tokens: number;\n            cache_creation_input_tokens: number;\n            cache_read_input_tokens: number;\n        } | null;\n    };\n}\n\nexport interface AssistantMessage {\n    type: \"assistant\";\n    message: {\n        model: string;\n        usage: {\n            input_tokens: number;\n            output_tokens: number;\n        };\n    };\n}\n\n// ANSI Color codes\nconst COLORS: Record<string, string> = {\n    reset: \"\\x1b[0m\",\n    bold: \"\\x1b[1m\",\n    dim: \"\\x1b[2m\",\n    // Standard colors\n    black: \"\\x1b[30m\",\n    red: \"\\x1b[31m\",\n    green: \"\\x1b[32m\",\n    yellow: \"\\x1b[33m\",\n    blue: \"\\x1b[34m\",\n    magenta: \"\\x1b[35m\",\n    cyan: \"\\x1b[36m\",\n    white: \"\\x1b[37m\",\n    // Bright colors\n    bright_black: \"\\x1b[90m\",\n    bright_red: \"\\x1b[91m\",\n    bright_green: \"\\x1b[92m\",\n    bright_yellow: \"\\x1b[93m\",\n    bright_blue: \"\\x1b[94m\",\n    bright_magenta: \"\\x1b[95m\",\n    bright_cyan: \"\\x1b[96m\",\n    bright_white: \"\\x1b[97m\",\n    // Background colors\n    bg_black: \"\\x1b[40m\",\n    bg_red: \"\\x1b[41m\",\n    bg_green: \"\\x1b[42m\",\n    bg_yellow: \"\\x1b[43m\",\n    bg_blue: \"\\x1b[44m\",\n    bg_magenta: \"\\x1b[45m\",\n    bg_cyan: \"\\x1b[46m\",\n    bg_white: \"\\x1b[47m\",\n    // Bright background colors\n    bg_bright_black: \"\\x1b[100m\",\n    bg_bright_red: \"\\x1b[101m\",\n    bg_bright_green: \"\\x1b[102m\",\n    bg_bright_yellow: \"\\x1b[103m\",\n    bg_bright_blue: \"\\x1b[104m\",\n    bg_bright_magenta: \"\\x1b[105m\",\n    bg_bright_cyan: \"\\x1b[106m\",\n    bg_bright_white: \"\\x1b[107m\",\n};\n\n// Use TrueColor (24-bit color) to support hexadecimal colors\nconst TRUE_COLOR_PREFIX = \"\\x1b[38;2;\";\nconst TRUE_COLOR_BG_PREFIX = \"\\x1b[48;2;\";\n\n// Convert hexadecimal color to RGB format\nfunction hexToRgb(hex: string): { r: number; g: number; b: number } | null {\n    // Remove # and spaces\n    hex = hex.replace(/^#/, '').trim();\n\n    // Handle shorthand form (#RGB -> #RRGGBB)\n    if (hex.length === 3) {\n        hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];\n    }\n\n    if (hex.length !== 6) {\n        return null;\n    }\n\n    const r = parseInt(hex.substring(0, 2), 16);\n    const g = parseInt(hex.substring(2, 4), 16);\n    const b = parseInt(hex.substring(4, 6), 16);\n\n    // Validate RGB values\n    if (isNaN(r) || isNaN(g) || isNaN(b) || r < 0 || r > 255 || g < 0 || g > 255 || b < 0 || b > 255) {\n        return null;\n    }\n\n    return { r, g, b };\n}\n\n// Get color code\nfunction getColorCode(colorName: string): string {\n    // Check if it's a hexadecimal color\n    if (colorName.startsWith('#') || /^[0-9a-fA-F]{6}$/.test(colorName) || /^[0-9a-fA-F]{3}$/.test(colorName)) {\n        const rgb = hexToRgb(colorName);\n        if (rgb) {\n            return `${TRUE_COLOR_PREFIX}${rgb.r};${rgb.g};${rgb.b}m`;\n        }\n    }\n\n    // Default to empty string\n    return \"\";\n}\n\n\n// Variable replacement function, supports {{var}} format variable replacement\nfunction replaceVariables(text: string, variables: Record<string, string>): string {\n    return text.replace(/\\{\\{(\\w+)\\}\\}/g, (_match, varName) => {\n        return variables[varName] || \"\";\n    });\n}\n\n// Execute script and get output\nasync function executeScript(scriptPath: string, variables: Record<string, string>, options?: Record<string, any>): Promise<string> {\n    try {\n        // Check if file exists\n        await fs.access(scriptPath);\n\n        // Use require to dynamically load script module\n        const scriptModule = require(scriptPath);\n\n        // If export is a function, call it with variables\n        if (typeof scriptModule === 'function') {\n            const result = scriptModule(variables, options);\n            // If returns a Promise, wait for it to complete\n            if (result instanceof Promise) {\n                return await result;\n            }\n            return result;\n        }\n\n        // If export is a default function, call it\n        if (scriptModule.default && typeof scriptModule.default === 'function') {\n            const result = scriptModule.default(variables);\n            // If returns a Promise, wait for it to complete\n            if (result instanceof Promise) {\n                return await result;\n            }\n            return result;\n        }\n\n        // If export is a string, return directly\n        if (typeof scriptModule === 'string') {\n            return scriptModule;\n        }\n\n        // If export is a default string, return it\n        if (scriptModule.default && typeof scriptModule.default === 'string') {\n            return scriptModule.default;\n        }\n\n        // Default to empty string\n        return \"\";\n    } catch (error) {\n        console.error(`Error executing script ${scriptPath}:`, error);\n        return \"\";\n    }\n}\n\n// Default theme configuration - using Nerd Fonts icons and beautiful color scheme\nconst DEFAULT_THEME: StatusLineThemeConfig = {\n    modules: [\n        {\n            type: \"workDir\",\n            icon: \"󰉋\", // nf-md-folder_outline\n            text: \"{{workDirName}}\",\n            color: \"bright_blue\"\n        },\n        {\n            type: \"gitBranch\",\n            icon: \"\", // nf-dev-git_branch\n            text: \"{{gitBranch}}\",\n            color: \"bright_magenta\"\n        },\n        {\n            type: \"model\",\n            icon: \"󰚩\", // nf-md-robot_outline\n            text: \"{{model}}\",\n            color: \"bright_cyan\"\n        },\n        {\n            type: \"usage\",\n            icon: \"↑\", // Up arrow\n            text: \"{{inputTokens}}\",\n            color: \"bright_green\"\n        },\n        {\n            type: \"usage\",\n            icon: \"↓\", // Down arrow\n            text: \"{{outputTokens}}\",\n            color: \"bright_yellow\"\n        }\n    ]\n};\n\n// Powerline style theme configuration\nconst POWERLINE_THEME: StatusLineThemeConfig = {\n    modules: [\n        {\n            type: \"workDir\",\n            icon: \"󰉋\", // nf-md-folder_outline\n            text: \"{{workDirName}}\",\n            color: \"white\",\n            background: \"bg_bright_blue\"\n        },\n        {\n            type: \"gitBranch\",\n            icon: \"\", // nf-dev-git_branch\n            text: \"{{gitBranch}}\",\n            color: \"white\",\n            background: \"bg_bright_magenta\"\n        },\n        {\n            type: \"model\",\n            icon: \"󰚩\", // nf-md-robot_outline\n            text: \"{{model}}\",\n            color: \"white\",\n            background: \"bg_bright_cyan\"\n        },\n        {\n            type: \"usage\",\n            icon: \"↑\", // Up arrow\n            text: \"{{inputTokens}}\",\n            color: \"white\",\n            background: \"bg_bright_green\"\n        },\n        {\n            type: \"usage\",\n            icon: \"↓\", // Down arrow\n            text: \"{{outputTokens}}\",\n            color: \"white\",\n            background: \"bg_bright_yellow\"\n        }\n    ]\n};\n\n// Simple text theme configuration - fallback for when icons cannot be displayed\nconst SIMPLE_THEME: StatusLineThemeConfig = {\n    modules: [\n        {\n            type: \"workDir\",\n            icon: \"\",\n            text: \"{{workDirName}}\",\n            color: \"bright_blue\"\n        },\n        {\n            type: \"gitBranch\",\n            icon: \"\",\n            text: \"{{gitBranch}}\",\n            color: \"bright_magenta\"\n        },\n        {\n            type: \"model\",\n            icon: \"\",\n            text: \"{{model}}\",\n            color: \"bright_cyan\"\n        },\n        {\n            type: \"usage\",\n            icon: \"↑\",\n            text: \"{{inputTokens}}\",\n            color: \"bright_green\"\n        },\n        {\n            type: \"usage\",\n            icon: \"↓\",\n            text: \"{{outputTokens}}\",\n            color: \"bright_yellow\"\n        }\n    ]\n};\n\n// Full theme configuration - showcasing all available modules\nconst FULL_THEME: StatusLineThemeConfig = {\n    modules: [\n        {\n            type: \"workDir\",\n            icon: \"󰉋\",\n            text: \"{{workDirName}}\",\n            color: \"bright_blue\"\n        },\n        {\n            type: \"gitBranch\",\n            icon: \"\",\n            text: \"{{gitBranch}}\",\n            color: \"bright_magenta\"\n        },\n        {\n            type: \"model\",\n            icon: \"󰚩\",\n            text: \"{{model}}\",\n            color: \"bright_cyan\"\n        },\n        {\n            type: \"context\",\n            icon: \"🪟\",\n            text: \"{{contextPercent}}% / {{contextWindowSize}}\",\n            color: \"bright_green\"\n        },\n        {\n            type: \"speed\",\n            icon: \"⚡\",\n            text: \"{{tokenSpeed}} t/s {{isStreaming}}\",\n            color: \"bright_yellow\"\n        },\n        {\n            type: \"cost\",\n            icon: \"💰\",\n            text: \"{{cost}}\",\n            color: \"bright_magenta\"\n        },\n        {\n            type: \"duration\",\n            icon: \"⏱️\",\n            text: \"{{duration}}\",\n            color: \"bright_white\"\n        },\n        {\n            type: \"lines\",\n            icon: \"📝\",\n            text: \"+{{linesAdded}}/-{{linesRemoved}}\",\n            color: \"bright_cyan\"\n        }\n    ]\n};\n\n// Format usage information, use k unit if greater than 1000\nfunction formatUsage(input_tokens: number, output_tokens: number): string {\n    if (input_tokens > 1000 || output_tokens > 1000) {\n        const inputFormatted = input_tokens > 1000 ? `${(input_tokens / 1000).toFixed(1)}k` : `${input_tokens}`;\n        const outputFormatted = output_tokens > 1000 ? `${(output_tokens / 1000).toFixed(1)}k` : `${output_tokens}`;\n        return `${inputFormatted} ${outputFormatted}`;\n    }\n    return `${input_tokens} ${output_tokens}`;\n}\n\n// Calculate context window usage percentage\nfunction calculateContextPercent(context_window: StatusLineInput['context_window']): number {\n    if (!context_window || !context_window.current_usage) {\n        return 0;\n    }\n    const { current_usage, context_window_size } = context_window;\n    const currentTokens = current_usage.input_tokens +\n                        current_usage.cache_creation_input_tokens +\n                        current_usage.cache_read_input_tokens;\n    return Math.round((currentTokens / context_window_size) * 100);\n}\n\n// Format cost display\nfunction formatCost(cost_usd: number): string {\n    if (cost_usd < 0.01) {\n        return `${(cost_usd * 100).toFixed(2)}¢`;\n    }\n    return `$${cost_usd.toFixed(2)}`;\n}\n\n// Format duration\nfunction formatDuration(ms: number): string {\n    if (Number.isNaN(ms)) {\n        return ''\n    }\n    if (ms < 1000) {\n        return `${ms}ms`;\n    } else if (ms < 60000) {\n        return `${(ms / 1000).toFixed(1)}s`;\n    } else {\n        const minutes = Math.floor(ms / 60000);\n        const seconds = ((ms % 60000) / 1000).toFixed(0);\n        if (Number.isNaN(minutes) || Number.isNaN(seconds)) {\n            return ''\n        }\n        return `${minutes}m${seconds}s`;\n    }\n}\n\n// Read token-speed statistics from temp file\nasync function getTokenSpeedStats(sessionId: string): Promise<{\n    tokensPerSecond: number;\n    timeToFirstToken?: number;\n} | null> {\n    try {\n        // Use system temp directory\n        const tempDir = path.join(tmpdir(), 'claude-code-router');\n\n        // Check if temp directory exists\n        try {\n            await fs.access(tempDir);\n        } catch {\n            return null;\n        }\n\n        const statsFilePath = path.join(tempDir, `session-${sessionId}.json`);\n        try {\n            await fs.access(statsFilePath);\n        } catch {\n            return null;\n        }\n\n        // Read stats file\n        const content = await fs.readFile(statsFilePath, 'utf-8');\n        const data = JSON.parse(content);\n\n        // Check if data has tokensPerSecond\n        if (data.tokensPerSecond !== undefined && data.tokensPerSecond > 0) {\n            // Check if timestamp is within last 3 seconds\n            const now = Date.now();\n            const timestamp = data.timestamp || 0;\n            const ageInSeconds = (now - timestamp) / 1000;\n\n            // If data is older than 3 seconds, return 0 speed\n            if (ageInSeconds > 3) {\n                return {\n                    tokensPerSecond: 0,\n                    timeToFirstToken: data.timeToFirstToken\n                };\n            }\n\n            const result = {\n                tokensPerSecond: parseInt(data.tokensPerSecond),\n                timeToFirstToken: data.timeToFirstToken\n            };\n            return result;\n        }\n\n        return null;\n    } catch (error) {\n        // Silently fail on error\n        return null;\n    }\n}\n\n// Read theme configuration from user home directory\nasync function getProjectThemeConfig(): Promise<{ theme: StatusLineThemeConfig | null, style: string }> {\n    try {\n        // Only use fixed configuration file in home directory\n        const configPath = CONFIG_FILE;\n\n        // Check if configuration file exists\n        try {\n            await fs.access(configPath);\n        } catch {\n            return { theme: null, style: 'default' };\n        }\n\n        const configContent = await fs.readFile(configPath, \"utf-8\");\n        const config = JSON5.parse(configContent);\n\n        // Check if there's StatusLine configuration\n        if (config.StatusLine) {\n            // Get current style, default to 'default'\n            const currentStyle = config.StatusLine.currentStyle || 'default';\n\n            // Check if there's configuration for the corresponding style\n            if (config.StatusLine[currentStyle] && config.StatusLine[currentStyle].modules) {\n                return { theme: config.StatusLine[currentStyle], style: currentStyle };\n            }\n        }\n    } catch (error) {\n        // Return null if reading fails\n        // console.error(\"Failed to read theme config:\", error);\n    }\n\n    return { theme: null, style: 'default' };\n}\n\n// Read theme configuration from preset\nasync function getPresetThemeConfig(presetName: string): Promise<{ theme: StatusLineThemeConfig | null, style: string }> {\n    try {\n        // Read preset manifest\n        const manifest = await readPresetFile(presetName);\n        if (!manifest) {\n            return { theme: null, style: 'default' };\n        }\n\n        // Load preset configuration (applies userValues if present)\n        const presetDir = getPresetDir(presetName);\n        const config = loadConfigFromManifest(manifest, presetDir);\n\n        // Check if there's StatusLine configuration in preset\n        if (config.StatusLine) {\n            // Get current style, default to 'default'\n            const currentStyle = config.StatusLine.currentStyle || 'default';\n\n            // Check if there's configuration for the corresponding style\n            if (config.StatusLine[currentStyle] && config.StatusLine[currentStyle].modules) {\n                return { theme: config.StatusLine[currentStyle], style: currentStyle };\n            }\n        }\n    } catch (error) {\n        // Return null if reading fails\n        // console.error(\"Failed to read preset theme config:\", error);\n    }\n\n    return { theme: null, style: 'default' };\n}\n\n// Check if simple theme should be used (fallback scheme)\n// When environment variable USE_SIMPLE_ICONS is set, or when a terminal that might not support Nerd Fonts is detected\nfunction shouldUseSimpleTheme(): boolean {\n    // Check environment variable\n    if (process.env.USE_SIMPLE_ICONS === 'true') {\n        return true;\n    }\n\n    // Check terminal type (some common terminals that don't support complex icons)\n    const term = process.env.TERM || '';\n    const unsupportedTerms = ['dumb', 'unknown'];\n    if (unsupportedTerms.includes(term)) {\n        return true;\n    }\n\n    // By default, assume terminal supports Nerd Fonts\n    return false;\n}\n\n// Check if Nerd Fonts icons can be displayed correctly\n// By checking terminal font information or using heuristic methods\nfunction canDisplayNerdFonts(): boolean {\n    // If environment variable explicitly specifies simple icons, Nerd Fonts cannot be displayed\n    if (process.env.USE_SIMPLE_ICONS === 'true') {\n        return false;\n    }\n\n    // Check some common terminal environment variables that support Nerd Fonts\n    const fontEnvVars = ['NERD_FONT', 'NERDFONT', 'FONT'];\n    for (const envVar of fontEnvVars) {\n        const value = process.env[envVar];\n        if (value && (value.includes('Nerd') || value.includes('nerd'))) {\n            return true;\n        }\n    }\n\n    // Check terminal type\n    const termProgram = process.env.TERM_PROGRAM || '';\n    const supportedTerminals = ['iTerm.app', 'vscode', 'Hyper', 'kitty', 'alacritty'];\n    if (supportedTerminals.includes(termProgram)) {\n        return true;\n    }\n\n    // Check COLORTERM environment variable\n    const colorTerm = process.env.COLORTERM || '';\n    if (colorTerm.includes('truecolor') || colorTerm.includes('24bit')) {\n        return true;\n    }\n\n    // By default, assume Nerd Fonts can be displayed (but allow users to override via environment variables)\n    return process.env.USE_SIMPLE_ICONS !== 'true';\n}\n\nexport async function parseStatusLineData(input: StatusLineInput, presetName?: string): Promise<string> {\n    try {\n        // Check if simple theme should be used\n        const useSimpleTheme = shouldUseSimpleTheme();\n\n        // Check if Nerd Fonts icons can be displayed\n        const canDisplayNerd = canDisplayNerdFonts();\n\n        // Determine which theme to use: use simple theme if user forces it or Nerd Fonts cannot be displayed\n        const effectiveTheme = useSimpleTheme || !canDisplayNerd ? SIMPLE_THEME : DEFAULT_THEME;\n\n        // Get theme configuration: preset config > home directory config > default theme\n        let projectTheme: StatusLineThemeConfig | null = null;\n        let currentStyle = 'default';\n\n        if (presetName) {\n            // Try to get theme configuration from preset first\n            const presetConfig = await getPresetThemeConfig(presetName);\n            projectTheme = presetConfig.theme;\n            currentStyle = presetConfig.style;\n        }\n\n        // If preset theme not found or no preset specified, try home directory config\n        if (!projectTheme) {\n            const homeConfig = await getProjectThemeConfig();\n            projectTheme = homeConfig.theme;\n            currentStyle = homeConfig.style;\n        }\n\n        const theme = projectTheme || effectiveTheme;\n\n        // Get current working directory and Git branch\n        const workDir = input.workspace.current_dir;\n        let gitBranch = \"\";\n\n        try {\n            // Try to get Git branch name\n            gitBranch = execSync(\"git branch --show-current\", {\n                cwd: workDir,\n                stdio: [\"pipe\", \"pipe\", \"ignore\"],\n            })\n                .toString()\n                .trim();\n        } catch (error) {\n            // If not a Git repository or retrieval fails, ignore error\n        }\n\n        // Read last assistant message from transcript_path file\n        const transcriptContent = await fs.readFile(input.transcript_path, \"utf-8\");\n        const lines = transcriptContent.trim().split(\"\\n\");\n\n        // Traverse in reverse to find last assistant message\n        let model = \"\";\n        let inputTokens = 0;\n        let outputTokens = 0;\n\n        for (let i = lines.length - 1; i >= 0; i--) {\n            try {\n                const message: AssistantMessage = JSON.parse(lines[i]);\n                if (message.type === \"assistant\" && message.message.model) {\n                    model = message.message.model;\n\n                    if (message.message.usage) {\n                        inputTokens = message.message.usage.input_tokens;\n                        outputTokens = message.message.usage.output_tokens;\n                    }\n                    break;\n                }\n            } catch (parseError) {\n                // Ignore parse errors, continue searching\n                continue;\n            }\n        }\n\n        // If model name not retrieved from transcript, try to get from configuration file\n        if (!model) {\n            try {\n                // Get project configuration file path\n                const projectConfigPath = path.join(workDir, \".claude-code-router\", \"config.json\");\n                let configPath = projectConfigPath;\n\n                // Check if project configuration file exists, if not use user home directory configuration file\n                try {\n                    await fs.access(projectConfigPath);\n                } catch {\n                    configPath = CONFIG_FILE;\n                }\n\n                // Read configuration file\n                const configContent = await fs.readFile(configPath, \"utf-8\");\n                const config = JSON5.parse(configContent);\n\n                // Get model name from Router field's default content\n                if (config.Router && config.Router.default) {\n                    const [, defaultModel] = config.Router.default.split(\",\");\n                    if (defaultModel) {\n                        model = defaultModel.trim();\n                    }\n                }\n            } catch (configError) {\n                // If configuration file reading fails, ignore error\n            }\n        }\n\n        // If still unable to get model name, use display_name from input JSON data's model field\n        if (!model) {\n            model = input.model.display_name;\n        }\n\n        // Get working directory name\n        const workDirName = workDir.split(\"/\").pop() || \"\";\n\n        // Format usage information\n        const usage = formatUsage(inputTokens, outputTokens);\n        const [formattedInputTokens, formattedOutputTokens] = usage.split(\" \");\n\n        // Get token-speed statistics\n        const tokenSpeedData = await getTokenSpeedStats(input.session_id);\n        const formattedTokenSpeed = tokenSpeedData && tokenSpeedData.tokensPerSecond > 0\n            ? tokenSpeedData.tokensPerSecond.toString()\n            : '';\n\n        // Check if streaming (has active token speed)\n        const isStreaming = tokenSpeedData !== null && tokenSpeedData.tokensPerSecond > 0;\n\n        const streamingIndicator = isStreaming ? '[Streaming]' : ''\n\n        // Format time to first token\n        let formattedTimeToFirstToken = '';\n        if (tokenSpeedData?.timeToFirstToken !== undefined) {\n            formattedTimeToFirstToken = formatDuration(tokenSpeedData.timeToFirstToken);\n        }\n\n        // Process context window data\n        const contextPercent = input.context_window ? calculateContextPercent(input.context_window) : 0;\n        const totalInputTokens = input.context_window?.total_input_tokens || 0;\n        const totalOutputTokens = input.context_window?.total_output_tokens || 0;\n        const contextWindowSize = input.context_window?.context_window_size || 0;\n\n        // Process cost data\n        const totalCost = input.cost?.total_cost_usd || 0;\n        const formattedCost = totalCost > 0 ? formatCost(totalCost) : '';\n        const totalDuration = input.cost?.total_duration_ms || 0;\n        const formattedDuration = totalDuration > 0 ? formatDuration(totalDuration) : '';\n        const linesAdded = input.cost?.total_lines_added || 0;\n        const linesRemoved = input.cost?.total_lines_removed || 0;\n\n        // Define variable replacement mapping\n        const variables: Record<string, string> = {\n            workDirName,\n            gitBranch,\n            model,\n            inputTokens: formattedInputTokens,\n            outputTokens: formattedOutputTokens,\n            tokenSpeed: formattedTokenSpeed || '0',\n            isStreaming: isStreaming ? 'streaming' : '',\n            timeToFirstToken: formattedTimeToFirstToken,\n            contextPercent: contextPercent.toString(),\n            streamingIndicator,\n            contextWindowSize: contextWindowSize > 1000 ? `${(contextWindowSize / 1000).toFixed(0)}k` : contextWindowSize.toString(),\n            totalInputTokens: totalInputTokens > 1000 ? `${(totalInputTokens / 1000).toFixed(1)}k` : totalInputTokens.toString(),\n            totalOutputTokens: totalOutputTokens > 1000 ? `${(totalOutputTokens / 1000).toFixed(1)}k` : totalOutputTokens.toString(),\n            cost: formattedCost || '',\n            duration: formattedDuration || '',\n            linesAdded: linesAdded.toString(),\n            linesRemoved: linesRemoved.toString(),\n            netLines: (linesAdded - linesRemoved).toString(),\n            version: input.version || '',\n            sessionId: input.session_id.substring(0, 8)\n        };\n\n        // Determine the style to use\n        const isPowerline = currentStyle === 'powerline';\n\n        // Render status line based on style\n        if (isPowerline) {\n            return await renderPowerlineStyle(theme, variables);\n        } else {\n            return await renderDefaultStyle(theme, variables);\n        }\n    } catch (error) {\n        // Return empty string on error\n        return \"\";\n    }\n}\n\n// Render default style status line\nasync function renderDefaultStyle(\n    theme: StatusLineThemeConfig,\n    variables: Record<string, string>\n): Promise<string> {\n    const modules = theme.modules || DEFAULT_THEME.modules;\n    const parts: string[] = [];\n\n    // Iterate through module array, rendering each module (maximum 10)\n    for (let i = 0; i < modules.length; i++) {\n        const module = modules[i];\n\n        const color = module.color ? getColorCode(module.color) : \"\";\n        const background = module.background ? getColorCode(module.background) : \"\";\n        const icon = module.icon || \"\";\n\n        // If script type, execute script to get text\n        let text = \"\";\n        if (module.type === \"script\" && module.scriptPath) {\n            text = await executeScript(module.scriptPath, variables, module.options);\n        } else {\n            text = replaceVariables(module.text, variables);\n        }\n\n        // Build display text\n        let displayText = \"\";\n        if (icon) {\n            displayText += `${icon} `;\n        }\n        displayText += text;\n\n        // Skip module if displayText is empty or only has icon without actual text\n        if (!displayText || !text) {\n            continue;\n        }\n\n        // Build module string\n        let part = `${background}${color}`;\n        part += `${displayText}${COLORS.reset}`;\n\n        parts.push(part);\n    }\n\n    // Join all parts with spaces\n    return parts.join(\" \");\n}\n\n// Powerline symbols\nconst SEP_RIGHT = \"\\uE0B0\"; // \n\n// Color numbers (256-color table)\nconst COLOR_MAP: Record<string, number> = {\n    // Basic colors mapped to 256 colors\n    black: 0,\n    red: 1,\n    green: 2,\n    yellow: 3,\n    blue: 4,\n    magenta: 5,\n    cyan: 6,\n    white: 7,\n    bright_black: 8,\n    bright_red: 9,\n    bright_green: 10,\n    bright_yellow: 11,\n    bright_blue: 12,\n    bright_magenta: 13,\n    bright_cyan: 14,\n    bright_white: 15,\n    // Bright background color mapping\n    bg_black: 0,\n    bg_red: 1,\n    bg_green: 2,\n    bg_yellow: 3,\n    bg_blue: 4,\n    bg_magenta: 5,\n    bg_cyan: 6,\n    bg_white: 7,\n    bg_bright_black: 8,\n    bg_bright_red: 9,\n    bg_bright_green: 10,\n    bg_bright_yellow: 11,\n    bg_bright_blue: 12,\n    bg_bright_magenta: 13,\n    bg_bright_cyan: 14,\n    bg_bright_white: 15,\n    // Custom color mapping\n    bg_bright_orange: 202,\n    bg_bright_purple: 129,\n};\n\n// Get TrueColor RGB value\nfunction getTrueColorRgb(colorName: string): { r: number; g: number; b: number } | null {\n    // If predefined color, return corresponding RGB\n    if (COLOR_MAP[colorName] !== undefined) {\n        const color256 = COLOR_MAP[colorName];\n        return color256ToRgb(color256);\n    }\n\n    // Handle hexadecimal color\n    if (colorName.startsWith('#') || /^[0-9a-fA-F]{6}$/.test(colorName) || /^[0-9a-fA-F]{3}$/.test(colorName)) {\n        return hexToRgb(colorName);\n    }\n\n    // Handle background color hexadecimal\n    if (colorName.startsWith('bg_#')) {\n        return hexToRgb(colorName.substring(3));\n    }\n\n    return null;\n}\n\n// Convert 256-color table index to RGB value\nfunction color256ToRgb(index: number): { r: number; g: number; b: number } | null {\n    if (index < 0 || index > 255) return null;\n\n    // ANSI 256-color table conversion\n    if (index < 16) {\n        // Basic colors\n        const basicColors = [\n            [0, 0, 0], [128, 0, 0], [0, 128, 0], [128, 128, 0],\n            [0, 0, 128], [128, 0, 128], [0, 128, 128], [192, 192, 192],\n            [128, 128, 128], [255, 0, 0], [0, 255, 0], [255, 255, 0],\n            [0, 0, 255], [255, 0, 255], [0, 255, 255], [255, 255, 255]\n        ];\n        return { r: basicColors[index][0], g: basicColors[index][1], b: basicColors[index][2] };\n    } else if (index < 232) {\n        // 216 colors: 6×6×6 color cube\n        const i = index - 16;\n        const r = Math.floor(i / 36);\n        const g = Math.floor((i % 36) / 6);\n        const b = i % 6;\n        const rgb = [0, 95, 135, 175, 215, 255];\n        return { r: rgb[r], g: rgb[g], b: rgb[b] };\n    } else {\n        // Grayscale colors\n        const gray = 8 + (index - 232) * 10;\n        return { r: gray, g: gray, b: gray };\n    }\n}\n\n// Generate a seamless segment: text displayed on bgN, separator transitions from bgN to nextBgN\nfunction segment(text: string, textFg: string, bgColor: string, nextBgColor: string | null): string {\n    const bgRgb = getTrueColorRgb(bgColor);\n    if (!bgRgb) {\n        // If unable to get RGB, use default blue background\n        const defaultBlueRgb = { r: 33, g: 150, b: 243 };\n        const curBg = `\\x1b[48;2;${defaultBlueRgb.r};${defaultBlueRgb.g};${defaultBlueRgb.b}m`;\n        const fgColor = `\\x1b[38;2;255;255;255m`;\n        const body = `${curBg}${fgColor} ${text} \\x1b[0m`;\n        return body;\n    }\n\n    const curBg = `\\x1b[48;2;${bgRgb.r};${bgRgb.g};${bgRgb.b}m`;\n\n    // Get foreground color RGB\n    let fgRgb = { r: 255, g: 255, b: 255 }; // Default foreground color is white\n    const textFgRgb = getTrueColorRgb(textFg);\n    if (textFgRgb) {\n        fgRgb = textFgRgb;\n    }\n\n    const fgColor = `\\x1b[38;2;${fgRgb.r};${fgRgb.g};${fgRgb.b}m`;\n    const body = `${curBg}${fgColor} ${text} \\x1b[0m`;\n\n    if (nextBgColor != null) {\n        const nextBgRgb = getTrueColorRgb(nextBgColor);\n        if (nextBgRgb) {\n            // Separator: foreground color is current segment's background color, background color is next segment's background color\n            const sepCurFg = `\\x1b[38;2;${bgRgb.r};${bgRgb.g};${bgRgb.b}m`;\n            const sepNextBg = `\\x1b[48;2;${nextBgRgb.r};${nextBgRgb.g};${nextBgRgb.b}m`;\n            const sep = `${sepCurFg}${sepNextBg}${SEP_RIGHT}\\x1b[0m`;\n            return body + sep;\n        }\n        // If no next background color, assume terminal background is black and render black arrow\n        const sepCurFg = `\\x1b[38;2;${bgRgb.r};${bgRgb.g};${bgRgb.b}m`;\n        const sepNextBg = `\\x1b[48;2;0;0;0m`; // Black background\n        const sep = `${sepCurFg}${sepNextBg}${SEP_RIGHT}\\x1b[0m`;\n        return body + sep;\n    }\n\n    return body;\n}\n\n// Render Powerline style status line\nasync function renderPowerlineStyle(\n    theme: StatusLineThemeConfig,\n    variables: Record<string, string>\n): Promise<string> {\n    const modules = theme.modules || POWERLINE_THEME.modules;\n    const segments: string[] = [];\n\n    // Iterate through module array, rendering each module (maximum 10)\n    for (let i = 0; i < Math.min(modules.length, 10); i++) {\n        const module = modules[i];\n        const color = module.color || \"white\";\n        const backgroundName = module.background || \"\";\n        const icon = module.icon || \"\";\n\n        // If script type, execute script to get text\n        let text = \"\";\n        if (module.type === \"script\" && module.scriptPath) {\n            text = await executeScript(module.scriptPath, variables);\n        } else if (module.type === \"speed\") {\n            // speed module: use tokenSpeed variable\n            text = replaceVariables(module.text, variables);\n        } else {\n            text = replaceVariables(module.text, variables);\n        }\n\n        // Build display text\n        let displayText = \"\";\n        if (icon) {\n            displayText += `${icon} `;\n        }\n        displayText += text;\n\n        // Skip module if displayText is empty or only has icon without actual text\n        if (!displayText || !text) {\n            continue;\n        }\n\n        // Get next module's background color (for separator)\n        let nextBackground: string | null = null;\n        if (i < modules.length - 1) {\n            const nextModule = modules[i + 1];\n            nextBackground = nextModule.background || null;\n        }\n\n        // Use module-defined background color, or provide default background color for Powerline style\n        const actualBackground = backgroundName || \"bg_bright_blue\";\n\n        // Generate segment, supports hexadecimal colors\n        const segmentStr = segment(displayText, color, actualBackground, nextBackground);\n        segments.push(segmentStr);\n    }\n\n    return segments.join(\"\");\n}\n"
  },
  {
    "path": "packages/cli/src/utils/update.ts",
    "content": "import { exec } from \"child_process\";\nimport { promisify } from \"util\";\nimport { join } from \"path\";\nimport { readFileSync } from \"fs\";\n\nconst execPromise = promisify(exec);\n\n/**\n * Check if new version is available\n * @param currentVersion Current version\n * @returns Object containing update information\n */\nexport async function checkForUpdates(currentVersion: string) {\n  try {\n    // Get latest version info from npm registry\n    const { stdout } = await execPromise(\"npm view @musistudio/claude-code-router version\");\n    const latestVersion = stdout.trim();\n    \n    // Compare versions\n    const hasUpdate = compareVersions(latestVersion, currentVersion) > 0;\n    \n    // If there is an update, get changelog\n    let changelog = \"\";\n    \n    return { hasUpdate, latestVersion, changelog };\n  } catch (error) {\n    console.error(\"Error checking for updates:\", error);\n    // If check fails, assume no update\n    return { hasUpdate: false, latestVersion: currentVersion, changelog: \"\" };\n  }\n}\n\n/**\n * Perform update operation\n * @returns Update result\n */\nexport async function performUpdate() {\n  try {\n    // Execute npm update command\n    const { stdout, stderr } = await execPromise(\"npm update -g @musistudio/claude-code-router\");\n    \n    if (stderr) {\n      console.error(\"Update stderr:\", stderr);\n    }\n    \n    console.log(\"Update stdout:\", stdout);\n    \n    return { \n      success: true, \n      message: \"Update completed successfully. Please restart the application to apply changes.\" \n    };\n  } catch (error) {\n    console.error(\"Error performing update:\", error);\n    return { \n      success: false, \n      message: `Failed to perform update: ${error instanceof Error ? error.message : 'Unknown error'}` \n    };\n  }\n}\n\n/**\n * Compare two version numbers\n * @param v1 Version number 1\n * @param v2 Version number 2\n * @returns 1 if v1 > v2, -1 if v1 < v2, 0 if equal\n */\nfunction compareVersions(v1: string, v2: string): number {\n  const parts1 = v1.split(\".\").map(Number);\n  const parts2 = v2.split(\".\").map(Number);\n  \n  for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {\n    const num1 = i < parts1.length ? parts1[i] : 0;\n    const num2 = i < parts2.length ? parts2[i] : 0;\n    \n    if (num1 > num2) return 1;\n    if (num1 < num2) return -1;\n  }\n  \n  return 0;\n}"
  },
  {
    "path": "packages/cli/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.base.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"./dist\",\n    \"rootDir\": \"./src\",\n    \"baseUrl\": \"./src\"\n  },\n  \"include\": [\"src/**/*.ts\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/core/.npmignore",
    "content": "src\nnode_modules\n.claude\nCLAUDE.md\nscreenshoots\n.DS_Store\n.vscode\n.idea\n.env\n.blog\ndocs\nscripts\neslint.config.cjs\n*.log\nconfig.json\ntsconfig.json\ndist\n"
  },
  {
    "path": "packages/core/package.json",
    "content": "{\n  \"name\": \"@musistudio/llms\",\n  \"version\": \"1.0.51\",\n  \"description\": \"A universal LLM API transformation server\",\n  \"main\": \"dist/cjs/server.cjs\",\n  \"module\": \"dist/esm/server.mjs\",\n  \"types\": \"./dist/index.d.ts\",\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": \"./dist/esm/server.mjs\",\n      \"require\": \"./dist/cjs/server.cjs\"\n    }\n  },\n  \"scripts\": {\n    \"tsx\": \"tsx\",\n    \"build\": \"tsx scripts/build.ts\",\n    \"build:watch\": \"tsx scripts/build.ts --watch\",\n    \"dev\": \"nodemon\",\n    \"start\": \"node dist/cjs/server.cjs\",\n    \"start:esm\": \"node dist/esm/server.mjs\",\n    \"lint\": \"eslint src --ext .ts,.tsx\"\n  },\n  \"keywords\": [\n    \"llm\",\n    \"anthropic\",\n    \"openai\",\n    \"gemini\",\n    \"transformer\",\n    \"api\"\n  ],\n  \"author\": \"musistudio\",\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"@anthropic-ai/sdk\": \"^0.54.0\",\n    \"@fastify/cors\": \"^11.0.1\",\n    \"@google/genai\": \"^1.7.0\",\n    \"@huggingface/tokenizers\": \"^0.0.6\",\n    \"dotenv\": \"^16.5.0\",\n    \"fastify\": \"^5.4.0\",\n    \"fastify-plugin\": \"^5.1.0\",\n    \"google-auth-library\": \"^10.1.0\",\n    \"json5\": \"^2.2.3\",\n    \"jsonrepair\": \"^3.13.0\",\n    \"lru-cache\": \"^11.2.2\",\n    \"openai\": \"^5.6.0\",\n    \"tiktoken\": \"^1.0.21\",\n    \"undici\": \"^7.10.0\",\n    \"uuid\": \"^11.1.0\"\n  },\n  \"devDependencies\": {\n    \"@CCR/shared\": \"workspace:*\",\n    \"@types/node\": \"^24.0.15\",\n    \"esbuild\": \"^0.25.1\",\n    \"tsx\": \"^4.20.3\",\n    \"typescript\": \"^5.8.2\"\n  }\n}\n"
  },
  {
    "path": "packages/core/scripts/build.ts",
    "content": "import * as esbuild from \"esbuild\";\nimport * as path from \"path\";\nimport * as fs from \"fs\";\nimport { fileURLToPath } from \"url\";\nimport { execSync } from \"child_process\";\nimport { pathAliasPlugin } from \"./esbuild-plugin-path-alias\";\n\nconst watch = process.argv.includes(\"--watch\");\n\n// Get the absolute path to the src directory (ES module compatible)\n// @ts-ignore\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconst baseUrl = path.resolve(__dirname, \"..\");\n\nconst baseConfig: esbuild.BuildOptions = {\n  entryPoints: [\"src/server.ts\"],\n  bundle: true,\n  minify: true,\n  sourcemap: true,\n  platform: \"node\",\n  target: \"node18\",\n  plugins: [\n    // Add path alias plugin to resolve @/ imports\n    pathAliasPlugin({\n      alias: {\n        \"@/*\": \"src/*\",\n      },\n      baseUrl,\n    }),\n  ],\n  external: [\"fastify\", \"dotenv\", \"@fastify/cors\", \"undici\", \"tiktoken\", \"@CCR/shared\", \"lru-cache\"],\n};\n\n// Generate type declarations with resolved path aliases\nfunction generateTypeDeclarations() {\n  console.log(\"Skipping type declaration generation (using manual plugins.d.ts)...\");\n  // Type declarations are manually maintained in dist/plugins.d.ts\n  // This avoids issues with @/ path aliases in auto-generated declarations\n}\n\n// Replace @/ paths with relative paths in .d.ts files\nfunction replacePathAliases(dir: string, baseDir = dir) {\n  const files = fs.readdirSync(dir);\n\n  for (const file of files) {\n    const fullPath = path.join(dir, file);\n    const stat = fs.statSync(fullPath);\n\n    if (stat.isDirectory()) {\n      replacePathAliases(fullPath, baseDir);\n    } else if (file.endsWith(\".d.ts\")) {\n      let content = fs.readFileSync(fullPath, \"utf-8\");\n\n      // Replace @/ imports with relative paths\n      content = content.replace(/from\\s+[\"']@(\\/[^\"']+)[\"']/g, (_, importPath) => {\n        const absolutePath = path.resolve(baseUrl, \"src\", importPath.slice(2));\n        const currentDir = path.dirname(fullPath);\n        const relativePath = path.relative(currentDir, absolutePath);\n        const normalizedPath = relativePath.split(path.sep).join(\"/\");\n        return `from \"${normalizedPath}\"`;\n      });\n\n      fs.writeFileSync(fullPath, content);\n    }\n  }\n}\n\n// Copy .d.ts files maintaining directory structure\nfunction copyDtsFiles(sourceDir: string, targetDir: string) {\n  const files = fs.readdirSync(sourceDir);\n\n  if (!fs.existsSync(targetDir)) {\n    fs.mkdirSync(targetDir, { recursive: true });\n  }\n\n  for (const file of files) {\n    const sourcePath = path.join(sourceDir, file);\n    const targetPath = path.join(targetDir, file);\n    const stat = fs.statSync(sourcePath);\n\n    if (stat.isDirectory()) {\n      copyDtsFiles(sourcePath, targetPath);\n    } else if (file.endsWith(\".d.ts\")) {\n      fs.copyFileSync(sourcePath, targetPath);\n    }\n  }\n}\n\nconst cjsConfig: esbuild.BuildOptions = {\n  ...baseConfig,\n  outdir: \"dist/cjs\",\n  format: \"cjs\",\n  outExtension: { \".js\": \".cjs\" },\n};\n\nconst esmConfig: esbuild.BuildOptions = {\n  ...baseConfig,\n  outdir: \"dist/esm\",\n  format: \"esm\",\n  outExtension: { \".js\": \".mjs\" },\n};\n\nasync function build() {\n  console.log(\"Building CJS and ESM versions...\");\n\n  // First, generate type declarations\n  generateTypeDeclarations();\n\n  const cjsCtx = await esbuild.context(cjsConfig);\n  const esmCtx = await esbuild.context(esmConfig);\n\n  if (watch) {\n    console.log(\"Watching for changes...\");\n    await Promise.all([\n      cjsCtx.watch(),\n      esmCtx.watch(),\n    ]);\n  } else {\n    await Promise.all([\n      cjsCtx.rebuild(),\n      esmCtx.rebuild(),\n    ]);\n\n    await Promise.all([\n      cjsCtx.dispose(),\n      esmCtx.dispose(),\n    ]);\n\n    console.log(\"✅ Build completed successfully!\");\n    console.log(\"  - CJS: dist/cjs/server.cjs\");\n    console.log(\"  - ESM: dist/esm/server.mjs\");\n    console.log(\"  - Types: dist/*.d.ts\");\n  }\n}\n\nbuild().catch((err) => {\n  console.error(err);\n  process.exit(1);\n});\n"
  },
  {
    "path": "packages/core/scripts/esbuild-plugin-path-alias.ts",
    "content": "import * as esbuild from \"esbuild\";\nimport * as path from \"path\";\nimport * as fs from \"fs\";\n\n/**\n * esbuild plugin to resolve @/ path aliases\n * Converts @/ imports to relative paths based on baseUrl\n */\nexport const pathAliasPlugin = (options: {\n  alias: Record<string, string>;\n  baseUrl: string;\n}): esbuild.Plugin => {\n  return {\n    name: \"path-alias\",\n    setup(build) {\n      const { alias, baseUrl } = options;\n\n      // Resolve each alias pattern\n      for (const [pattern, target] of Object.entries(alias)) {\n        // Remove trailing /* from pattern if present\n        const patternKey = pattern.replace(/\\/\\*$/, \"\");\n        // Escape special regex characters in pattern\n        const escapedPattern = patternKey.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\n        build.onResolve(\n          { filter: new RegExp(`^${escapedPattern}/`) },\n          (args) => {\n            // Extract the path after @/\n            const importPath = args.path.replace(new RegExp(`^${escapedPattern}/`), \"\");\n            // Remove file extension if present in import\n            const importPathWithoutExt = importPath.replace(/\\.[^.]+$/, \"\");\n            const resolvedPath = path.resolve(baseUrl, target.replace(/\\*$/, \"\"), importPathWithoutExt);\n\n            // Try to find the file with different extensions\n            const extensions = [\".ts\", \".tsx\", \".js\", \".jsx\", \".json\"];\n            for (const ext of extensions) {\n              const fileWithExt = resolvedPath + ext;\n              if (fs.existsSync(fileWithExt) && fs.statSync(fileWithExt).isFile()) {\n                return { path: fileWithExt };\n              }\n            }\n\n            // Check if it's a directory with index file\n            if (fs.existsSync(resolvedPath) && fs.statSync(resolvedPath).isDirectory()) {\n              for (const ext of extensions) {\n                const indexPath = path.join(resolvedPath, `index${ext}`);\n                if (fs.existsSync(indexPath) && fs.statSync(indexPath).isFile()) {\n                  return { path: indexPath };\n                }\n              }\n            }\n\n            // Return resolved path even if file doesn't exist (esbuild will handle error)\n            return { path: resolvedPath + \".ts\" };\n          }\n        );\n      }\n    },\n  };\n};\n"
  },
  {
    "path": "packages/core/src/api/middleware.ts",
    "content": "import { FastifyRequest, FastifyReply } from \"fastify\";\n\nexport interface ApiError extends Error {\n  statusCode?: number;\n  code?: string;\n  type?: string;\n}\n\nexport function createApiError(\n  message: string,\n  statusCode: number = 500,\n  code: string = \"internal_error\",\n  type: string = \"api_error\"\n): ApiError {\n  const error = new Error(message) as ApiError;\n  error.statusCode = statusCode;\n  error.code = code;\n  error.type = type;\n  return error;\n}\n\nexport async function errorHandler(\n  error: ApiError,\n  request: FastifyRequest,\n  reply: FastifyReply\n) {\n  request.log.error(error);\n\n  const statusCode = error.statusCode || 500;\n  const response = {\n    error: {\n      message: error.message + error.stack || \"Internal Server Error\",\n      type: error.type || \"api_error\",\n      code: error.code || \"internal_error\",\n    },\n  };\n\n  return reply.code(statusCode).send(response);\n}\n"
  },
  {
    "path": "packages/core/src/api/routes.ts",
    "content": "import {\n  FastifyInstance,\n  FastifyPluginAsync,\n  FastifyRequest,\n  FastifyReply,\n} from \"fastify\";\nimport { RegisterProviderRequest, LLMProvider } from \"@/types/llm\";\nimport { sendUnifiedRequest } from \"@/utils/request\";\nimport { createApiError } from \"./middleware\";\nimport { version } from \"../../package.json\";\nimport { ConfigService } from \"@/services/config\";\nimport { ProviderService } from \"@/services/provider\";\nimport { TransformerService } from \"@/services/transformer\";\nimport { Transformer } from \"@/types/transformer\";\n\n// Extend FastifyInstance to include custom services\ndeclare module \"fastify\" {\n  interface FastifyInstance {\n    configService: ConfigService;\n    providerService: ProviderService;\n    transformerService: TransformerService;\n  }\n\n  interface FastifyRequest {\n    provider?: string;\n  }\n}\n\n/**\n * Main handler for transformer endpoints\n * Coordinates the entire request processing flow: validate provider, handle request transformers,\n * send request, handle response transformers, format response\n */\nasync function handleTransformerEndpoint(\n  req: FastifyRequest,\n  reply: FastifyReply,\n  fastify: FastifyInstance,\n  transformer: any\n) {\n  const body = req.body as any;\n  const providerName = req.provider!;\n  const provider = fastify.providerService.getProvider(providerName);\n\n  // Validate provider exists\n  if (!provider) {\n    throw createApiError(\n      `Provider '${providerName}' not found`,\n      404,\n      \"provider_not_found\"\n    );\n  }\n\n  try {\n    // Process request transformer chain\n    const { requestBody, config, bypass } = await processRequestTransformers(\n      body,\n      provider,\n      transformer,\n      req.headers,\n      {\n        req,\n      }\n    );\n\n    // Send request to LLM provider\n    const response = await sendRequestToProvider(\n      requestBody,\n      config,\n      provider,\n      fastify,\n      bypass,\n      transformer,\n      {\n        req,\n      }\n    );\n\n    // Process response transformer chain\n    const finalResponse = await processResponseTransformers(\n      requestBody,\n      response,\n      provider,\n      transformer,\n      bypass,\n      {\n        req,\n      }\n    );\n\n    // Format and return response\n    return formatResponse(finalResponse, reply, body);\n  } catch (error: any) {\n    // Handle fallback if error occurs\n    if (error.code === 'provider_response_error') {\n      const fallbackResult = await handleFallback(req, reply, fastify, transformer, error);\n      if (fallbackResult) {\n        return fallbackResult;\n      }\n    }\n    throw error;\n  }\n}\n\n/**\n * Handle fallback logic when request fails\n * Tries each fallback model in sequence until one succeeds\n */\nasync function handleFallback(\n  req: FastifyRequest,\n  reply: FastifyReply,\n  fastify: FastifyInstance,\n  transformer: any,\n  error: any\n): Promise<any> {\n  const scenarioType = (req as any).scenarioType || 'default';\n  const fallbackConfig = fastify.configService.get<any>('fallback');\n\n  if (!fallbackConfig || !fallbackConfig[scenarioType]) {\n    return null;\n  }\n\n  const fallbackList = fallbackConfig[scenarioType] as string[];\n  if (!Array.isArray(fallbackList) || fallbackList.length === 0) {\n    return null;\n  }\n\n  req.log.warn(`Request failed for ${(req as any).scenarioType}, trying ${fallbackList.length} fallback models`);\n\n  // Try each fallback model in sequence\n  for (const fallbackModel of fallbackList) {\n    try {\n      req.log.info(`Trying fallback model: ${fallbackModel}`);\n\n      // Update request with fallback model\n      const newBody = { ...(req.body as any) };\n      const [fallbackProvider, ...fallbackModelName] = fallbackModel.split(',');\n      newBody.model = fallbackModelName.join(',');\n\n      // Create new request object with updated provider and body\n      const newReq = {\n        ...req,\n        provider: fallbackProvider,\n        body: newBody,\n      };\n\n      const provider = fastify.providerService.getProvider(fallbackProvider);\n      if (!provider) {\n        req.log.warn(`Fallback provider '${fallbackProvider}' not found, skipping`);\n        continue;\n      }\n\n      // Process request transformer chain\n      const { requestBody, config, bypass } = await processRequestTransformers(\n        newBody,\n        provider,\n        transformer,\n        req.headers,\n        { req: newReq }\n      );\n\n      // Send request to LLM provider\n      const response = await sendRequestToProvider(\n        requestBody,\n        config,\n        provider,\n        fastify,\n        bypass,\n        transformer,\n        { req: newReq }\n      );\n\n      // Process response transformer chain\n      const finalResponse = await processResponseTransformers(\n        requestBody,\n        response,\n        provider,\n        transformer,\n        bypass,\n        { req: newReq }\n      );\n\n      req.log.info(`Fallback model ${fallbackModel} succeeded`);\n\n      // Format and return response\n      return formatResponse(finalResponse, reply, newBody);\n    } catch (fallbackError: any) {\n      req.log.warn(`Fallback model ${fallbackModel} failed: ${fallbackError.message}`);\n      continue;\n    }\n  }\n\n  req.log.error(`All fallback models failed for yichu ${scenarioType}`);\n  return null;\n}\n\n/**\n * Process request transformer chain\n * Sequentially execute transformRequestOut, provider transformers, model-specific transformers\n * Returns processed request body, config, and flag indicating whether to skip transformers\n */\nasync function processRequestTransformers(\n  body: any,\n  provider: any,\n  transformer: any,\n  headers: any,\n  context: any\n) {\n  let requestBody = body;\n  let config: any = {};\n  let bypass = false;\n\n  // Check if transformers should be bypassed (passthrough mode)\n  bypass = shouldBypassTransformers(provider, transformer, body);\n\n  if (bypass) {\n    if (headers instanceof Headers) {\n      headers.delete(\"content-length\");\n    } else {\n      delete headers[\"content-length\"];\n    }\n    config.headers = headers;\n  }\n\n  // Execute transformer's transformRequestOut method\n  if (!bypass && typeof transformer.transformRequestOut === \"function\") {\n    const transformOut = await transformer.transformRequestOut(requestBody);\n    if (transformOut.body) {\n      requestBody = transformOut.body;\n      config = transformOut.config || {};\n    } else {\n      requestBody = transformOut;\n    }\n  }\n\n  // Execute provider-level transformers\n  if (!bypass && provider.transformer?.use?.length) {\n    for (const providerTransformer of provider.transformer.use) {\n      if (\n        !providerTransformer ||\n        typeof providerTransformer.transformRequestIn !== \"function\"\n      ) {\n        continue;\n      }\n      const transformIn = await providerTransformer.transformRequestIn(\n        requestBody,\n        provider,\n        context\n      );\n      if (transformIn.body) {\n        requestBody = transformIn.body;\n        config = { ...config, ...transformIn.config };\n      } else {\n        requestBody = transformIn;\n      }\n    }\n  }\n\n  // Execute model-specific transformers\n  if (!bypass && provider.transformer?.[body.model]?.use?.length) {\n    for (const modelTransformer of provider.transformer[body.model].use) {\n      if (\n        !modelTransformer ||\n        typeof modelTransformer.transformRequestIn !== \"function\"\n      ) {\n        continue;\n      }\n      requestBody = await modelTransformer.transformRequestIn(\n        requestBody,\n        provider,\n        context\n      );\n    }\n  }\n\n  return { requestBody, config, bypass };\n}\n\n/**\n * Determine if transformers should be bypassed (passthrough mode)\n * Skip other transformers when provider only uses one transformer and it matches the current one\n */\nfunction shouldBypassTransformers(\n  provider: any,\n  transformer: any,\n  body: any\n): boolean {\n  return (\n    provider.transformer?.use?.length === 1 &&\n    provider.transformer.use[0].name === transformer.name &&\n    (!provider.transformer?.[body.model]?.use.length ||\n      (provider.transformer?.[body.model]?.use.length === 1 &&\n        provider.transformer?.[body.model]?.use[0].name === transformer.name))\n  );\n}\n\n/**\n * Send request to LLM provider\n * Handle authentication, build request config, send request and handle errors\n */\nasync function sendRequestToProvider(\n  requestBody: any,\n  config: any,\n  provider: any,\n  fastify: FastifyInstance,\n  bypass: boolean,\n  transformer: any,\n  context: any\n) {\n  const url = config.url || new URL(provider.baseUrl);\n\n  // Handle authentication in passthrough mode\n  if (bypass && typeof transformer.auth === \"function\") {\n    const auth = await transformer.auth(requestBody, provider);\n    if (auth.body) {\n      requestBody = auth.body;\n      let headers = config.headers || {};\n      if (auth.config?.headers) {\n        headers = {\n          ...headers,\n          ...auth.config.headers,\n        };\n        delete headers.host;\n        delete auth.config.headers;\n      }\n      config = {\n        ...config,\n        ...auth.config,\n        headers,\n      };\n    } else {\n      requestBody = auth;\n    }\n  }\n\n  // Send HTTP request\n  // Prepare headers\n  const requestHeaders: Record<string, string> = {\n    Authorization: `Bearer ${provider.apiKey}`,\n    ...(config?.headers || {}),\n  };\n\n  for (const key in requestHeaders) {\n    if (requestHeaders[key] === \"undefined\") {\n      delete requestHeaders[key];\n    } else if (\n      [\"authorization\", \"Authorization\"].includes(key) &&\n      requestHeaders[key]?.includes(\"undefined\")\n    ) {\n      delete requestHeaders[key];\n    }\n  }\n\n  const response = await sendUnifiedRequest(\n    url,\n    requestBody,\n    {\n      httpsProxy: fastify.configService.getHttpsProxy(),\n      ...config,\n      headers: JSON.parse(JSON.stringify(requestHeaders)),\n    },\n    context,\n    fastify.log\n  );\n\n  // Handle request errors\n  if (!response.ok) {\n    const errorText = await response.text();\n    fastify.log.error(\n      `[provider_response_error] Error from provider(${provider.name},${requestBody.model}: ${response.status}): ${errorText}`,\n    );\n    throw createApiError(\n      `Error from provider(${provider.name},${requestBody.model}: ${response.status}): ${errorText}`,\n      response.status,\n      \"provider_response_error\"\n    );\n  }\n\n  return response;\n}\n\n/**\n * Process response transformer chain\n * Sequentially execute provider transformers, model-specific transformers, transformer's transformResponseIn\n */\nasync function processResponseTransformers(\n  requestBody: any,\n  response: any,\n  provider: any,\n  transformer: any,\n  bypass: boolean,\n  context: any\n) {\n  let finalResponse = response;\n\n  // Execute provider-level response transformers\n  if (!bypass && provider.transformer?.use?.length) {\n    for (const providerTransformer of Array.from(\n      provider.transformer.use\n    ).reverse() as Transformer[]) {\n      if (\n        !providerTransformer ||\n        typeof providerTransformer.transformResponseOut !== \"function\"\n      ) {\n        continue;\n      }\n      finalResponse = await providerTransformer.transformResponseOut!(\n        finalResponse,\n        context\n      );\n    }\n  }\n\n  // Execute model-specific response transformers\n  if (!bypass && provider.transformer?.[requestBody.model]?.use?.length) {\n    for (const modelTransformer of Array.from(\n      provider.transformer[requestBody.model].use\n    ).reverse() as Transformer[]) {\n      if (\n        !modelTransformer ||\n        typeof modelTransformer.transformResponseOut !== \"function\"\n      ) {\n        continue;\n      }\n      finalResponse = await modelTransformer.transformResponseOut!(\n        finalResponse,\n        context\n      );\n    }\n  }\n\n  // Execute transformer's transformResponseIn method\n  if (!bypass && transformer.transformResponseIn) {\n    finalResponse = await transformer.transformResponseIn(\n      finalResponse,\n      context\n    );\n  }\n\n  return finalResponse;\n}\n\n/**\n * Format and return response\n * Handle HTTP status codes, format streaming and regular responses\n */\nfunction formatResponse(response: any, reply: FastifyReply, body: any) {\n  // Set HTTP status code\n  if (!response.ok) {\n    reply.code(response.status);\n  }\n\n  // Handle streaming response\n  const isStream = body.stream === true;\n  if (isStream) {\n    reply.header(\"Content-Type\", \"text/event-stream\");\n    reply.header(\"Cache-Control\", \"no-cache\");\n    reply.header(\"Connection\", \"keep-alive\");\n    return reply.send(response.body);\n  } else {\n    // Handle regular JSON response\n    return response.json();\n  }\n}\n\nexport const registerApiRoutes = async (\n  fastify: FastifyInstance\n) => {\n  // Health and info endpoints\n  fastify.get(\"/\", async () => {\n    return { message: \"LLMs API\", version };\n  });\n\n  fastify.get(\"/health\", async () => {\n    return { status: \"ok\", timestamp: new Date().toISOString() };\n  });\n\n  const transformersWithEndpoint =\n    fastify.transformerService.getTransformersWithEndpoint();\n\n  for (const { transformer } of transformersWithEndpoint) {\n    if (transformer.endPoint) {\n      fastify.post(\n        transformer.endPoint,\n        async (req: FastifyRequest, reply: FastifyReply) => {\n          return handleTransformerEndpoint(req, reply, fastify, transformer);\n        }\n      );\n    }\n  }\n\n  fastify.post(\n    \"/providers\",\n    {\n      schema: {\n        body: {\n          type: \"object\",\n          properties: {\n            id: { type: \"string\" },\n            name: { type: \"string\" },\n            type: { type: \"string\", enum: [\"openai\", \"anthropic\"] },\n            baseUrl: { type: \"string\" },\n            apiKey: { type: \"string\" },\n            models: { type: \"array\", items: { type: \"string\" } },\n          },\n          required: [\"id\", \"name\", \"type\", \"baseUrl\", \"apiKey\", \"models\"],\n        },\n      },\n    },\n    async (\n      request: FastifyRequest<{ Body: RegisterProviderRequest }>,\n      reply: FastifyReply\n    ) => {\n      // Validation\n      const { name, baseUrl, apiKey, models } = request.body;\n\n      if (!name?.trim()) {\n        throw createApiError(\n          \"Provider name is required\",\n          400,\n          \"invalid_request\"\n        );\n      }\n\n      if (!baseUrl || !isValidUrl(baseUrl)) {\n        throw createApiError(\n          \"Valid base URL is required\",\n          400,\n          \"invalid_request\"\n        );\n      }\n\n      if (!apiKey?.trim()) {\n        throw createApiError(\"API key is required\", 400, \"invalid_request\");\n      }\n\n      if (!models || !Array.isArray(models) || models.length === 0) {\n        throw createApiError(\n          \"At least one model is required\",\n          400,\n          \"invalid_request\"\n        );\n      }\n\n      // Check if provider already exists\n      if (fastify.providerService.getProvider(request.body.name)) {\n        throw createApiError(\n          `Provider with name '${request.body.name}' already exists`,\n          400,\n          \"provider_exists\"\n        );\n      }\n\n      return fastify.providerService.registerProvider(request.body);\n    }\n  );\n\n  fastify.get(\"/providers\", async () => {\n    return fastify.providerService.getProviders();\n  });\n\n  fastify.get(\n    \"/providers/:id\",\n    {\n      schema: {\n        params: {\n          type: \"object\",\n          properties: { id: { type: \"string\" } },\n          required: [\"id\"],\n        },\n      },\n    },\n    async (request: FastifyRequest<{ Params: { id: string } }>) => {\n      const provider = fastify.providerService.getProvider(\n        request.params.id\n      );\n      if (!provider) {\n        throw createApiError(\"Provider not found\", 404, \"provider_not_found\");\n      }\n      return provider;\n    }\n  );\n\n  fastify.put(\n    \"/providers/:id\",\n    {\n      schema: {\n        params: {\n          type: \"object\",\n          properties: { id: { type: \"string\" } },\n          required: [\"id\"],\n        },\n        body: {\n          type: \"object\",\n          properties: {\n            name: { type: \"string\" },\n            type: { type: \"string\", enum: [\"openai\", \"anthropic\"] },\n            baseUrl: { type: \"string\" },\n            apiKey: { type: \"string\" },\n            models: { type: \"array\", items: { type: \"string\" } },\n            enabled: { type: \"boolean\" },\n          },\n        },\n      },\n    },\n    async (\n      request: FastifyRequest<{\n        Params: { id: string };\n        Body: Partial<LLMProvider>;\n      }>,\n      reply\n    ) => {\n      const provider = fastify.providerService.updateProvider(\n        request.params.id,\n        request.body\n      );\n      if (!provider) {\n        throw createApiError(\"Provider not found\", 404, \"provider_not_found\");\n      }\n      return provider;\n    }\n  );\n\n  fastify.delete(\n    \"/providers/:id\",\n    {\n      schema: {\n        params: {\n          type: \"object\",\n          properties: { id: { type: \"string\" } },\n          required: [\"id\"],\n        },\n      },\n    },\n    async (request: FastifyRequest<{ Params: { id: string } }>) => {\n      const success = fastify.providerService.deleteProvider(\n        request.params.id\n      );\n      if (!success) {\n        throw createApiError(\"Provider not found\", 404, \"provider_not_found\");\n      }\n      return { message: \"Provider deleted successfully\" };\n    }\n  );\n\n  fastify.patch(\n    \"/providers/:id/toggle\",\n    {\n      schema: {\n        params: {\n          type: \"object\",\n          properties: { id: { type: \"string\" } },\n          required: [\"id\"],\n        },\n        body: {\n          type: \"object\",\n          properties: { enabled: { type: \"boolean\" } },\n          required: [\"enabled\"],\n        },\n      },\n    },\n    async (\n      request: FastifyRequest<{\n        Params: { id: string };\n        Body: { enabled: boolean };\n      }>,\n      reply\n    ) => {\n      const success = fastify.providerService.toggleProvider(\n        request.params.id,\n        request.body.enabled\n      );\n      if (!success) {\n        throw createApiError(\"Provider not found\", 404, \"provider_not_found\");\n      }\n      return {\n        message: `Provider ${\n          request.body.enabled ? \"enabled\" : \"disabled\"\n        } successfully`,\n      };\n    }\n  );\n};\n\n// Helper function\nfunction isValidUrl(url: string): boolean {\n  try {\n    new URL(url);\n    return true;\n  } catch {\n    return false;\n  }\n}\n"
  },
  {
    "path": "packages/core/src/plugins/index.ts",
    "content": "export * from './types';\nexport { pluginManager } from './plugin-manager';\nexport { tokenSpeedPlugin, getTokenSpeedStats, getGlobalTokenSpeedStats } from './token-speed';\nexport * from './output';\n"
  },
  {
    "path": "packages/core/src/plugins/output/console-handler.ts",
    "content": "import { OutputHandler, OutputOptions, ConsoleOutputConfig } from './types';\n\n/**\n * Console output handler\n * Supports colored output and multiple log levels\n */\nexport class ConsoleOutputHandler implements OutputHandler {\n  type = 'console' as const;\n  private config: ConsoleOutputConfig;\n\n  // ANSI color codes\n  private colors = {\n    reset: '\\x1b[0m',\n    bright: '\\x1b[1m',\n    dim: '\\x1b[2m',\n    red: '\\x1b[31m',\n    green: '\\x1b[32m',\n    yellow: '\\x1b[33m',\n    blue: '\\x1b[34m',\n    magenta: '\\x1b[35m',\n    cyan: '\\x1b[36m',\n    white: '\\x1b[37m'\n  };\n\n  constructor(config: ConsoleOutputConfig = {}) {\n    this.config = {\n      colors: true,\n      level: 'log',\n      ...config\n    };\n  }\n\n  /**\n   * Format output data\n   */\n  private formatData(data: any, options: OutputOptions): string {\n    const { format = 'text', timestamp = true, prefix, metadata } = options || {};\n\n    // Build prefix\n    let output = '';\n\n    if (timestamp) {\n      const time = new Date().toISOString();\n      output += this.config.colors\n        ? `${this.colors.cyan}[${time}]${this.colors.reset} `\n        : `[${time}] `;\n    }\n\n    if (prefix) {\n      output += this.config.colors\n        ? `${this.colors.bright}${prefix}${this.colors.reset} `\n        : `${prefix} `;\n    }\n\n    // Format data\n    switch (format) {\n      case 'json':\n        output += JSON.stringify(data, null, 2);\n        break;\n\n      case 'markdown':\n        if (typeof data === 'object') {\n          output += this.toMarkdown(data);\n        } else {\n          output += String(data);\n        }\n        break;\n\n      case 'text':\n      default:\n        if (typeof data === 'object') {\n          output += JSON.stringify(data, null, 2);\n        } else {\n          output += String(data);\n        }\n        break;\n    }\n\n    // Add metadata\n    if (metadata && Object.keys(metadata).length > 0) {\n      output += '\\n' + (this.config.colors ? `${this.colors.dim}` : '');\n      output += 'Metadata: ' + JSON.stringify(metadata, null, 2);\n      if (this.config.colors) output += this.colors.reset;\n    }\n\n    return output;\n  }\n\n  /**\n   * Convert object to Markdown format\n   */\n  private toMarkdown(data: any, indent = 0): string {\n    const padding = '  '.repeat(indent);\n\n    if (Array.isArray(data)) {\n      return data.map(item => {\n        if (typeof item === 'object') {\n          return `${padding}-\\n${this.toMarkdown(item, indent + 1)}`;\n        }\n        return `${padding}- ${item}`;\n      }).join('\\n');\n    }\n\n    if (typeof data === 'object' && data !== null) {\n      return Object.entries(data).map(([key, value]) => {\n        if (typeof value === 'object' && value !== null) {\n          return `${padding}${key}:\\n${this.toMarkdown(value, indent + 1)}`;\n        }\n        return `${padding}${key}: ${value}`;\n      }).join('\\n');\n    }\n\n    return `${padding}${data}`;\n  }\n\n  /**\n   * Output data\n   */\n  async output(data: any, options: OutputOptions = {}): Promise<boolean> {\n    try {\n      const formatted = this.formatData(data, options);\n      const logMethod = this.config.level || 'log';\n\n      // Output based on configured log level\n      switch (logMethod) {\n        case 'info':\n          console.info(formatted);\n          break;\n        case 'warn':\n          console.warn(formatted);\n          break;\n        case 'error':\n          console.error(formatted);\n          break;\n        case 'debug':\n          console.debug(formatted);\n          break;\n        case 'log':\n        default:\n          console.log(formatted);\n          break;\n      }\n\n      return true;\n    } catch (error) {\n      console.error('[ConsoleOutputHandler] Output failed:', error);\n      return false;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/core/src/plugins/output/index.ts",
    "content": "// Type definitions\nexport * from './types';\n\n// Output handler implementations\nexport { ConsoleOutputHandler } from './console-handler';\nexport { WebhookOutputHandler } from './webhook-handler';\nexport { TempFileOutputHandler } from './temp-file-handler';\n\n// Output manager\nexport { outputManager, output, outputTo } from './output-manager';\n\n/**\n * Convenience function: Create and register a Console output handler\n * @param config Console output handler configuration\n * @returns Output manager instance\n */\nexport function registerConsoleOutput(config?: import('./types').ConsoleOutputConfig) {\n  const { ConsoleOutputHandler } = require('./console-handler');\n  const handler = new ConsoleOutputHandler(config);\n  const { outputManager } = require('./output-manager');\n  const name = 'console_' + Date.now();\n  outputManager.registerHandler(name, handler);\n  return outputManager;\n}\n\n/**\n * Convenience function: Create and register a Webhook output handler\n * @param config Webhook output handler configuration\n * @returns Output manager instance\n */\nexport function registerWebhookOutput(config: import('./types').WebhookOutputConfig) {\n  const { WebhookOutputHandler } = require('./webhook-handler');\n  const handler = new WebhookOutputHandler(config);\n  const { outputManager } = require('./output-manager');\n  const name = 'webhook_' + Date.now();\n  outputManager.registerHandler(name, handler);\n  return outputManager;\n}\n\n/**\n * Convenience function: Create and register a Temp File output handler\n * @param config Temp file output handler configuration\n * @returns Output manager instance\n */\nexport function registerTempFileOutput(config?: import('./types').TempFileOutputConfig) {\n  const { TempFileOutputHandler } = require('./temp-file-handler');\n  const handler = new TempFileOutputHandler(config);\n  const { outputManager } = require('./output-manager');\n  const name = 'temp-file_' + Date.now();\n  outputManager.registerHandler(name, handler);\n  return outputManager;\n}\n\n/**\n * Convenience function: Register output handlers in batch\n * @param configs Output handler configuration array\n * @returns Output manager instance\n */\nexport function registerOutputHandlers(configs: import('./types').OutputHandlerConfig[]) {\n  const { outputManager } = require('./output-manager');\n  outputManager.registerHandlers(configs);\n  return outputManager;\n}\n"
  },
  {
    "path": "packages/core/src/plugins/output/output-manager.ts",
    "content": "import { OutputHandler, OutputOptions, OutputHandlerConfig } from './types';\nimport { ConsoleOutputHandler } from './console-handler';\nimport { WebhookOutputHandler } from './webhook-handler';\nimport { TempFileOutputHandler } from './temp-file-handler';\n\n/**\n * Output manager\n * Manages multiple output handlers and provides unified output interface\n */\nclass OutputManager {\n  private handlers: Map<string, OutputHandler> = new Map();\n  private defaultOptions: OutputOptions = {};\n\n  /**\n   * Register output handler\n   * @param name Output handler name\n   * @param handler Output handler instance\n   */\n  registerHandler(name: string, handler: OutputHandler): void {\n    this.handlers.set(name, handler);\n  }\n\n  /**\n   * Register output handlers in batch\n   * @param configs Output handler configuration array\n   */\n  registerHandlers(configs: OutputHandlerConfig[]): void {\n    for (const config of configs) {\n      if (config.enabled === false) {\n        continue;\n      }\n\n      try {\n        const handler = this.createHandler(config);\n        const name = config.type + '_' + Date.now();\n        this.registerHandler(name, handler);\n      } catch (error) {\n        console.error(`[OutputManager] Failed to register ${config.type} handler:`, error);\n      }\n    }\n  }\n\n  /**\n   * Create output handler instance\n   * @param config Output handler configuration\n   */\n  private createHandler(config: OutputHandlerConfig): OutputHandler {\n    switch (config.type) {\n      case 'console':\n        return new ConsoleOutputHandler(config.config as any);\n\n      case 'webhook':\n        return new WebhookOutputHandler(config.config as any);\n\n      case 'temp-file':\n        return new TempFileOutputHandler(config.config as any);\n\n      // Reserved for other output handler types\n      // case 'websocket':\n      //   return new WebSocketOutputHandler(config.config as any);\n\n      default:\n        throw new Error(`Unknown output handler type: ${config.type}`);\n    }\n  }\n\n  /**\n   * Remove output handler\n   * @param name Output handler name\n   */\n  unregisterHandler(name: string): boolean {\n    return this.handlers.delete(name);\n  }\n\n  /**\n   * Get output handler\n   * @param name Output handler name\n   */\n  getHandler(name: string): OutputHandler | undefined {\n    return this.handlers.get(name);\n  }\n\n  /**\n   * Get all output handlers\n   */\n  getAllHandlers(): Map<string, OutputHandler> {\n    return new Map(this.handlers);\n  }\n\n  /**\n   * Clear all output handlers\n   */\n  clearHandlers(): void {\n    this.handlers.clear();\n  }\n\n  /**\n   * Set default output options\n   * @param options Output options\n   */\n  setDefaultOptions(options: OutputOptions): void {\n    this.defaultOptions = { ...this.defaultOptions, ...options };\n  }\n\n  /**\n   * Get default output options\n   */\n  getDefaultOptions(): OutputOptions {\n    return { ...this.defaultOptions };\n  }\n\n  /**\n   * Output data to all registered output handlers\n   * @param data Data to output\n   * @param options Output options\n   * @returns Promise<{success: string[], failed: string[]}> Names of successful and failed handlers\n   */\n  async output(\n    data: any,\n    options?: OutputOptions\n  ): Promise<{ success: string[]; failed: string[] }> {\n    const mergedOptions = { ...this.defaultOptions, ...options };\n    const results = { success: [] as string[], failed: [] as string[] };\n\n    // Send data to all handlers in parallel\n    const promises = Array.from(this.handlers.entries()).map(\n      async ([name, handler]) => {\n        try {\n          const success = await handler.output(data, mergedOptions);\n          if (success) {\n            results.success.push(name);\n          } else {\n            results.failed.push(name);\n          }\n        } catch (error) {\n          console.error(`[OutputManager] Handler ${name} failed:`, error);\n          results.failed.push(name);\n        }\n      }\n    );\n\n    await Promise.all(promises);\n    return results;\n  }\n\n  /**\n   * Output data to specified output handlers\n   * @param handlerNames Array of output handler names\n   * @param data Data to output\n   * @param options Output options\n   * @returns Promise<{success: string[], failed: string[]}> Names of successful and failed handlers\n   */\n  async outputTo(\n    handlerNames: string[],\n    data: any,\n    options?: OutputOptions\n  ): Promise<{ success: string[]; failed: string[] }> {\n    const mergedOptions = { ...this.defaultOptions, ...options };\n    const results = { success: [] as string[], failed: [] as string[] };\n\n    const promises = handlerNames.map(async name => {\n      const handler = this.handlers.get(name);\n      if (!handler) {\n        console.warn(`[OutputManager] Handler ${name} not found`);\n        results.failed.push(name);\n        return;\n      }\n\n      try {\n        const success = await handler.output(data, mergedOptions);\n        if (success) {\n          results.success.push(name);\n        } else {\n          results.failed.push(name);\n        }\n      } catch (error) {\n        console.error(`[OutputManager] Handler ${name} failed:`, error);\n        results.failed.push(name);\n      }\n    });\n\n    await Promise.all(promises);\n    return results;\n  }\n\n  /**\n   * Output data to specified type of output handlers\n   * @param type Output handler type\n   * @param data Data to output\n   * @param options Output options\n   * @returns Promise<{success: string[], failed: string[]}> Names of successful and failed handlers\n   */\n  async outputToType(\n    type: string,\n    data: any,\n    options?: OutputOptions\n  ): Promise<{ success: string[]; failed: string[] }> {\n    const targetHandlers = Array.from(this.handlers.entries())\n      .filter(([_, handler]) => handler.type === type)\n      .map(([name]) => name);\n\n    return this.outputTo(targetHandlers, data, options);\n  }\n}\n\n/**\n * Global output manager instance\n */\nexport const outputManager = new OutputManager();\n\n/**\n * Convenience method: Quickly output data to all registered handlers\n * @param data Data to output\n * @param options Output options\n */\nexport async function output(data: any, options?: OutputOptions) {\n  return outputManager.output(data, options);\n}\n\n/**\n * Convenience method: Quickly output data to specified type of handlers\n * @param type Output handler type ('console' | 'webhook' | 'websocket')\n * @param data Data to output\n * @param options Output options\n */\nexport async function outputTo(type: string, data: any, options?: OutputOptions) {\n  return outputManager.outputToType(type, data, options);\n}\n"
  },
  {
    "path": "packages/core/src/plugins/output/temp-file-handler.ts",
    "content": "import { OutputHandler, OutputOptions } from './types';\nimport { writeFileSync, existsSync, mkdirSync } from 'fs';\nimport { join } from 'path';\nimport { tmpdir } from 'os';\n\n/**\n * Temp file output handler configuration\n */\nexport interface TempFileOutputConfig {\n  /**\n   * Subdirectory under system temp directory (default: 'claude-code-router')\n   */\n  subdirectory?: string;\n\n  /**\n   * File extension (default: 'json')\n   */\n  extension?: string;\n\n  /**\n   * Whether to include timestamp in filename (default: true)\n   */\n  includeTimestamp?: boolean;\n\n  /**\n   * Custom prefix for temp files (default: 'session')\n   */\n  prefix?: string;\n}\n\n/**\n * Temp file output handler\n * Writes data to temporary files in system temp directory\n */\nexport class TempFileOutputHandler implements OutputHandler {\n  type = 'temp-file' as const;\n  private config: TempFileOutputConfig;\n  private baseDir: string;\n\n  constructor(config: TempFileOutputConfig = {}) {\n    this.config = {\n      subdirectory: 'claude-code-router',\n      extension: 'json',\n      includeTimestamp: false,\n      prefix: 'session',\n      ...config\n    };\n\n    // Use system temp directory\n    const systemTempDir = tmpdir();\n    this.baseDir = join(systemTempDir, this.config.subdirectory!);\n\n    // Ensure directory exists\n    this.ensureDir();\n  }\n\n  /**\n   * Ensure directory exists\n   */\n  private ensureDir(): void {\n    try {\n      if (!existsSync(this.baseDir)) {\n        mkdirSync(this.baseDir, { recursive: true });\n      }\n    } catch (error) {\n      // Silently fail\n    }\n  }\n\n  /**\n   * Extract session ID from user_id string\n   * Format: \"user_..._session_<uuid>\"\n   */\n  private extractSessionId(userId: string): string | null {\n    try {\n      const match = userId.match(/_session_([a-f0-9-]+)/i);\n      return match ? match[1] : null;\n    } catch {\n      return null;\n    }\n  }\n\n  /**\n   * Get file path for temp file\n   */\n  private getFilePath(sessionId: string): string {\n    const prefix = this.config.prefix || 'session';\n    const ext = this.config.extension ? `.${this.config.extension}` : '';\n\n    let filename: string;\n    if (this.config.includeTimestamp) {\n      // Include timestamp in filename: prefix-sessionId-timestamp.ext\n      const timestamp = Date.now();\n      filename = `${prefix}-${sessionId}-${timestamp}${ext}`;\n    } else {\n      // Simple filename: prefix-sessionId.ext\n      filename = `${prefix}-${sessionId}${ext}`;\n    }\n\n    return join(this.baseDir, filename);\n  }\n\n  /**\n   * Output data to temp file\n   */\n  async output(data: any, options: OutputOptions = {}): Promise<boolean> {\n    try {\n      // Extract session ID from metadata\n      const sessionId = options.metadata?.sessionId;\n\n      if (!sessionId) {\n        // No session ID, skip output\n        return false;\n      }\n\n      // Prepare output data\n      const outputData = {\n        ...data,\n        timestamp: Date.now(),\n        sessionId\n      };\n\n      // Write to file\n      const filePath = this.getFilePath(sessionId);\n      writeFileSync(filePath, JSON.stringify(outputData, null, 2), 'utf-8');\n\n      return true;\n    } catch (error) {\n      // Silently fail to avoid disrupting main flow\n      return false;\n    }\n  }\n\n  /**\n   * Get the base directory where temp files are stored\n   */\n  getBaseDir(): string {\n    return this.baseDir;\n  }\n}\n"
  },
  {
    "path": "packages/core/src/plugins/output/types.ts",
    "content": "/**\n * Output handler interface\n * All output handlers must implement this interface\n */\nexport interface OutputHandler {\n  /**\n   * Output handler type name\n   */\n  type: string;\n\n  /**\n   * Output data\n   * @param data Data to output\n   * @param options Output options\n   * @returns Promise<boolean> Whether output was successful\n   */\n  output(data: any, options?: OutputOptions): Promise<boolean>;\n}\n\n/**\n * Output options\n */\nexport interface OutputOptions {\n  /**\n   * Output format\n   */\n  format?: 'json' | 'text' | 'markdown';\n\n  /**\n   * Whether to include timestamp\n   */\n  timestamp?: boolean;\n\n  /**\n   * Custom prefix\n   */\n  prefix?: string;\n\n  /**\n   * Additional metadata\n   */\n  metadata?: Record<string, any>;\n\n  /**\n   * Timeout (milliseconds)\n   */\n  timeout?: number;\n}\n\n/**\n * Console output handler configuration\n */\nexport interface ConsoleOutputConfig {\n  /**\n   * Whether to use colored output\n   */\n  colors?: boolean;\n\n  /**\n   * Log level\n   */\n  level?: 'log' | 'info' | 'warn' | 'error' | 'debug';\n}\n\n/**\n * Webhook output handler configuration\n */\nexport interface WebhookOutputConfig {\n  /**\n   * Webhook URL\n   */\n  url: string;\n\n  /**\n   * HTTP request method\n   */\n  method?: 'POST' | 'PUT' | 'PATCH';\n\n  /**\n   * Request headers\n   */\n  headers?: Record<string, string>;\n\n  /**\n   * Authentication information\n   */\n  auth?: {\n    type: 'bearer' | 'basic' | 'custom';\n    token?: string;\n    username?: string;\n    password?: string;\n    custom?: {\n      header: string;\n      value: string;\n    };\n  };\n\n  /**\n   * Retry configuration\n   */\n  retry?: {\n    maxAttempts: number;\n    backoffMs: number;\n  };\n\n  /**\n   * Whether to handle failures silently (only log, don't throw)\n   */\n  silent?: boolean;\n}\n\n/**\n * WebSocket output handler configuration (reserved for future use)\n */\nexport interface WebSocketOutputConfig {\n  /**\n   * WebSocket URL\n   */\n  url: string;\n\n  /**\n   * Reconnection configuration\n   */\n  reconnect?: {\n    maxAttempts: number;\n    intervalMs: number;\n  };\n\n  /**\n   * Heartbeat configuration\n   */\n  heartbeat?: {\n    intervalMs: number;\n    message?: string;\n  };\n}\n\n/**\n * Temp file output handler configuration\n */\nexport interface TempFileOutputConfig {\n  /**\n   * Subdirectory under system temp directory (default: 'claude-code-router')\n   */\n  subdirectory?: string;\n\n  /**\n   * File extension (default: 'json')\n   */\n  extension?: string;\n\n  /**\n   * Whether to include timestamp in filename (default: true)\n   */\n  includeTimestamp?: boolean;\n\n  /**\n   * Custom prefix for temp files (default: 'session')\n   */\n  prefix?: string;\n}\n\n/**\n * Output handler registration configuration\n */\nexport interface OutputHandlerConfig {\n  /**\n   * Output handler type\n   */\n  type: 'console' | 'webhook' | 'websocket' | 'temp-file';\n\n  /**\n   * Whether enabled\n   */\n  enabled?: boolean;\n\n  /**\n   * Configuration options\n   */\n  config?: ConsoleOutputConfig | WebhookOutputConfig | WebSocketOutputConfig | TempFileOutputConfig;\n}\n"
  },
  {
    "path": "packages/core/src/plugins/output/webhook-handler.ts",
    "content": "import { OutputHandler, OutputOptions, WebhookOutputConfig } from './types';\n\n/**\n * Webhook output handler\n * Supports sending data to HTTP endpoints with retry and authentication\n */\nexport class WebhookOutputHandler implements OutputHandler {\n  type = 'webhook' as const;\n  private config: WebhookOutputConfig;\n  private defaultTimeout = 30000; // 30 second default timeout\n\n  constructor(config: WebhookOutputConfig) {\n    if (!config.url) {\n      throw new Error('Webhook URL is required');\n    }\n    this.config = {\n      method: 'POST',\n      retry: {\n        maxAttempts: 3,\n        backoffMs: 1000\n      },\n      silent: false,\n      ...config\n    };\n  }\n\n  /**\n   * Build request headers\n   */\n  private buildHeaders(): Record<string, string> {\n    const headers: Record<string, string> = {\n      'Content-Type': 'application/json',\n      ...(this.config.headers || {})\n    };\n\n    // Add authentication headers\n    if (this.config.auth) {\n      switch (this.config.auth.type) {\n        case 'bearer':\n          if (this.config.auth.token) {\n            headers['Authorization'] = `Bearer ${this.config.auth.token}`;\n          }\n          break;\n\n        case 'basic':\n          if (this.config.auth.username && this.config.auth.password) {\n            const credentials = Buffer.from(\n              `${this.config.auth.username}:${this.config.auth.password}`\n            ).toString('base64');\n            headers['Authorization'] = `Basic ${credentials}`;\n          }\n          break;\n\n        case 'custom':\n          if (this.config.auth.custom) {\n            headers[this.config.auth.custom.header] = this.config.auth.custom.value;\n          }\n          break;\n      }\n    }\n\n    return headers;\n  }\n\n  /**\n   * Build request body\n   */\n  private buildBody(data: any, options: OutputOptions): any {\n    const { format = 'json', timestamp = true, prefix, metadata } = options || {};\n\n    const body: any = {\n      data\n    };\n\n    if (timestamp) {\n      body.timestamp = new Date().toISOString();\n    }\n\n    if (prefix) {\n      body.prefix = prefix;\n    }\n\n    if (metadata && Object.keys(metadata).length > 0) {\n      body.metadata = metadata;\n    }\n\n    return body;\n  }\n\n  /**\n   * Send HTTP request\n   */\n  private async sendRequest(\n    url: string,\n    method: string,\n    headers: Record<string, string>,\n    body: any,\n    timeout: number\n  ): Promise<Response> {\n    const controller = new AbortController();\n    const timeoutId = setTimeout(() => controller.abort(), timeout);\n\n    try {\n      const response = await fetch(url, {\n        method,\n        headers,\n        body: JSON.stringify(body),\n        signal: controller.signal\n      });\n\n      clearTimeout(timeoutId);\n\n      if (!response.ok) {\n        throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n      }\n\n      return response;\n    } catch (error) {\n      clearTimeout(timeoutId);\n      throw error;\n    }\n  }\n\n  /**\n   * Delay function (for retry backoff)\n   */\n  private delay(ms: number): Promise<void> {\n    return new Promise(resolve => setTimeout(resolve, ms));\n  }\n\n  /**\n   * Send request with retry\n   */\n  private async sendWithRetry(\n    url: string,\n    method: string,\n    headers: Record<string, string>,\n    body: any,\n    timeout: number,\n    retry: { maxAttempts: number; backoffMs: number }\n  ): Promise<Response> {\n    let lastError: Error | null = null;\n\n    for (let attempt = 1; attempt <= retry.maxAttempts; attempt++) {\n      try {\n        return await this.sendRequest(url, method, headers, body, timeout);\n      } catch (error) {\n        lastError = error as Error;\n\n        // If this is the last attempt, throw error directly\n        if (attempt === retry.maxAttempts) {\n          break;\n        }\n\n        // Calculate backoff time (exponential backoff)\n        const backoffTime = retry.backoffMs * Math.pow(2, attempt - 1);\n\n        console.warn(\n          `[WebhookOutputHandler] Request failed (attempt ${attempt}/${retry.maxAttempts}), ` +\n          `retrying in ${backoffTime}ms...`,\n          (error as Error).message\n        );\n\n        await this.delay(backoffTime);\n      }\n    }\n\n    throw lastError;\n  }\n\n  /**\n   * Output data to Webhook\n   */\n  async output(data: any, options: OutputOptions = {}): Promise<boolean> {\n    const timeout = options.timeout || this.defaultTimeout;\n\n    try {\n      const headers = this.buildHeaders();\n      const body = this.buildBody(data, options);\n\n      const response = await this.sendWithRetry(\n        this.config.url,\n        this.config.method!,\n        headers,\n        body,\n        timeout,\n        this.config.retry!\n      );\n\n      return true;\n    } catch (error) {\n      const errorMessage = error instanceof Error ? error.message : String(error);\n\n      if (this.config.silent) {\n        console.error(`[WebhookOutputHandler] Failed to send data: ${errorMessage}`);\n        return false;\n      }\n\n      throw new Error(`Webhook output failed: ${errorMessage}`);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/core/src/plugins/plugin-manager.ts",
    "content": "import { FastifyInstance } from 'fastify';\nimport { CCRPlugin, PluginMetadata } from './types';\n\n/**\n * Plugin manager\n */\nclass PluginManager {\n  private plugins: Map<string, PluginMetadata> = new Map();\n  private pluginInstances: Map<string, CCRPlugin> = new Map();\n\n  /**\n   * Register a plugin\n   * @param plugin Plugin instance\n   * @param options Plugin configuration options\n   */\n  registerPlugin(plugin: CCRPlugin, options: any = {}): void {\n    this.pluginInstances.set(plugin.name, plugin);\n    this.plugins.set(plugin.name, {\n      name: plugin.name,\n      enabled: options.enabled !== false,\n      options\n    });\n  }\n\n  /**\n   * Enable a single plugin\n   * @param name Plugin name\n   * @param fastify Fastify instance\n   */\n  async enablePlugin(name: string, fastify: FastifyInstance): Promise<void> {\n    const metadata = this.plugins.get(name);\n    const plugin = this.pluginInstances.get(name);\n    if (!metadata || !plugin) {\n      throw new Error(`Plugin ${name} not found`);\n    }\n\n    if (metadata.enabled) {\n      await fastify.register(plugin.register, metadata.options);\n    }\n  }\n\n  /**\n   * Enable all registered plugins in batch\n   * @param fastify Fastify instance\n   */\n  async enablePlugins(fastify: FastifyInstance): Promise<void> {\n    for (const [name, metadata] of this.plugins) {\n      if (metadata.enabled) {\n        try {\n          await this.enablePlugin(name, fastify);\n        } catch (error) {\n          const errorMessage = error instanceof Error ? error.message : String(error);\n          fastify.log?.error(`Failed to enable plugin ${name}: ${errorMessage}`);\n        }\n      }\n    }\n  }\n\n  /**\n   * Get list of registered plugins\n   */\n  getPlugins(): PluginMetadata[] {\n    return Array.from(this.plugins.values());\n  }\n\n  /**\n   * Get plugin instance\n   * @param name Plugin name\n   */\n  getPlugin(name: string): CCRPlugin | undefined {\n    return this.pluginInstances.get(name);\n  }\n\n  /**\n   * Check if plugin is registered\n   * @param name Plugin name\n   */\n  hasPlugin(name: string): boolean {\n    return this.pluginInstances.has(name);\n  }\n\n  /**\n   * Check if plugin is enabled\n   * @param name Plugin name\n   */\n  isPluginEnabled(name: string): boolean {\n    const metadata = this.plugins.get(name);\n    return metadata?.enabled || false;\n  }\n\n  /**\n   * Dynamically enable/disable plugin\n   * @param name Plugin name\n   * @param enabled Whether to enable\n   */\n  setPluginEnabled(name: string, enabled: boolean): void {\n    const metadata = this.plugins.get(name);\n    if (metadata) {\n      metadata.enabled = enabled;\n    }\n  }\n\n  /**\n   * Remove plugin\n   * @param name Plugin name\n   */\n  removePlugin(name: string): void {\n    this.plugins.delete(name);\n    this.pluginInstances.delete(name);\n  }\n\n  /**\n   * Clear all plugins\n   */\n  clear(): void {\n    this.plugins.clear();\n    this.pluginInstances.clear();\n  }\n}\n\nexport const pluginManager = new PluginManager();\n"
  },
  {
    "path": "packages/core/src/plugins/token-speed.ts",
    "content": "import fp from 'fastify-plugin';\nimport { CCRPlugin, CCRPluginOptions } from './types';\nimport { SSEParserTransform } from '../utils/sse';\nimport { OutputHandlerConfig, OutputOptions, outputManager } from './output';\nimport { ITokenizer, TokenizerConfig } from '../types/tokenizer';\n\n/**\n * Token statistics interface\n */\ninterface TokenStats {\n  requestId: string;\n  sessionId?: string;\n  startTime: number;\n  firstTokenTime?: number;\n  lastTokenTime: number;\n  tokenCount: number;\n  tokensPerSecond: number;\n  timeToFirstToken?: number;\n  stream: boolean; // Whether this is a streaming request\n  tokenTimestamps: number[]; // Store timestamps of each token for per-second calculation\n}\n\n/**\n * Plugin options\n */\ninterface TokenSpeedOptions extends CCRPluginOptions {\n  /**\n   * Reporter type(s) to use for output\n   * Can be a single type or an array of types: 'console' | 'temp-file' | 'webhook'\n   * Default: ['console', 'temp-file']\n   */\n  reporter?: string | string[];\n\n  /**\n   * Output handler configurations\n   * Supports console, webhook, and other output handlers\n   */\n  outputHandlers?: OutputHandlerConfig[];\n\n  /**\n   * Default output options (format, prefix, etc.)\n   */\n  outputOptions?: OutputOptions;\n}\n\n// Store request-level statistics\nconst requestStats = new Map<string, TokenStats>();\n\n// Cache tokenizers by provider and model to avoid repeated initialization\nconst tokenizerCache = new Map<string, ITokenizer>();\n\n/**\n * Token speed measurement plugin\n */\nexport const tokenSpeedPlugin: CCRPlugin = {\n  name: 'token-speed',\n  version: '1.0.0',\n  description: 'Statistics for streaming response token generation speed',\n\n  // Use fp() to break encapsulation and apply hooks globally\n  register: fp(async (fastify, options: TokenSpeedOptions) => {\n    const opts = {\n      reporter: ['console', 'temp-file'],\n      ...options\n    };\n\n    // Normalize reporter to array\n    const reporters = Array.isArray(opts.reporter) ? opts.reporter : [opts.reporter];\n\n    // Initialize output handlers based on reporters if not explicitly configured\n    if (opts.outputHandlers && opts.outputHandlers.length > 0) {\n      outputManager.registerHandlers(opts.outputHandlers);\n    } else {\n      // Auto-register handlers based on reporter types\n      const handlersToRegister: OutputHandlerConfig[] = [];\n\n      for (const reporter of reporters) {\n        if (reporter === 'console') {\n          handlersToRegister.push({\n            type: 'console',\n            enabled: true,\n            config: {\n              colors: true,\n              level: 'log'\n            }\n          });\n        } else if (reporter === 'temp-file') {\n          handlersToRegister.push({\n            type: 'temp-file',\n            enabled: true,\n            config: {\n              subdirectory: 'claude-code-router',\n              extension: 'json',\n              includeTimestamp: true,\n              prefix: 'session'\n            }\n          });\n        } else if (reporter === 'webhook') {\n          // Webhook requires explicit config, skip auto-registration\n          console.warn(`[TokenSpeedPlugin] Webhook reporter requires explicit configuration in outputHandlers`);\n        }\n      }\n\n      if (handlersToRegister.length > 0) {\n        outputManager.registerHandlers(handlersToRegister);\n      }\n    }\n\n    // Set default output options\n    if (opts.outputOptions) {\n      outputManager.setDefaultOptions(opts.outputOptions);\n    }\n\n    /**\n     * Get or create tokenizer for a specific provider and model\n     */\n    const getTokenizerForRequest = async (request: any): Promise<ITokenizer | null> => {\n      const tokenizerService = (fastify as any).tokenizerService;\n      if (!tokenizerService) {\n        fastify.log?.warn('TokenizerService not available');\n        return null;\n      }\n\n      // Extract provider and model from request\n      // Format: \"provider,model\" or just \"model\"\n      if (!request.provider || !request.model) {\n        return null;\n      }\n      const providerName = request.provider;\n      const modelName = request.model;\n\n      // Create cache key\n      const cacheKey = `${providerName}:${modelName}`;\n\n      // Check cache first\n      if (tokenizerCache.has(cacheKey)) {\n        return tokenizerCache.get(cacheKey)!;\n      }\n\n      // Get tokenizer config for this model\n      const tokenizerConfig = tokenizerService.getTokenizerConfigForModel(providerName, modelName);\n\n      if (!tokenizerConfig) {\n        // No specific config, use fallback\n        fastify.log?.debug(`No tokenizer config for ${providerName}:${modelName}, using fallback`);\n        return null;\n      }\n\n      try {\n        // Create and cache tokenizer\n        const tokenizer = await tokenizerService.getTokenizer(tokenizerConfig);\n        tokenizerCache.set(cacheKey, tokenizer);\n        fastify.log?.info(`Created tokenizer for ${providerName}:${modelName} - ${tokenizer.name}`);\n        return tokenizer;\n      } catch (error: any) {\n        fastify.log?.warn(`Failed to create tokenizer for ${providerName}:${modelName}: ${error.message}`);\n        return null;\n      }\n    };\n\n    // Add onRequest hook to capture actual request start time (before processing)\n    fastify.addHook('onRequest', async (request) => {\n      const url = new URL(`http://127.0.0.1${request.url}`);\n      if (!url.pathname.endsWith(\"/v1/messages\")) return;\n      (request as any).requestStartTime = performance.now();\n    });\n\n    // Add onSend hook to intercept both streaming and non-streaming responses\n    fastify.addHook('onSend', async (request, _reply, payload) => {\n      const startTime = (request as any).requestStartTime;\n      if (!startTime) return;\n      const requestId = (request as any).id || Date.now().toString();\n\n      // Extract session ID from request body metadata\n      let sessionId: string | undefined;\n      try {\n        const userId = (request.body as any)?.metadata?.user_id;\n        if (userId && typeof userId === 'string') {\n          const match = userId.match(/_session_([a-f0-9-]+)/i);\n          sessionId = match ? match[1] : undefined;\n        }\n      } catch (error) {\n      }\n      if (!sessionId) return;\n\n      // Get tokenizer for this specific request\n      const tokenizer = await getTokenizerForRequest(request);\n\n      // Handle streaming responses\n      if (payload instanceof ReadableStream) {\n        // Mark this request as streaming\n        requestStats.set(requestId, {\n          requestId,\n          sessionId,\n          startTime,\n          lastTokenTime: startTime,\n          tokenCount: 0,\n          tokensPerSecond: 0,\n          tokenTimestamps: [],\n          stream: true\n        });\n\n        // Tee the stream: one for stats, one for the client\n        const [originalStream, statsStream] = payload.tee();\n\n        // Process stats in background\n        const processStats = async () => {\n          let outputTimer: NodeJS.Timeout | null = null;\n\n          // Output stats function - calculate current speed using sliding window\n          const doOutput = async (isFinal: boolean) => {\n            const stats = requestStats.get(requestId);\n            if (!stats) return;\n\n            const now = performance.now();\n\n            if (!isFinal) {\n              // For streaming output, use sliding window: count tokens in last 1 second\n              const oneSecondAgo = now - 1000;\n              stats.tokenTimestamps = stats.tokenTimestamps.filter(ts => ts > oneSecondAgo);\n              stats.tokensPerSecond = stats.tokenTimestamps.length;\n            } else {\n              // For final output, use average speed over entire request\n              const duration = (stats.lastTokenTime - stats.startTime) / 1000; // seconds\n              if (duration > 0) {\n                stats.tokensPerSecond = Math.round(stats.tokenCount / duration);\n              }\n            }\n\n            await outputStats(stats, reporters, opts.outputOptions, isFinal).catch(err => {\n              fastify.log?.warn(`Failed to output streaming stats: ${err.message}`);\n            });\n          };\n\n          try {\n            // Decode byte stream to text, then parse SSE events\n            const eventStream = statsStream\n              .pipeThrough(new TextDecoderStream())\n              .pipeThrough(new SSEParserTransform());\n            const reader = eventStream.getReader();\n\n            // Start timer immediately - output every 1 second\n            outputTimer = setInterval(async () => {\n              const stats = requestStats.get(requestId);\n              if (stats) {\n                await doOutput(false);\n              }\n            }, 1000);\n\n            while (true) {\n              const { done, value } = await reader.read();\n              if (done) break;\n\n              const data = value;\n              const stats = requestStats.get(requestId);\n              if (!stats) continue;\n\n              const now = performance.now();\n\n              // Record first token time when we receive any content-related event\n              // This includes: content_block_start, content_block_delta, text_block\n              if (!stats.firstTokenTime && (\n                data.event === 'content_block_start' ||\n                data.event === 'content_block_delta' ||\n                data.event === 'text_block' ||\n                data.event === 'content_block'\n              )) {\n                stats.firstTokenTime = now;\n                stats.timeToFirstToken = Math.round(now - stats.startTime);\n              }\n\n              // Detect content_block_delta event (incremental tokens)\n              // Support multiple delta types: text_delta, input_json_delta, thinking_delta\n              if (data.event === 'content_block_delta' && data.data?.delta) {\n                const deltaType = data.data.delta.type;\n                let text = '';\n\n                // Extract text based on delta type\n                if (deltaType === 'text_delta') {\n                  text = data.data.delta.text || '';\n                } else if (deltaType === 'input_json_delta') {\n                  text = data.data.delta.partial_json || '';\n                } else if (deltaType === 'thinking_delta') {\n                  text = data.data.delta.thinking || '';\n                }\n\n                // Calculate tokens if we have text content\n                if (text) {\n                  const tokenCount = tokenizer\n                    ? (tokenizer.encodeText ? tokenizer.encodeText(text).length : estimateTokens(text))\n                    : estimateTokens(text);\n\n                  stats.tokenCount += tokenCount;\n                  stats.lastTokenTime = now;\n\n                  // Record timestamps for each token (for sliding window calculation)\n                  for (let i = 0; i < tokenCount; i++) {\n                    stats.tokenTimestamps.push(now);\n                  }\n                }\n              }\n\n              // Output final statistics when message ends\n              if (data.event === 'message_stop') {\n                // Clear timer\n                if (outputTimer) {\n                  clearInterval(outputTimer);\n                  outputTimer = null;\n                }\n\n                await doOutput(true);\n\n                requestStats.delete(requestId);\n              }\n            }\n          } catch (error: any) {\n            // Clean up timer on error\n            if (outputTimer) {\n              clearInterval(outputTimer);\n            }\n            if (error.name !== 'AbortError' && error.code !== 'ERR_STREAM_PREMATURE_CLOSE') {\n              fastify.log?.warn(`Error processing token stats: ${error.message}`);\n            }\n          }\n        };\n\n        // Start background processing without blocking\n        processStats().catch((error) => {\n          console.log(error);\n          fastify.log?.warn(`Background stats processing failed: ${error.message}`);\n        });\n\n        // Return original stream to client\n        return originalStream;\n      }\n\n      // Handle non-streaming responses\n      // Try to extract token count from the response payload\n      const endTime = performance.now();\n      let tokenCount = 0;\n\n      // Payload should be a string or object for non-streaming responses\n      if (payload && typeof payload === 'string') {\n        try {\n          const response = JSON.parse(payload);\n\n          // Prefer usage.output_tokens if available (most accurate)\n          if (response.usage?.output_tokens) {\n            tokenCount = response.usage.output_tokens;\n          } else {\n            // Fallback: calculate from content\n            const content = response.content || response.message?.content || '';\n\n            if (tokenizer) {\n              if (Array.isArray(content)) {\n                tokenCount = content.reduce((sum: number, block: any) => {\n                  if (block.type === 'text') {\n                    const text = block.text || '';\n                    return sum + (tokenizer.encodeText ? tokenizer.encodeText(text).length : estimateTokens(text));\n                  }\n                  return sum;\n                }, 0);\n              } else if (typeof content === 'string') {\n                tokenCount = tokenizer.encodeText ? tokenizer.encodeText(content).length : estimateTokens(content);\n              }\n            } else {\n              const text = Array.isArray(content) ? content.map((c: any) => c.text).join('') : content;\n              tokenCount = estimateTokens(text);\n            }\n          }\n        } catch (error) {\n          // Could not parse or extract tokens\n        }\n      }\n\n      // Only output stats if we found tokens\n      if (tokenCount > 0) {\n        const duration = (endTime - startTime) / 1000; // seconds\n\n        const stats: TokenStats = {\n          requestId,\n          sessionId,\n          startTime,\n          lastTokenTime: endTime,\n          tokenCount,\n          tokensPerSecond: duration > 0 ? Math.round(tokenCount / duration) : 0,\n          timeToFirstToken: Math.round(endTime - startTime),\n          stream: false,\n          tokenTimestamps: []\n        };\n\n        await outputStats(stats, reporters, opts.outputOptions, true);\n      }\n\n      // Return payload as-is\n      return payload;\n    });\n  }),\n};\n\n/**\n * Estimate token count (fallback method)\n */\nfunction estimateTokens(text: string): number {\n  // Rough estimation: English ~4 chars/token, Chinese ~1.5 chars/token\n  const chineseChars = (text.match(/[\\u4e00-\\u9fa5]/g) || []).length;\n  const otherChars = text.length - chineseChars;\n  return Math.ceil(chineseChars / 1.5 + otherChars / 4);\n}\n\n/**\n * Output single request statistics\n */\nasync function outputStats(\n  stats: TokenStats,\n  reporters: string[],\n  options?: OutputOptions,\n  isFinal = false\n) {\n  const prefix = isFinal ? '[Token Speed Final]' : '[Token Speed]';\n\n  const logData = {\n    requestId: stats.requestId.substring(0, 8),\n    sessionId: stats.sessionId,\n    stream: stats.stream,\n    tokenCount: stats.tokenCount,\n    tokensPerSecond: stats.tokensPerSecond,\n    timeToFirstToken: stats.timeToFirstToken ? `${stats.timeToFirstToken}ms` : 'N/A',\n    duration: `${((stats.lastTokenTime - stats.startTime) / 1000).toFixed(2)}s`,\n    timestamp: Date.now()\n  };\n\n  const outputOptions = {\n    prefix,\n    metadata: {\n      sessionId: stats.sessionId\n    },\n    ...options\n  };\n\n  // Output to each specified reporter type\n  for (const reporter of reporters) {\n    try {\n      await outputManager.outputToType(reporter, logData, outputOptions);\n    } catch (error) {\n      console.error(`[TokenSpeedPlugin] Failed to output to ${reporter}:`, error);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/core/src/plugins/types.ts",
    "content": "import { FastifyPluginAsync } from 'fastify';\n\n/**\n * Plugin configuration interface\n */\nexport interface CCRPluginOptions {\n  enabled?: boolean;\n  [key: string]: any;\n}\n\n/**\n * Plugin interface\n */\nexport interface CCRPlugin {\n  name: string;\n  version?: string;\n  description?: string;\n  register: FastifyPluginAsync<CCRPluginOptions>;\n}\n\n/**\n * Plugin metadata\n */\nexport interface PluginMetadata {\n  name: string;\n  enabled: boolean;\n  options?: any;\n}\n"
  },
  {
    "path": "packages/core/src/server.ts",
    "content": "import Fastify, {\n  FastifyInstance,\n  FastifyReply,\n  FastifyRequest,\n  FastifyPluginAsync,\n  FastifyPluginCallback,\n  FastifyPluginOptions,\n  FastifyRegisterOptions,\n  preHandlerHookHandler,\n  onRequestHookHandler,\n  preParsingHookHandler,\n  preValidationHookHandler,\n  preSerializationHookHandler,\n  onSendHookHandler,\n  onResponseHookHandler,\n  onTimeoutHookHandler,\n  onErrorHookHandler,\n  onRouteHookHandler,\n  onRegisterHookHandler,\n  onReadyHookHandler,\n  onListenHookHandler,\n  onCloseHookHandler,\n  FastifyBaseLogger,\n  FastifyLoggerOptions,\n  FastifyServerOptions,\n} from \"fastify\";\nimport cors from \"@fastify/cors\";\nimport { ConfigService, AppConfig } from \"./services/config\";\nimport { errorHandler } from \"./api/middleware\";\nimport { registerApiRoutes } from \"./api/routes\";\nimport { ProviderService } from \"./services/provider\";\nimport { TransformerService } from \"./services/transformer\";\nimport { TokenizerService } from \"./services/tokenizer\";\nimport { router, calculateTokenCount, searchProjectBySession } from \"./utils/router\";\nimport { sessionUsageCache } from \"./utils/cache\";\n\n// Extend FastifyRequest to include custom properties\ndeclare module \"fastify\" {\n  interface FastifyRequest {\n    provider?: string;\n    model?: string;\n    scenarioType?: string;\n  }\n  interface FastifyInstance {\n    _server?: Server;\n  }\n}\n\ninterface ServerOptions extends FastifyServerOptions {\n  initialConfig?: AppConfig;\n}\n\n// Application factory\nfunction createApp(options: FastifyServerOptions = {}): FastifyInstance {\n  const fastify = Fastify({\n    bodyLimit: 50 * 1024 * 1024,\n    ...options,\n  });\n\n  // Register error handler\n  fastify.setErrorHandler(errorHandler);\n\n  // Register CORS\n  fastify.register(cors);\n  return fastify;\n}\n\n// Server class\nclass Server {\n  private app: FastifyInstance;\n  configService: ConfigService;\n  providerService!: ProviderService;\n  transformerService: TransformerService;\n  tokenizerService: TokenizerService;\n\n  constructor(options: ServerOptions = {}) {\n    const { initialConfig, ...fastifyOptions } = options;\n    this.app = createApp({\n      ...fastifyOptions,\n      logger: fastifyOptions.logger ?? true,\n    });\n    this.configService = new ConfigService(options);\n    this.transformerService = new TransformerService(\n      this.configService,\n      this.app.log\n    );\n    this.tokenizerService = new TokenizerService(\n      this.configService,\n      this.app.log\n    );\n    this.transformerService.initialize().finally(() => {\n      this.providerService = new ProviderService(\n        this.configService,\n        this.transformerService,\n        this.app.log\n      );\n    });\n    // Initialize tokenizer service\n    this.tokenizerService.initialize().catch((error) => {\n      this.app.log.error(`Failed to initialize TokenizerService: ${error}`);\n    });\n  }\n\n  async register<Options extends FastifyPluginOptions = FastifyPluginOptions>(\n    plugin: FastifyPluginAsync<Options> | FastifyPluginCallback<Options>,\n    options?: FastifyRegisterOptions<Options>\n  ): Promise<void> {\n    await (this.app as any).register(plugin, options);\n  }\n\n  addHook(hookName: \"onRequest\", hookFunction: onRequestHookHandler): void;\n  addHook(hookName: \"preParsing\", hookFunction: preParsingHookHandler): void;\n  addHook(\n    hookName: \"preValidation\",\n    hookFunction: preValidationHookHandler\n  ): void;\n  addHook(hookName: \"preHandler\", hookFunction: preHandlerHookHandler): void;\n  addHook(\n    hookName: \"preSerialization\",\n    hookFunction: preSerializationHookHandler\n  ): void;\n  addHook(hookName: \"onSend\", hookFunction: onSendHookHandler): void;\n  addHook(hookName: \"onResponse\", hookFunction: onResponseHookHandler): void;\n  addHook(hookName: \"onTimeout\", hookFunction: onTimeoutHookHandler): void;\n  addHook(hookName: \"onError\", hookFunction: onErrorHookHandler): void;\n  addHook(hookName: \"onRoute\", hookFunction: onRouteHookHandler): void;\n  addHook(hookName: \"onRegister\", hookFunction: onRegisterHookHandler): void;\n  addHook(hookName: \"onReady\", hookFunction: onReadyHookHandler): void;\n  addHook(hookName: \"onListen\", hookFunction: onListenHookHandler): void;\n  addHook(hookName: \"onClose\", hookFunction: onCloseHookHandler): void;\n  public addHook(hookName: string, hookFunction: any): void {\n    this.app.addHook(hookName as any, hookFunction);\n  }\n\n  public async registerNamespace(name: string, options?: any) {\n    if (!name) throw new Error(\"name is required\");\n    if (name === '/') {\n      await this.app.register(async (fastify) => {\n        fastify.decorate('configService', this.configService);\n        fastify.decorate('transformerService', this.transformerService);\n        fastify.decorate('providerService', this.providerService);\n        fastify.decorate('tokenizerService', this.tokenizerService);\n        // Add router hook for main namespace\n        fastify.addHook('preHandler', async (req: any, reply: any) => {\n          const url = new URL(`http://127.0.0.1${req.url}`);\n          if (url.pathname.endsWith(\"/v1/messages\")) {\n            await router(req, reply, {\n              configService: this.configService,\n              tokenizerService: this.tokenizerService,\n            });\n          }\n        });\n        await registerApiRoutes(fastify);\n      });\n      return\n    }\n    if (!options) throw new Error(\"options is required\");\n    const configService = new ConfigService({\n      initialConfig: {\n        providers: options.Providers,\n        Router: options.Router,\n      }\n    });\n    const transformerService = new TransformerService(\n      configService,\n      this.app.log\n    );\n    await transformerService.initialize();\n    const providerService = new ProviderService(\n      configService,\n      transformerService,\n      this.app.log\n    );\n    const tokenizerService = new TokenizerService(\n      configService,\n      this.app.log\n    );\n    await tokenizerService.initialize();\n    await this.app.register(async (fastify) => {\n      fastify.decorate('configService', configService);\n      fastify.decorate('transformerService', transformerService);\n      fastify.decorate('providerService', providerService);\n      fastify.decorate('tokenizerService', tokenizerService);\n      // Add router hook for namespace\n      fastify.addHook('preHandler', async (req: any, reply: any) => {\n        const url = new URL(`http://127.0.0.1${req.url}`);\n        if (url.pathname.endsWith(\"/v1/messages\")) {\n          await router(req, reply, {\n            configService,\n            tokenizerService,\n          });\n        }\n      });\n      await registerApiRoutes(fastify);\n    }, { prefix: name });\n  }\n\n  async start(): Promise<void> {\n    try {\n      this.app._server = this;\n\n      this.app.addHook(\"preHandler\", (req, reply, done) => {\n        const url = new URL(`http://127.0.0.1${req.url}`);\n        if (url.pathname.endsWith(\"/v1/messages\") && req.body) {\n          const body = req.body as any;\n          req.log.info({ data: body, type: \"request body\" });\n          if (!body.stream) {\n            body.stream = false;\n          }\n        }\n        done();\n      });\n\n      await this.registerNamespace('/')\n\n      this.app.addHook(\n        \"preHandler\",\n        async (req: FastifyRequest, reply: FastifyReply) => {\n          const url = new URL(`http://127.0.0.1${req.url}`);\n          if (url.pathname.endsWith(\"/v1/messages\") && req.body) {\n            try {\n              const body = req.body as any;\n              if (!body || !body.model) {\n                return reply\n                  .code(400)\n                  .send({ error: \"Missing model in request body\" });\n              }\n              const [provider, ...model] = body.model.split(\",\");\n              body.model = model.join(\",\");\n              req.provider = provider;\n              req.model = model;\n              return;\n            } catch (err) {\n              req.log.error({error: err}, \"Error in modelProviderMiddleware:\");\n              return reply.code(500).send({ error: \"Internal server error\" });\n            }\n          }\n        }\n      );\n\n\n      const address = await this.app.listen({\n        port: parseInt(this.configService.get(\"PORT\") || \"3000\", 10),\n        host: this.configService.get(\"HOST\") || \"127.0.0.1\",\n      });\n\n      this.app.log.info(`🚀 LLMs API server listening on ${address}`);\n\n      const shutdown = async (signal: string) => {\n        this.app.log.info(`Received ${signal}, shutting down gracefully...`);\n        await this.app.close();\n        process.exit(0);\n      };\n\n      process.on(\"SIGINT\", () => shutdown(\"SIGINT\"));\n      process.on(\"SIGTERM\", () => shutdown(\"SIGTERM\"));\n    } catch (error) {\n      this.app.log.error(`Error starting server: ${error}`);\n      process.exit(1);\n    }\n  }\n}\n\n// Export for external use\nexport default Server;\nexport { sessionUsageCache };\nexport { router };\nexport { calculateTokenCount };\nexport { searchProjectBySession };\nexport type { RouterScenarioType, RouterFallbackConfig } from \"./utils/router\";\nexport { ConfigService } from \"./services/config\";\nexport { ProviderService } from \"./services/provider\";\nexport { TransformerService } from \"./services/transformer\";\nexport { TokenizerService } from \"./services/tokenizer\";\nexport { pluginManager, tokenSpeedPlugin, getTokenSpeedStats, getGlobalTokenSpeedStats, CCRPlugin, CCRPluginOptions, PluginMetadata } from \"./plugins\";\nexport { SSEParserTransform, SSESerializerTransform, rewriteStream } from \"./utils/sse\";\n"
  },
  {
    "path": "packages/core/src/services/config.ts",
    "content": "import { readFileSync, existsSync } from \"fs\";\nimport { join } from \"path\";\nimport { config } from \"dotenv\";\nimport JSON5 from 'json5';\n\nexport interface ConfigOptions {\n  envPath?: string;\n  jsonPath?: string;\n  useEnvFile?: boolean;\n  useJsonFile?: boolean;\n  useEnvironmentVariables?: boolean;\n  initialConfig?: AppConfig;\n}\n\nexport interface AppConfig {\n  [key: string]: any;\n}\n\nexport class ConfigService {\n  private config: AppConfig = {};\n  private options: ConfigOptions;\n\n  constructor(\n    options: ConfigOptions = {\n      jsonPath: \"./config.json\",\n    }\n  ) {\n    this.options = {\n      envPath: options.envPath || \".env\",\n      jsonPath: options.jsonPath,\n      useEnvFile: false,\n      useJsonFile: options.useJsonFile !== false,\n      useEnvironmentVariables: options.useEnvironmentVariables !== false,\n      ...options,\n    };\n\n    this.loadConfig();\n  }\n\n  private loadConfig(): void {\n    if (this.options.useJsonFile && this.options.jsonPath) {\n      this.loadJsonConfig();\n    }\n\n    if (this.options.initialConfig) {\n      this.config = { ...this.config, ...this.options.initialConfig };\n    }\n\n    if (this.options.useEnvFile) {\n      this.loadEnvConfig();\n    }\n\n    // if (this.options.useEnvironmentVariables) {\n    //   this.loadEnvironmentVariables();\n    // }\n\n    if (this.config.LOG_FILE) {\n      process.env.LOG_FILE = this.config.LOG_FILE;\n    }\n    if (this.config.LOG) {\n      process.env.LOG = this.config.LOG;\n    }\n  }\n\n  private loadJsonConfig(): void {\n    if (!this.options.jsonPath) return;\n\n    const jsonPath = this.isAbsolutePath(this.options.jsonPath)\n      ? this.options.jsonPath\n      : join(process.cwd(), this.options.jsonPath);\n\n    if (existsSync(jsonPath)) {\n      try {\n        const jsonContent = readFileSync(jsonPath, \"utf-8\");\n        const jsonConfig = JSON5.parse(jsonContent);\n        this.config = { ...this.config, ...jsonConfig };\n        console.log(`Loaded JSON config from: ${jsonPath}`);\n      } catch (error) {\n        console.warn(`Failed to load JSON config from ${jsonPath}:`, error);\n      }\n    } else {\n      console.warn(`JSON config file not found: ${jsonPath}`);\n    }\n  }\n\n  private loadEnvConfig(): void {\n    const envPath = this.isAbsolutePath(this.options.envPath!)\n      ? this.options.envPath!\n      : join(process.cwd(), this.options.envPath!);\n\n    if (existsSync(envPath)) {\n      try {\n        const result = config({ path: envPath });\n        if (result.parsed) {\n          this.config = {\n            ...this.config,\n            ...this.parseEnvConfig(result.parsed),\n          };\n        }\n      } catch (error) {\n        console.warn(`Failed to load .env config from ${envPath}:`, error);\n      }\n    }\n  }\n\n  private loadEnvironmentVariables(): void {\n    const envConfig = this.parseEnvConfig(process.env);\n    this.config = { ...this.config, ...envConfig };\n  }\n\n  private parseEnvConfig(\n    env: Record<string, string | undefined>\n  ): Partial<AppConfig> {\n    const parsed: Partial<AppConfig> = {};\n\n    Object.assign(parsed, env);\n\n    return parsed;\n  }\n\n  private isAbsolutePath(path: string): boolean {\n    return path.startsWith(\"/\") || path.includes(\":\");\n  }\n\n  public get<T = any>(key: keyof AppConfig): T | undefined;\n  public get<T = any>(key: keyof AppConfig, defaultValue: T): T;\n  public get<T = any>(key: keyof AppConfig, defaultValue?: T): T | undefined {\n    const value = this.config[key];\n    return value !== undefined ? (value as T) : defaultValue;\n  }\n\n  public getAll(): AppConfig {\n    return { ...this.config };\n  }\n\n  public getHttpsProxy(): string | undefined {\n    return (\n      this.get(\"HTTPS_PROXY\") ||\n      this.get(\"https_proxy\") ||\n      this.get(\"httpsProxy\") ||\n      this.get(\"PROXY_URL\")\n    );\n  }\n\n  public has(key: keyof AppConfig): boolean {\n    return this.config[key] !== undefined;\n  }\n\n  public set(key: keyof AppConfig, value: any): void {\n    this.config[key] = value;\n  }\n\n  public reload(): void {\n    this.config = {};\n    this.loadConfig();\n  }\n\n  public getConfigSummary(): string {\n    const summary: string[] = [];\n\n    if (this.options.initialConfig) {\n      summary.push(\"Initial Config\");\n    }\n\n    if (this.options.useJsonFile && this.options.jsonPath) {\n      summary.push(`JSON: ${this.options.jsonPath}`);\n    }\n\n    if (this.options.useEnvFile) {\n      summary.push(`ENV: ${this.options.envPath}`);\n    }\n\n    if (this.options.useEnvironmentVariables) {\n      summary.push(\"Environment Variables\");\n    }\n\n    return `Config sources: ${summary.join(\", \")}`;\n  }\n}\n"
  },
  {
    "path": "packages/core/src/services/provider.ts",
    "content": "import { TransformerConstructor } from \"@/types/transformer\";\nimport {\n  LLMProvider,\n  RegisterProviderRequest,\n  ModelRoute,\n  RequestRouteInfo,\n  ConfigProvider,\n} from \"../types/llm\";\nimport { ConfigService } from \"./config\"; \nimport { TransformerService } from \"./transformer\";\n\nexport class ProviderService {\n  private providers: Map<string, LLMProvider> = new Map();\n  private modelRoutes: Map<string, ModelRoute> = new Map();\n\n  constructor(private readonly configService: ConfigService, private readonly transformerService: TransformerService, private readonly logger: any) {\n    this.initializeCustomProviders();\n  }\n\n  private initializeCustomProviders() {\n    const providersConfig =\n      this.configService.get<ConfigProvider[]>(\"providers\");\n    if (providersConfig && Array.isArray(providersConfig)) {\n      this.initializeFromProvidersArray(providersConfig);\n      return;\n    }\n  }\n\n  private initializeFromProvidersArray(providersConfig: ConfigProvider[]) {\n    providersConfig.forEach((providerConfig: ConfigProvider) => {\n      try {\n        if (\n          !providerConfig.name ||\n          !providerConfig.api_base_url ||\n          !providerConfig.api_key\n        ) {\n          return;\n        }\n\n        const transformer: LLMProvider[\"transformer\"] = {}\n\n        if (providerConfig.transformer) {\n          Object.keys(providerConfig.transformer).forEach(key => {\n            if (key === 'use') {\n              if (Array.isArray(providerConfig.transformer.use)) {\n                transformer.use = providerConfig.transformer.use.map((transformer) => {\n                  if (Array.isArray(transformer) && typeof transformer[0] === 'string') {\n                    const Constructor = this.transformerService.getTransformer(transformer[0]);\n                    if (Constructor) {\n                      return new (Constructor as TransformerConstructor)(transformer[1]);\n                    }\n                  }\n                  if (typeof transformer === 'string') {\n                    const transformerInstance = this.transformerService.getTransformer(transformer);\n                    if (typeof transformerInstance === 'function') {\n                      return new transformerInstance();\n                    }\n                    return transformerInstance;\n                  }\n                }).filter((transformer) => typeof transformer !== 'undefined');\n              }\n            } else {\n              if (Array.isArray(providerConfig.transformer[key]?.use)) {\n                transformer[key] = {\n                  use: providerConfig.transformer[key].use.map((transformer) => {\n                    if (Array.isArray(transformer) && typeof transformer[0] === 'string') {\n                      const Constructor = this.transformerService.getTransformer(transformer[0]);\n                      if (Constructor) {\n                        return new (Constructor as TransformerConstructor)(transformer[1]);\n                      }\n                    }\n                    if (typeof transformer === 'string') {\n                      const transformerInstance = this.transformerService.getTransformer(transformer);\n                      if (typeof transformerInstance === 'function') {\n                        return new transformerInstance();\n                      }\n                      return transformerInstance;\n                    }\n                  }).filter((transformer) => typeof transformer !== 'undefined')\n                }\n              }\n            }\n          })\n        }\n\n        this.registerProvider({\n          name: providerConfig.name,\n          baseUrl: providerConfig.api_base_url,\n          apiKey: providerConfig.api_key,\n          models: providerConfig.models || [],\n          transformer: providerConfig.transformer ? transformer : undefined,\n        });\n\n        this.logger.info(`${providerConfig.name} provider registered`);\n      } catch (error) {\n        this.logger.error(`${providerConfig.name} provider registered error: ${error}`);\n      }\n    });\n  }\n\n  registerProvider(request: RegisterProviderRequest): LLMProvider {\n    const provider: LLMProvider = {\n      ...request,\n    };\n\n    this.providers.set(provider.name, provider);\n\n    request.models.forEach((model) => {\n      const fullModel = `${provider.name},${model}`;\n      const route: ModelRoute = {\n        provider: provider.name,\n        model,\n        fullModel,\n      };\n      this.modelRoutes.set(fullModel, route);\n      if (!this.modelRoutes.has(model)) {\n        this.modelRoutes.set(model, route);\n      }\n    });\n\n    return provider;\n  }\n\n  getProviders(): LLMProvider[] {\n    return Array.from(this.providers.values());\n  }\n\n  getProvider(name: string): LLMProvider | undefined {\n    return this.providers.get(name);\n  }\n\n  updateProvider(\n    id: string,\n    updates: Partial<LLMProvider>\n  ): LLMProvider | null {\n    const provider = this.providers.get(id);\n    if (!provider) {\n      return null;\n    }\n\n    const updatedProvider = {\n      ...provider,\n      ...updates,\n      updatedAt: new Date(),\n    };\n\n    this.providers.set(id, updatedProvider);\n\n    if (updates.models) {\n      provider.models.forEach((model) => {\n        const fullModel = `${provider.name},${model}`;\n        this.modelRoutes.delete(fullModel);\n        this.modelRoutes.delete(model);\n      });\n\n      updates.models.forEach((model) => {\n        const fullModel = `${provider.name},${model}`;\n        const route: ModelRoute = {\n          provider: provider.name,\n          model,\n          fullModel,\n        };\n        this.modelRoutes.set(fullModel, route);\n        if (!this.modelRoutes.has(model)) {\n          this.modelRoutes.set(model, route);\n        }\n      });\n    }\n\n    return updatedProvider;\n  }\n\n  deleteProvider(id: string): boolean {\n    const provider = this.providers.get(id);\n    if (!provider) {\n      return false;\n    }\n\n    provider.models.forEach((model) => {\n      const fullModel = `${provider.name},${model}`;\n      this.modelRoutes.delete(fullModel);\n      this.modelRoutes.delete(model);\n    });\n\n    this.providers.delete(id);\n    return true;\n  }\n\n  toggleProvider(name: string, enabled: boolean): boolean {\n    const provider = this.providers.get(name);\n    if (!provider) {\n      return false;\n    }\n    return true;\n  }\n\n  resolveModelRoute(modelName: string): RequestRouteInfo | null {\n    const route = this.modelRoutes.get(modelName);\n    if (!route) {\n      return null;\n    }\n\n    const provider = this.providers.get(route.provider);\n    if (!provider) {\n      return null;\n    }\n\n    return {\n      provider,\n      originalModel: modelName,\n      targetModel: route.model,\n    };\n  }\n\n  getAvailableModelNames(): string[] {\n    const modelNames: string[] = [];\n    this.providers.forEach((provider) => {\n      provider.models.forEach((model) => {\n        modelNames.push(model);\n        modelNames.push(`${provider.name},${model}`);\n      });\n    });\n    return modelNames;\n  }\n\n  getModelRoutes(): ModelRoute[] {\n    return Array.from(this.modelRoutes.values());\n  }\n\n  private parseTransformerConfig(transformerConfig: any): any {\n    if (!transformerConfig) return {};\n\n    if (Array.isArray(transformerConfig)) {\n      return transformerConfig.reduce((acc, item) => {\n        if (Array.isArray(item)) {\n          const [name, config = {}] = item;\n          acc[name] = config;\n        } else {\n          acc[item] = {};\n        }\n        return acc;\n      }, {});\n    }\n\n    return transformerConfig;\n  }\n\n  async getAvailableModels(): Promise<{\n    object: string;\n    data: Array<{\n      id: string;\n      object: string;\n      owned_by: string;\n      provider: string;\n    }>;\n  }> {\n    const models: Array<{\n      id: string;\n      object: string;\n      owned_by: string;\n      provider: string;\n    }> = [];\n\n    this.providers.forEach((provider) => {\n      provider.models.forEach((model) => {\n        models.push({\n          id: model,\n          object: \"model\",\n          owned_by: provider.name,\n          provider: provider.name,\n        });\n\n        models.push({\n          id: `${provider.name},${model}`,\n          object: \"model\",\n          owned_by: provider.name,\n          provider: provider.name,\n        });\n      });\n    });\n\n    return {\n      object: \"list\",\n      data: models,\n    };\n  }\n}\n"
  },
  {
    "path": "packages/core/src/services/tokenizer.ts",
    "content": "import { ConfigService } from \"./config\";\nimport {\n  ITokenizer,\n  TokenizeRequest,\n  TokenizerConfig,\n  TokenizerResult,\n  TokenizerOptions,\n} from \"../types/tokenizer\";\nimport { TiktokenTokenizer } from \"../tokenizer/tiktoken-tokenizer\";\nimport { HuggingFaceTokenizer } from \"../tokenizer/huggingface-tokenizer\";\nimport { ApiTokenizer } from \"../tokenizer/api-tokenizer\";\n\n/**\n * TokenizerService - Manages tokenization for different model types\n *\n * Supports three types of tokenizers:\n * - tiktoken: Fast, OpenAI-compatible tokenizer (default)\n * - huggingface: Local model-based tokenizer for open-source models\n * - api: API-based tokenizer for closed-source models\n *\n * Features:\n * - Automatic fallback to tiktoken on errors\n * - Config-driven tokenizer selection\n * - Per-provider and per-model configuration\n */\nexport class TokenizerService {\n  private tokenizers: Map<string, ITokenizer> = new Map();\n  private configService: ConfigService;\n  private logger: any;\n  private options: TokenizerOptions;\n\n  // Fallback tokenizer (default to tiktoken)\n  private fallbackTokenizer?: ITokenizer;\n\n  constructor(\n    configService: ConfigService,\n    logger: any,\n    options: TokenizerOptions = {}\n  ) {\n    this.configService = configService;\n    this.logger = logger;\n    this.options = {\n      timeout: options.timeout ?? 30000,\n      ...options,\n    };\n  }\n\n  async initialize(): Promise<void> {\n    try {\n      // Initialize fallback tokenizer (tiktoken with cl100k_base)\n      this.fallbackTokenizer = new TiktokenTokenizer(\"cl100k_base\");\n      await this.fallbackTokenizer.initialize();\n      this.tokenizers.set(\"fallback\", this.fallbackTokenizer);\n\n      this.logger?.info(\"TokenizerService initialized successfully\");\n    } catch (error: any) {\n      this.logger?.error(\n        `TokenizerService initialization error: ${error.message}`\n      );\n      throw error;\n    }\n  }\n\n  /**\n   * Get or create a tokenizer for specific configuration\n   */\n  async getTokenizer(config: TokenizerConfig): Promise<ITokenizer> {\n    const cacheKey = this.getCacheKey(config);\n\n    // Check cache first\n    if (this.tokenizers.has(cacheKey)) {\n      return this.tokenizers.get(cacheKey)!;\n    }\n\n    let tokenizer: ITokenizer;\n\n    try {\n      switch (config.type) {\n        case \"tiktoken\":\n          tokenizer = new TiktokenTokenizer(\n            config.encoding || \"cl100k_base\"\n          );\n          break;\n\n        case \"huggingface\":\n          this.logger?.info(`Initializing HuggingFace tokenizer for model: ${config.model}`);\n          tokenizer = new HuggingFaceTokenizer(\n            config.model!,\n            this.logger,\n            { timeout: this.options.timeout }\n          );\n          break;\n\n        case \"api\":\n          tokenizer = new ApiTokenizer(\n            config,\n            this.logger,\n            { timeout: this.options.timeout }\n          );\n          break;\n\n        default:\n          throw new Error(`Unknown tokenizer type: ${config.type}`);\n      }\n\n      this.logger?.info(`Calling initialize() on ${config.type} tokenizer...`);\n      await tokenizer.initialize();\n      this.tokenizers.set(cacheKey, tokenizer);\n\n      this.logger?.info(\n        `Tokenizer initialized successfully: ${config.type} (${cacheKey})`\n      );\n\n      return tokenizer;\n    } catch (error: any) {\n      this.logger?.error(\n        `Failed to initialize ${config.type} tokenizer: ${error.message}`\n      );\n      this.logger?.error(`Error stack: ${error.stack}`);\n\n      // Return fallback tokenizer\n      if (!this.fallbackTokenizer) {\n        await this.initialize();\n      }\n      return this.fallbackTokenizer!;\n    }\n  }\n\n  /**\n   * Count tokens for a request using the specified tokenizer configuration\n   */\n  async countTokens(\n    request: TokenizeRequest,\n    config?: TokenizerConfig\n  ): Promise<TokenizerResult> {\n    // Get appropriate tokenizer\n    const tokenizer = config\n      ? await this.getTokenizer(config)\n      : this.fallbackTokenizer!;\n\n    // Count tokens\n    const tokenCount = await tokenizer.countTokens(request);\n\n    return {\n      tokenCount,\n      tokenizerUsed: tokenizer.name,\n      cached: false,\n    };\n  }\n\n  /**\n   * Get tokenizer configuration for a specific model/provider\n   */\n  getTokenizerConfigForModel(\n    providerName: string,\n    modelName: string\n  ): TokenizerConfig | undefined {\n    const providers = this.configService.get<any[]>(\"providers\") || [];\n    const provider = providers.find((p) => p.name === providerName);\n\n    if (!provider?.tokenizer) {\n      return undefined;\n    }\n\n    // Check model-specific config first\n    if (provider.tokenizer.models?.[modelName]) {\n      return provider.tokenizer.models[modelName];\n    }\n\n    // Fall back to default config\n    return provider.tokenizer.default;\n  }\n\n  /**\n   * Dispose all tokenizers\n   */\n  dispose(): void {\n    this.tokenizers.forEach((tokenizer) => {\n      try {\n        tokenizer.dispose();\n      } catch (error) {\n        this.logger?.error(`Error disposing tokenizer: ${error}`);\n      }\n    });\n    this.tokenizers.clear();\n  }\n\n  /**\n   * Generate cache key from tokenizer config\n   */\n  private getCacheKey(config: TokenizerConfig): string {\n    switch (config.type) {\n      case \"tiktoken\":\n        return `tiktoken:${config.encoding || \"cl100k_base\"}`;\n      case \"huggingface\":\n        return `hf:${config.model}`;\n      case \"api\":\n        return `api:${config.url}`;\n      default:\n        return `unknown:${JSON.stringify(config)}`;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/core/src/services/transformer.ts",
    "content": "import { Transformer, TransformerConstructor } from \"@/types/transformer\";\nimport { ConfigService } from \"./config\";\nimport Transformers from \"@/transformer\";\nimport Module from \"node:module\";\n\ninterface TransformerConfig {\n  transformers: Array<{\n    name: string;\n    type: \"class\" | \"module\";\n    path?: string;\n    options?: any;\n  }>;\n}\n\nexport class TransformerService {\n  private transformers: Map<string, Transformer | TransformerConstructor> =\n    new Map();\n\n  constructor(\n    private readonly configService: ConfigService,\n    private readonly logger: any\n  ) {}\n\n  registerTransformer(name: string, transformer: Transformer): void {\n    this.transformers.set(name, transformer);\n    this.logger.info(\n      `register transformer: ${name}${\n        transformer.endPoint\n          ? ` (endpoint: ${transformer.endPoint})`\n          : \" (no endpoint)\"\n      }`\n    );\n  }\n\n  getTransformer(\n    name: string\n  ): Transformer | TransformerConstructor | undefined {\n    return this.transformers.get(name);\n  }\n\n  getAllTransformers(): Map<string, Transformer | TransformerConstructor> {\n    return new Map(this.transformers);\n  }\n\n  getTransformersWithEndpoint(): { name: string; transformer: Transformer }[] {\n    const result: { name: string; transformer: Transformer }[] = [];\n\n    this.transformers.forEach((transformer, name) => {\n      // Check if it's an instance with endPoint\n      if (typeof transformer === 'object' && transformer.endPoint) {\n        result.push({ name, transformer });\n      }\n    });\n\n    return result;\n  }\n\n  getTransformersWithoutEndpoint(): {\n    name: string;\n    transformer: Transformer;\n  }[] {\n    const result: { name: string; transformer: Transformer }[] = [];\n\n    this.transformers.forEach((transformer, name) => {\n      // Check if it's an instance without endPoint\n      if (typeof transformer === 'object' && !transformer.endPoint) {\n        result.push({ name, transformer });\n      }\n    });\n\n    return result;\n  }\n\n  removeTransformer(name: string): boolean {\n    return this.transformers.delete(name);\n  }\n\n  hasTransformer(name: string): boolean {\n    return this.transformers.has(name);\n  }\n\n  async registerTransformerFromConfig(config: {\n    path?: string;\n    options?: any;\n  }): Promise<boolean> {\n    try {\n      if (config.path) {\n        const module = require(require.resolve(config.path));\n        if (module) {\n          const instance = new module(config.options);\n          // Set logger for transformer instance\n          if (instance && typeof instance === \"object\") {\n            (instance as any).logger = this.logger;\n          }\n          if (!instance.name) {\n            throw new Error(\n              `Transformer instance from ${config.path} does not have a name property.`\n            );\n          }\n          this.registerTransformer(instance.name, instance);\n          return true;\n        }\n      }\n      return false;\n    } catch (error: any) {\n      this.logger.error(\n        `load transformer (${config.path}) \\nerror: ${error.message}\\nstack: ${error.stack}`\n      );\n      return false;\n    }\n  }\n\n  async initialize(): Promise<void> {\n    try {\n      await this.registerDefaultTransformersInternal();\n      await this.loadFromConfig();\n    } catch (error: any) {\n      this.logger.error(\n        `TransformerService init error: ${error.message}\\nStack: ${error.stack}`\n      );\n    }\n  }\n\n  private async registerDefaultTransformersInternal(): Promise<void> {\n    try {\n      Object.values(Transformers).forEach(\n        (TransformerStatic: any) => {\n          if (\n            \"TransformerName\" in TransformerStatic &&\n            typeof TransformerStatic.TransformerName === \"string\"\n          ) {\n            this.registerTransformer(\n              TransformerStatic.TransformerName,\n              TransformerStatic\n            );\n          } else {\n            const transformerInstance = new TransformerStatic();\n            // Set logger for transformer instance\n            if (\n              transformerInstance &&\n              typeof transformerInstance === \"object\"\n            ) {\n              (transformerInstance as any).logger = this.logger;\n            }\n            this.registerTransformer(\n              transformerInstance.name!,\n              transformerInstance\n            );\n          }\n        }\n      );\n    } catch (error) {\n      this.logger.error({ error }, \"transformer regist error:\");\n    }\n  }\n\n  private async loadFromConfig(): Promise<void> {\n    const transformers = this.configService.get<\n      TransformerConfig[\"transformers\"]\n    >(\"transformers\", []);\n    for (const transformer of transformers) {\n      await this.registerTransformerFromConfig(transformer);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/core/src/tokenizer/api-tokenizer.ts",
    "content": "import {\n  ITokenizer,\n  TokenizeRequest,\n  TokenizerConfig,\n  ApiRequestFormat,\n} from \"../types/tokenizer\";\n\n/**\n * Options for API tokenizer\n */\ninterface ApiTokenizerOptions {\n  timeout?: number;\n}\n\n/**\n * API-based tokenizer implementation\n * Calls provider's tokenization API to get token counts\n * Supports flexible configuration for different API formats\n */\nexport class ApiTokenizer implements ITokenizer {\n  readonly type = \"api\";\n  readonly name: string;\n  private config: Required<Pick<TokenizerConfig, 'url' | 'apiKey' | 'requestFormat' | 'responseField'>> & {\n    headers: Record<string, string>;\n  };\n  private logger: any;\n  private options: ApiTokenizerOptions;\n\n  constructor(\n    config: TokenizerConfig,\n    logger: any,\n    options: ApiTokenizerOptions = {}\n  ) {\n    if (!config.url || !config.apiKey) {\n      throw new Error(\"API tokenizer requires url and apiKey\");\n    }\n\n    this.config = {\n      url: config.url,\n      apiKey: config.apiKey,\n      requestFormat: config.requestFormat || \"standard\",\n      responseField: config.responseField || \"token_count\",\n      headers: config.headers || {},\n    };\n    this.logger = logger;\n    this.options = options;\n\n    try {\n      const url = new URL(config.url);\n      this.name = `api-${url.hostname}`;\n    } catch {\n      this.name = `api-${config.url}`;\n    }\n  }\n\n  async initialize(): Promise<void> {\n    // API tokenizers don't need initialization\n    // Just verify the URL is valid\n    try {\n      new URL(this.config.url);\n    } catch (error) {\n      throw new Error(`Invalid API URL: ${this.config.url}`);\n    }\n  }\n\n  async countTokens(request: TokenizeRequest): Promise<number> {\n    try {\n      // Prepare request body based on format\n      const requestBody = this.formatRequestBody(request);\n\n      // Prepare headers\n      const headers = {\n        \"Content-Type\": \"application/json\",\n        \"Authorization\": `Bearer ${this.config.apiKey}`,\n        ...this.config.headers,\n      };\n\n      // Make API call\n      const controller = new AbortController();\n      const timeoutId = setTimeout(\n        () => controller.abort(),\n        this.options.timeout || 30000\n      );\n\n      const response = await fetch(this.config.url, {\n        method: \"POST\",\n        headers,\n        body: JSON.stringify(requestBody),\n        signal: controller.signal,\n      });\n\n      clearTimeout(timeoutId);\n\n      if (!response.ok) {\n        throw new Error(\n          `API tokenizer request failed: ${response.status} ${response.statusText}`\n        );\n      }\n\n      const data = await response.json();\n\n      // Extract token count from response\n      const tokenCount = this.extractTokenCount(data);\n\n      return tokenCount;\n    } catch (error: any) {\n      if (error.name === \"AbortError\") {\n        throw new Error(\"API tokenizer request timed out\");\n      }\n      throw error;\n    }\n  }\n\n  isInitialized(): boolean {\n    return true;\n  }\n\n  dispose(): void {\n    // Nothing to dispose for API tokenizer\n  }\n\n  /**\n   * Format request body based on requestFormat\n   */\n  private formatRequestBody(request: TokenizeRequest): any {\n    switch (this.config.requestFormat) {\n      case \"standard\":\n        // Standard format: send the entire request\n        return request;\n\n      case \"openai\":\n        // OpenAI format: extract text content and format as OpenAI request\n        return {\n          model: \"gpt-3.5-turbo\", // Placeholder, some APIs require this\n          messages: this.extractMessagesAsOpenAIFormat(request),\n        };\n\n      case \"anthropic\":\n        // Anthropic format: extract messages and tools\n        return {\n          messages: request.messages || [],\n          system: request.system,\n          tools: request.tools,\n        };\n\n      case \"custom\":\n        // Custom format: send concatenated text\n        return {\n          text: this.extractConcatenatedText(request),\n        };\n\n      default:\n        return request;\n    }\n  }\n\n  /**\n   * Extract messages in OpenAI format\n   */\n  private extractMessagesAsOpenAIFormat(request: TokenizeRequest): any[] {\n    if (!request.messages) return [];\n\n    return request.messages.map((msg) => ({\n      role: msg.role,\n      content: this.extractTextFromMessage(msg),\n    }));\n  }\n\n  /**\n   * Extract text from a message\n   */\n  private extractTextFromMessage(message: any): string {\n    if (typeof message.content === \"string\") {\n      return message.content;\n    }\n\n    if (Array.isArray(message.content)) {\n      return message.content\n        .map((part: any) => {\n          if (part.type === \"text\" && part.text) {\n            return part.text;\n          } else if (part.type === \"tool_use\" && part.input) {\n            return JSON.stringify(part.input);\n          } else if (part.type === \"tool_result\") {\n            return typeof part.content === \"string\"\n              ? part.content\n              : JSON.stringify(part.content);\n          }\n          return \"\";\n        })\n        .join(\" \");\n    }\n\n    return \"\";\n  }\n\n  /**\n   * Extract all text from request\n   */\n  private extractConcatenatedText(request: TokenizeRequest): string {\n    const parts: string[] = [];\n\n    // Extract messages\n    if (request.messages) {\n      request.messages.forEach((msg) => {\n        parts.push(this.extractTextFromMessage(msg));\n      });\n    }\n\n    // Extract system\n    if (typeof request.system === \"string\") {\n      parts.push(request.system);\n    } else if (Array.isArray(request.system)) {\n      request.system.forEach((item: any) => {\n        if (item.type === \"text\") {\n          if (typeof item.text === \"string\") {\n            parts.push(item.text);\n          } else if (Array.isArray(item.text)) {\n            item.text.forEach((textPart: any) => {\n              if (textPart) parts.push(textPart);\n            });\n          }\n        }\n      });\n    }\n\n    // Extract tools\n    if (request.tools) {\n      request.tools.forEach((tool) => {\n        if (tool.name) parts.push(tool.name);\n        if (tool.description) parts.push(tool.description);\n        if (tool.input_schema) parts.push(JSON.stringify(tool.input_schema));\n      });\n    }\n\n    return parts.join(\" \");\n  }\n\n  /**\n   * Extract token count from response using the configured field path\n   */\n  private extractTokenCount(data: any): number {\n    try {\n      const fieldPath = this.config.responseField;\n      const parts = fieldPath.split(\".\");\n\n      let value: any = data;\n      for (const part of parts) {\n        if (value === undefined || value === null) {\n          throw new Error(`Field path '${fieldPath}' not found in response`);\n        }\n        value = value[part];\n      }\n\n      if (typeof value !== \"number\") {\n        throw new Error(\n          `Expected number at field path '${fieldPath}', got ${typeof value}`\n        );\n      }\n\n      return value;\n    } catch (error: any) {\n      this.logger?.error(\n        `Failed to extract token count from API response: ${error.message}. Response: ${JSON.stringify(data)}`\n      );\n      throw new Error(\n        `Invalid response from API tokenizer: ${error.message}`\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "packages/core/src/tokenizer/huggingface-tokenizer.ts",
    "content": "import { join } from \"path\";\nimport { homedir } from \"os\";\nimport { existsSync, mkdirSync } from \"fs\";\nimport { promises as fs } from \"fs\";\nimport {\n  ITokenizer,\n  TokenizeRequest,\n} from \"../types/tokenizer\";\nimport { Tokenizer } from \"@huggingface/tokenizers\";\n\n/**\n * Options for HuggingFace tokenizer\n */\ninterface HFTokenizerOptions {\n  timeout?: number;\n  cacheDir?: string;\n}\n\n/**\n * HuggingFace tokenizer implementation\n * Uses @huggingface/tokenizers library for lightweight tokenization\n */\nexport class HuggingFaceTokenizer implements ITokenizer {\n  readonly type = \"huggingface\";\n  readonly name: string;\n  private readonly modelId: string;\n  private readonly logger: any;\n  private readonly options: HFTokenizerOptions;\n  private tokenizer: any = null;\n  private readonly cacheDir: string;\n  private readonly safeModelName: string;\n\n  constructor(modelId: string, logger: any, options: HFTokenizerOptions = {}) {\n    this.modelId = modelId;\n    this.logger = logger;\n    this.options = options;\n    this.cacheDir = options.cacheDir || join(homedir(), \".claude-code-router\", \".huggingface\");\n    // Cache safe model name to avoid repeated regex operations\n    this.safeModelName = modelId.replace(/\\//g, \"_\").replace(/[^a-zA-Z0-9_-]/g, \"_\");\n    this.name = `huggingface-${modelId.split(\"/\").pop()}`;\n  }\n\n  /**\n   * Get cache file paths for tokenizer files\n   */\n  private getCachePaths() {\n    const modelDir = join(this.cacheDir, this.safeModelName);\n    return {\n      modelDir,\n      tokenizerJson: join(modelDir, \"tokenizer.json\"),\n      tokenizerConfig: join(modelDir, \"tokenizer_config.json\"),\n    };\n  }\n\n  /**\n   * Ensure directory exists\n   */\n  private ensureDir(dir: string): void {\n    if (!existsSync(dir)) {\n      mkdirSync(dir, { recursive: true });\n    }\n  }\n\n  /**\n   * Load tokenizer files from local cache\n   */\n  private async loadFromCache(): Promise<{ tokenizerJson: any; tokenizerConfig: any } | null> {\n    try {\n      const paths = this.getCachePaths();\n\n      if (!existsSync(paths.tokenizerJson) || !existsSync(paths.tokenizerConfig)) {\n        return null;\n      }\n\n      const [tokenizerJsonContent, tokenizerConfigContent] = await Promise.all([\n        fs.readFile(paths.tokenizerJson, \"utf-8\"),\n        fs.readFile(paths.tokenizerConfig, \"utf-8\"),\n      ]);\n\n      return {\n        tokenizerJson: JSON.parse(tokenizerJsonContent),\n        tokenizerConfig: JSON.parse(tokenizerConfigContent),\n      };\n    } catch (error: any) {\n      this.logger?.warn(`Failed to load from cache: ${error.message}`);\n      return null;\n    }\n  }\n\n  /**\n   * Download tokenizer files from Hugging Face Hub and save to cache\n   */\n  private async downloadAndCache(): Promise<{ tokenizerJson: any; tokenizerConfig: any }> {\n    const paths = this.getCachePaths();\n    const urls = {\n      json: `https://huggingface.co/${this.modelId}/resolve/main/tokenizer.json`,\n      config: `https://huggingface.co/${this.modelId}/resolve/main/tokenizer_config.json`,\n    };\n\n    this.logger?.info(`Downloading tokenizer files for ${this.modelId}`);\n\n    const controller = new AbortController();\n    const timeoutId = setTimeout(() => controller.abort(), this.options.timeout || 30000);\n\n    try {\n      const [jsonRes, configRes] = await Promise.all([\n        fetch(urls.json, { signal: controller.signal }),\n        fetch(urls.config, { signal: controller.signal }),\n      ]);\n\n      if (!jsonRes.ok) {\n        throw new Error(`Failed to fetch tokenizer.json: ${jsonRes.statusText}`);\n      }\n\n      const [tokenizerJson, tokenizerConfig] = await Promise.all([\n        jsonRes.json(),\n        configRes.ok ? configRes.json() : Promise.resolve({}),\n      ]);\n\n      this.ensureDir(paths.modelDir);\n      await Promise.all([\n        fs.writeFile(paths.tokenizerJson, JSON.stringify(tokenizerJson, null, 2)),\n        fs.writeFile(paths.tokenizerConfig, JSON.stringify(tokenizerConfig, null, 2)),\n      ]);\n\n      return { tokenizerJson, tokenizerConfig };\n    } finally {\n      clearTimeout(timeoutId);\n    }\n  }\n\n  async initialize(): Promise<void> {\n    try {\n      this.logger?.info(`Initializing HuggingFace tokenizer: ${this.modelId}`);\n\n      const paths = this.getCachePaths();\n      this.ensureDir(this.cacheDir);\n\n      const tokenizerData = await this.loadFromCache() || await this.downloadAndCache();\n      this.tokenizer = new Tokenizer(tokenizerData.tokenizerJson, tokenizerData.tokenizerConfig);\n\n      this.logger?.info(`Tokenizer initialized: ${this.name}`);\n    } catch (error: any) {\n      this.logger?.error(`Failed to initialize tokenizer: ${error.message}`);\n      throw new Error(`Failed to initialize HuggingFace tokenizer for ${this.modelId}: ${error.message}`);\n    }\n  }\n\n  async countTokens(request: TokenizeRequest): Promise<number> {\n    if (!this.tokenizer) {\n      throw new Error(\"Tokenizer not initialized\");\n    }\n\n    try {\n      const text = this.extractTextFromRequest(request);\n      return this.tokenizer.encode(text).ids.length;\n    } catch (error: any) {\n      this.logger?.error(`Error counting tokens: ${error.message}`);\n      throw error;\n    }\n  }\n\n  isInitialized(): boolean {\n    return this.tokenizer !== null;\n  }\n\n  /**\n   * Encode text to tokens (for simple text tokenization)\n   */\n  encodeText(text: string): number[] {\n    if (!this.tokenizer) {\n      throw new Error(\"Tokenizer not initialized\");\n    }\n    return this.tokenizer.encode(text).ids;\n  }\n\n  dispose(): void {\n    this.tokenizer = null;\n  }\n\n  /**\n   * Extract text from tokenize request\n   */\n  private extractTextFromRequest(request: TokenizeRequest): string {\n    const parts: string[] = [];\n    const { messages, system, tools } = request;\n\n    // Extract messages\n    if (Array.isArray(messages)) {\n      for (const message of messages) {\n        if (typeof message.content === \"string\") {\n          parts.push(message.content);\n        } else if (Array.isArray(message.content)) {\n          for (const contentPart of message.content) {\n            if (contentPart.type === \"text\" && contentPart.text) {\n              parts.push(contentPart.text);\n            } else if (contentPart.type === \"tool_use\" && contentPart.input) {\n              parts.push(JSON.stringify(contentPart.input));\n            } else if (contentPart.type === \"tool_result\") {\n              parts.push(\n                typeof contentPart.content === \"string\"\n                  ? contentPart.content\n                  : JSON.stringify(contentPart.content)\n              );\n            }\n          }\n        }\n      }\n    }\n\n    // Extract system\n    if (typeof system === \"string\") {\n      parts.push(system);\n    } else if (Array.isArray(system)) {\n      for (const item of system) {\n        if (item.type === \"text\") {\n          if (typeof item.text === \"string\") {\n            parts.push(item.text);\n          } else if (Array.isArray(item.text)) {\n            for (const textPart of item.text) {\n              if (textPart) parts.push(textPart);\n            }\n          }\n        }\n      }\n    }\n\n    // Extract tools\n    if (tools) {\n      for (const tool of tools) {\n        if (tool.name) parts.push(tool.name);\n        if (tool.description) parts.push(tool.description);\n        if (tool.input_schema) parts.push(JSON.stringify(tool.input_schema));\n      }\n    }\n\n    return parts.join(\" \");\n  }\n}\n"
  },
  {
    "path": "packages/core/src/tokenizer/tiktoken-tokenizer.ts",
    "content": "import { get_encoding, Tiktoken, TiktokenEncoding } from \"tiktoken\";\nimport type { ITokenizer, TokenizeRequest } from \"../types/tokenizer\";\n\n/**\n * Tiktoken-based tokenizer implementation\n * Uses tiktoken library for fast token counting (OpenAI compatible)\n */\nexport class TiktokenTokenizer implements ITokenizer {\n  readonly type = \"tiktoken\";\n  readonly name: string;\n  private encoding?: Tiktoken;\n\n  constructor(encodingName: TiktokenEncoding = \"cl100k_base\") {\n    this.name = `tiktoken-${encodingName}`;\n    try {\n      this.encoding = get_encoding(encodingName);\n    } catch (error) {\n      throw new Error(`Failed to initialize tiktoken encoding: ${encodingName}`);\n    }\n  }\n\n  async initialize(): Promise<void> {\n    // Encoding is already initialized in constructor\n    if (!this.encoding) {\n      throw new Error(\"Tiktoken encoding not initialized\");\n    }\n  }\n\n  async countTokens(request: TokenizeRequest): Promise<number> {\n    const encoding = this.encoding;\n    if (!encoding) {\n      throw new Error(\"Encoding not initialized\");\n    }\n\n    let tokenCount = 0;\n    const { messages, system, tools } = request;\n\n    // Count messages\n    if (Array.isArray(messages)) {\n      messages.forEach((message) => {\n        if (typeof message.content === \"string\") {\n          tokenCount += encoding.encode(message.content).length;\n        } else if (Array.isArray(message.content)) {\n          message.content.forEach((contentPart: any) => {\n            if (contentPart.type === \"text\") {\n              tokenCount += encoding.encode(contentPart.text).length;\n            } else if (contentPart.type === \"tool_use\") {\n              tokenCount += encoding.encode(\n                JSON.stringify(contentPart.input)\n              ).length;\n            } else if (contentPart.type === \"tool_result\") {\n              const content =\n                typeof contentPart.content === \"string\"\n                  ? contentPart.content\n                  : JSON.stringify(contentPart.content);\n              tokenCount += encoding.encode(content).length;\n            }\n          });\n        }\n      });\n    }\n\n    // Count system\n    if (typeof system === \"string\") {\n      tokenCount += encoding.encode(system).length;\n    } else if (Array.isArray(system)) {\n      system.forEach((item: any) => {\n        if (item.type !== \"text\") return;\n        if (typeof item.text === \"string\") {\n          tokenCount += encoding.encode(item.text).length;\n        } else if (Array.isArray(item.text)) {\n          item.text.forEach((textPart: any) => {\n            tokenCount += encoding.encode(textPart || \"\").length;\n          });\n        }\n      });\n    }\n\n    // Count tools\n    if (tools) {\n      tools.forEach((tool: any) => {\n        if (tool.description) {\n          tokenCount += encoding.encode(\n            tool.name + tool.description\n          ).length;\n        }\n        if (tool.input_schema) {\n          tokenCount += encoding.encode(\n            JSON.stringify(tool.input_schema)\n          ).length;\n        }\n      });\n    }\n\n    return tokenCount;\n  }\n\n  isInitialized(): boolean {\n    return this.encoding !== undefined;\n  }\n\n  /**\n   * Encode text to tokens (for simple text tokenization)\n   */\n  encodeText(text: string): number[] {\n    const encoding = this.encoding;\n    if (!encoding) {\n      throw new Error(\"Encoding not initialized\");\n    }\n    return Array.from(encoding.encode(text));\n  }\n\n  dispose(): void {\n    if (this.encoding) {\n      this.encoding.free();\n      this.encoding = undefined;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/core/src/transformer/anthropic.transformer.ts",
    "content": "import { ChatCompletion } from \"openai/resources\";\nimport {\n  LLMProvider,\n  UnifiedChatRequest,\n  UnifiedMessage,\n  UnifiedTool,\n} from \"@/types/llm\";\nimport {\n  Transformer,\n  TransformerContext,\n  TransformerOptions,\n} from \"@/types/transformer\";\nimport { v4 as uuidv4 } from \"uuid\";\nimport { getThinkLevel } from \"@/utils/thinking\";\nimport { createApiError } from \"@/api/middleware\";\nimport { formatBase64 } from \"@/utils/image\";\n\nexport class AnthropicTransformer implements Transformer {\n  name = \"Anthropic\";\n  endPoint = \"/v1/messages\";\n  private useBearer: boolean;\n  logger?: any;\n\n  constructor(private readonly options?: TransformerOptions) {\n    this.useBearer = this.options?.UseBearer ?? false;\n  }\n\n  async auth(request: any, provider: LLMProvider): Promise<any> {\n    const headers: Record<string, string | undefined> = {};\n\n    if (this.useBearer) {\n      headers[\"authorization\"] = `Bearer ${provider.apiKey}`;\n      headers[\"x-api-key\"] = undefined;\n    } else {\n      headers[\"x-api-key\"] = provider.apiKey;\n      headers[\"authorization\"] = undefined;\n    }\n\n    return {\n      body: request,\n      config: {\n        headers,\n      },\n    };\n  }\n\n  async transformRequestOut(\n    request: Record<string, any>\n  ): Promise<UnifiedChatRequest> {\n    const messages: UnifiedMessage[] = [];\n\n    if (request.system) {\n      if (typeof request.system === \"string\") {\n        messages.push({\n          role: \"system\",\n          content: request.system,\n        });\n      } else if (Array.isArray(request.system) && request.system.length) {\n        const textParts = request.system\n          .filter((item: any) => item.type === \"text\" && item.text)\n          .map((item: any) => ({\n            type: \"text\" as const,\n            text: item.text,\n            cache_control: item.cache_control,\n          }));\n        messages.push({\n          role: \"system\",\n          content: textParts,\n        });\n      }\n    }\n\n    const requestMessages = JSON.parse(JSON.stringify(request.messages || []));\n\n    requestMessages?.forEach((msg: any) => {\n      if (msg.role === \"user\" || msg.role === \"assistant\") {\n        if (typeof msg.content === \"string\") {\n          messages.push({\n            role: msg.role,\n            content: msg.content,\n          });\n          return;\n        }\n\n        if (Array.isArray(msg.content)) {\n          if (msg.role === \"user\") {\n            const toolParts = msg.content.filter(\n              (c: any) => c.type === \"tool_result\" && c.tool_use_id\n            );\n            if (toolParts.length) {\n              toolParts.forEach((tool: any) => {\n                const toolMessage: UnifiedMessage = {\n                  role: \"tool\",\n                  content:\n                    typeof tool.content === \"string\"\n                      ? tool.content\n                      : JSON.stringify(tool.content),\n                  tool_call_id: tool.tool_use_id,\n                  cache_control: tool.cache_control,\n                };\n                messages.push(toolMessage);\n              });\n            }\n\n            const textAndMediaParts = msg.content.filter(\n              (c: any) =>\n                (c.type === \"text\" && c.text) ||\n                (c.type === \"image\" && c.source)\n            );\n            if (textAndMediaParts.length) {\n              messages.push({\n                role: \"user\",\n                content: textAndMediaParts.map((part: any) => {\n                  if (part?.type === \"image\") {\n                    return {\n                      type: \"image_url\",\n                      image_url: {\n                        url:\n                          part.source?.type === \"base64\"\n                            ? formatBase64(\n                                part.source.data,\n                                part.source.media_type\n                              )\n                            : part.source.url,\n                      },\n                      media_type: part.source.media_type,\n                    };\n                  }\n                  return part;\n                }),\n              });\n            }\n          } else if (msg.role === \"assistant\") {\n            const assistantMessage: UnifiedMessage = {\n              role: \"assistant\",\n              content: \"\",\n            };\n            const textParts = msg.content.filter(\n              (c: any) => c.type === \"text\" && c.text\n            );\n            if (textParts.length) {\n              assistantMessage.content = textParts\n                .map((text: any) => text.text)\n                .join(\"\\n\");\n            }\n\n            const toolCallParts = msg.content.filter(\n              (c: any) => c.type === \"tool_use\" && c.id\n            );\n            if (toolCallParts.length) {\n              assistantMessage.tool_calls = toolCallParts.map((tool: any) => {\n                return {\n                  id: tool.id,\n                  type: \"function\" as const,\n                  function: {\n                    name: tool.name,\n                    arguments: JSON.stringify(tool.input || {}),\n                  },\n                };\n              });\n            }\n\n            const thinkingPart = msg.content.find(\n              (c: any) => c.type === \"thinking\" && c.signature\n            );\n            if (thinkingPart) {\n              assistantMessage.thinking = {\n                content: thinkingPart.thinking,\n                signature: thinkingPart.signature,\n              };\n            }\n\n            messages.push(assistantMessage);\n          }\n          return;\n        }\n      }\n    });\n\n    const result: UnifiedChatRequest = {\n      messages,\n      model: request.model,\n      max_tokens: request.max_tokens,\n      temperature: request.temperature,\n      stream: request.stream,\n      tools: request.tools?.length\n        ? this.convertAnthropicToolsToUnified(request.tools)\n        : undefined,\n      tool_choice: request.tool_choice,\n    };\n    if (request.thinking) {\n      result.reasoning = {\n        effort: getThinkLevel(request.thinking.budget_tokens),\n        // max_tokens: request.thinking.budget_tokens,\n        enabled: request.thinking.type === \"enabled\",\n      };\n    }\n    if (request.tool_choice) {\n      if (request.tool_choice.type === \"tool\") {\n        result.tool_choice = {\n          type: \"function\",\n          function: { name: request.tool_choice.name },\n        };\n      } else {\n        result.tool_choice = request.tool_choice.type;\n      }\n    }\n    return result;\n  }\n\n  async transformResponseIn(\n    response: Response,\n    context?: TransformerContext\n  ): Promise<Response> {\n    const isStream = response.headers\n      .get(\"Content-Type\")\n      ?.includes(\"text/event-stream\");\n    if (isStream) {\n      if (!response.body) {\n        throw new Error(\"Stream response body is null\");\n      }\n      const convertedStream = await this.convertOpenAIStreamToAnthropic(\n        response.body,\n        context!\n      );\n      return new Response(convertedStream, {\n        headers: {\n          \"Content-Type\": \"text/event-stream\",\n          \"Cache-Control\": \"no-cache\",\n          Connection: \"keep-alive\",\n        },\n      });\n    } else {\n      const data = (await response.json()) as any;\n      const anthropicResponse = this.convertOpenAIResponseToAnthropic(\n        data,\n        context!\n      );\n      return new Response(JSON.stringify(anthropicResponse), {\n        headers: { \"Content-Type\": \"application/json\" },\n      });\n    }\n  }\n\n  private convertAnthropicToolsToUnified(tools: any[]): UnifiedTool[] {\n    return tools.map((tool) => ({\n      type: \"function\",\n      function: {\n        name: tool.name,\n        description: tool.description || \"\",\n        parameters: tool.input_schema,\n      },\n    }));\n  }\n\n  private async convertOpenAIStreamToAnthropic(\n    openaiStream: ReadableStream,\n    context: TransformerContext\n  ): Promise<ReadableStream> {\n    const readable = new ReadableStream({\n      start: async (controller) => {\n        const encoder = new TextEncoder();\n        const messageId = `msg_${Date.now()}`;\n        let stopReasonMessageDelta: null | Record<string, any> = null;\n        let model = \"unknown\";\n        let hasStarted = false;\n        let hasTextContentStarted = false;\n        let hasFinished = false;\n        const toolCalls = new Map<number, any>();\n        const toolCallIndexToContentBlockIndex = new Map<number, number>();\n        let totalChunks = 0;\n        let contentChunks = 0;\n        let toolCallChunks = 0;\n        let isClosed = false;\n        let isThinkingStarted = false;\n        let contentIndex = 0;\n        let currentContentBlockIndex = -1; // Track the current content block index\n\n        // 原子性的content block index分配函数\n        const assignContentBlockIndex = (): number => {\n          const currentIndex = contentIndex;\n          contentIndex++;\n          return currentIndex;\n        };\n\n        const safeEnqueue = (data: Uint8Array) => {\n          if (!isClosed) {\n            try {\n              controller.enqueue(data);\n              const dataStr = new TextDecoder().decode(data);\n              this.logger.debug({\n                reqId: context.req.id,\n                data: dataStr,\n                type: \"send data\",\n              });\n            } catch (error) {\n              if (\n                error instanceof TypeError &&\n                error.message.includes(\"Controller is already closed\")\n              ) {\n                isClosed = true;\n              } else {\n                this.logger.debug({\n                  reqId: context.req.id,\n                  error: error instanceof Error ? error.message : String(error),\n                  type: \"send data error\",\n                });\n                throw error;\n              }\n            }\n          }\n        };\n\n        const safeClose = () => {\n          if (!isClosed) {\n            try {\n              // Close any remaining open content block\n              if (currentContentBlockIndex >= 0) {\n                const contentBlockStop = {\n                  type: \"content_block_stop\",\n                  index: currentContentBlockIndex,\n                };\n                safeEnqueue(\n                  encoder.encode(\n                    `event: content_block_stop\\ndata: ${JSON.stringify(\n                      contentBlockStop\n                    )}\\n\\n`\n                  )\n                );\n                currentContentBlockIndex = -1;\n              }\n\n              if (stopReasonMessageDelta) {\n                safeEnqueue(\n                  encoder.encode(\n                    `event: message_delta\\ndata: ${JSON.stringify(\n                      stopReasonMessageDelta\n                    )}\\n\\n`\n                  )\n                );\n                stopReasonMessageDelta = null;\n              } else {\n                safeEnqueue(\n                  encoder.encode(\n                    `event: message_delta\\ndata: ${JSON.stringify({\n                      type: \"message_delta\",\n                      delta: {\n                        stop_reason: \"end_turn\",\n                        stop_sequence: null,\n                      },\n                      usage: {\n                        input_tokens: 0,\n                        output_tokens: 0,\n                        cache_read_input_tokens: 0,\n                      },\n                    })}\\n\\n`\n                  )\n                );\n              }\n              const messageStop = {\n                type: \"message_stop\",\n              };\n              safeEnqueue(\n                encoder.encode(\n                  `event: message_stop\\ndata: ${JSON.stringify(\n                    messageStop\n                  )}\\n\\n`\n                )\n              );\n              controller.close();\n              isClosed = true;\n            } catch (error) {\n              if (\n                error instanceof TypeError &&\n                error.message.includes(\"Controller is already closed\")\n              ) {\n                isClosed = true;\n              } else {\n                throw error;\n              }\n            }\n          }\n        };\n\n        let reader: ReadableStreamDefaultReader<Uint8Array> | null = null;\n\n        try {\n          reader = openaiStream.getReader();\n          const decoder = new TextDecoder();\n          let buffer = \"\";\n\n          while (true) {\n            if (isClosed) {\n              break;\n            }\n\n            const { done, value } = await reader.read();\n            if (done) break;\n\n            buffer += decoder.decode(value, { stream: true });\n            const lines = buffer.split(\"\\n\");\n            buffer = lines.pop() || \"\";\n\n            for (const line of lines) {\n              if (isClosed || hasFinished) break;\n\n              if (!line.startsWith(\"data:\")) continue;\n              const data = line.slice(5).trim();\n              this.logger.debug({\n                reqId: context.req.id,\n                type: \"recieved data\",\n                data,\n              });\n\n              if (data === \"[DONE]\") {\n                continue;\n              }\n\n              try {\n                const chunk = JSON.parse(data);\n                totalChunks++;\n                this.logger.debug({\n                  reqId: context.req.id,\n                  response: chunk,\n                  tppe: \"Original Response\",\n                });\n                if (chunk.error) {\n                  const errorMessage = {\n                    type: \"error\",\n                    message: {\n                      type: \"api_error\",\n                      message: JSON.stringify(chunk.error),\n                    },\n                  };\n\n                  safeEnqueue(\n                    encoder.encode(\n                      `event: error\\ndata: ${JSON.stringify(errorMessage)}\\n\\n`\n                    )\n                  );\n                  continue;\n                }\n\n                model = chunk.model || model;\n\n                if (!hasStarted && !isClosed && !hasFinished) {\n                  hasStarted = true;\n\n                  const messageStart = {\n                    type: \"message_start\",\n                    message: {\n                      id: messageId,\n                      type: \"message\",\n                      role: \"assistant\",\n                      content: [],\n                      model: model,\n                      stop_reason: null,\n                      stop_sequence: null,\n                      usage: {\n                        input_tokens: 0,\n                        output_tokens: 0,\n                      },\n                    },\n                  };\n\n                  safeEnqueue(\n                    encoder.encode(\n                      `event: message_start\\ndata: ${JSON.stringify(\n                        messageStart\n                      )}\\n\\n`\n                    )\n                  );\n                }\n\n                const choice = chunk.choices?.[0];\n                if (chunk.usage) {\n                  if (!stopReasonMessageDelta) {\n                    stopReasonMessageDelta = {\n                      type: \"message_delta\",\n                      delta: {\n                        stop_reason: \"end_turn\",\n                        stop_sequence: null,\n                      },\n                      usage: {\n                        input_tokens:\n                          (chunk.usage?.prompt_tokens || 0) -\n                          (chunk.usage?.prompt_tokens_details?.cached_tokens ||\n                            0),\n                        output_tokens: chunk.usage?.completion_tokens || 0,\n                        cache_read_input_tokens:\n                          chunk.usage?.prompt_tokens_details?.cached_tokens ||\n                          0,\n                      },\n                    };\n                  } else {\n                    stopReasonMessageDelta.usage = {\n                      input_tokens:\n                        (chunk.usage?.prompt_tokens || 0) -\n                        (chunk.usage?.prompt_tokens_details?.cached_tokens ||\n                          0),\n                      output_tokens: chunk.usage?.completion_tokens || 0,\n                      cache_read_input_tokens:\n                        chunk.usage?.prompt_tokens_details?.cached_tokens || 0,\n                    };\n                  }\n                }\n                if (!choice) {\n                  continue;\n                }\n\n                if (choice?.delta?.thinking && !isClosed && !hasFinished) {\n                  // Close any previous content block if open\n                  // if (currentContentBlockIndex >= 0) {\n                  //   const contentBlockStop = {\n                  //     type: \"content_block_stop\",\n                  //     index: currentContentBlockIndex,\n                  //   };\n                  //   safeEnqueue(\n                  //     encoder.encode(\n                  //       `event: content_block_stop\\ndata: ${JSON.stringify(\n                  //         contentBlockStop\n                  //       )}\\n\\n`\n                  //     )\n                  //   );\n                  //   currentContentBlockIndex = -1;\n                  // }\n\n                  if (!isThinkingStarted) {\n                    const thinkingBlockIndex = assignContentBlockIndex();\n                    const contentBlockStart = {\n                      type: \"content_block_start\",\n                      index: thinkingBlockIndex,\n                      content_block: { type: \"thinking\", thinking: \"\" },\n                    };\n                    safeEnqueue(\n                      encoder.encode(\n                        `event: content_block_start\\ndata: ${JSON.stringify(\n                          contentBlockStart\n                        )}\\n\\n`\n                      )\n                    );\n                    currentContentBlockIndex = thinkingBlockIndex;\n                    isThinkingStarted = true;\n                  }\n                  if (choice.delta.thinking.signature) {\n                    const thinkingSignature = {\n                      type: \"content_block_delta\",\n                      index: currentContentBlockIndex,\n                      delta: {\n                        type: \"signature_delta\",\n                        signature: choice.delta.thinking.signature,\n                      },\n                    };\n                    safeEnqueue(\n                      encoder.encode(\n                        `event: content_block_delta\\ndata: ${JSON.stringify(\n                          thinkingSignature\n                        )}\\n\\n`\n                      )\n                    );\n                    const contentBlockStop = {\n                      type: \"content_block_stop\",\n                      index: currentContentBlockIndex,\n                    };\n                    safeEnqueue(\n                      encoder.encode(\n                        `event: content_block_stop\\ndata: ${JSON.stringify(\n                          contentBlockStop\n                        )}\\n\\n`\n                      )\n                    );\n                    currentContentBlockIndex = -1;\n                  } else if (choice.delta.thinking.content) {\n                    const thinkingChunk = {\n                      type: \"content_block_delta\",\n                      index: currentContentBlockIndex,\n                      delta: {\n                        type: \"thinking_delta\",\n                        thinking: choice.delta.thinking.content || \"\",\n                      },\n                    };\n                    safeEnqueue(\n                      encoder.encode(\n                        `event: content_block_delta\\ndata: ${JSON.stringify(\n                          thinkingChunk\n                        )}\\n\\n`\n                      )\n                    );\n                  }\n                }\n\n                if (choice?.delta?.content && !isClosed && !hasFinished) {\n                  contentChunks++;\n\n                  // Close any previous content block if open and it's not a text content block\n                  if (currentContentBlockIndex >= 0) {\n                    // Check if current content block is text type\n                    const isCurrentTextBlock = hasTextContentStarted;\n                    if (!isCurrentTextBlock) {\n                      const contentBlockStop = {\n                        type: \"content_block_stop\",\n                        index: currentContentBlockIndex,\n                      };\n                      safeEnqueue(\n                        encoder.encode(\n                          `event: content_block_stop\\ndata: ${JSON.stringify(\n                            contentBlockStop\n                          )}\\n\\n`\n                        )\n                      );\n                      currentContentBlockIndex = -1;\n                    }\n                  }\n\n                  if (!hasTextContentStarted && !hasFinished) {\n                    hasTextContentStarted = true;\n                    const textBlockIndex = assignContentBlockIndex();\n                    const contentBlockStart = {\n                      type: \"content_block_start\",\n                      index: textBlockIndex,\n                      content_block: {\n                        type: \"text\",\n                        text: \"\",\n                      },\n                    };\n                    safeEnqueue(\n                      encoder.encode(\n                        `event: content_block_start\\ndata: ${JSON.stringify(\n                          contentBlockStart\n                        )}\\n\\n`\n                      )\n                    );\n                    currentContentBlockIndex = textBlockIndex;\n                  }\n\n                  if (!isClosed && !hasFinished) {\n                    const anthropicChunk = {\n                      type: \"content_block_delta\",\n                      index: currentContentBlockIndex, // Use current content block index\n                      delta: {\n                        type: \"text_delta\",\n                        text: choice.delta.content,\n                      },\n                    };\n                    safeEnqueue(\n                      encoder.encode(\n                        `event: content_block_delta\\ndata: ${JSON.stringify(\n                          anthropicChunk\n                        )}\\n\\n`\n                      )\n                    );\n                  }\n                }\n\n                if (\n                  choice?.delta?.annotations?.length &&\n                  !isClosed &&\n                  !hasFinished\n                ) {\n                  // Close text content block if open\n                  if (currentContentBlockIndex >= 0 && hasTextContentStarted) {\n                    const contentBlockStop = {\n                      type: \"content_block_stop\",\n                      index: currentContentBlockIndex,\n                    };\n                    safeEnqueue(\n                      encoder.encode(\n                        `event: content_block_stop\\ndata: ${JSON.stringify(\n                          contentBlockStop\n                        )}\\n\\n`\n                      )\n                    );\n                    currentContentBlockIndex = -1;\n                    hasTextContentStarted = false;\n                  }\n\n                  choice?.delta?.annotations.forEach((annotation: any) => {\n                    const annotationBlockIndex = assignContentBlockIndex();\n                    const contentBlockStart = {\n                      type: \"content_block_start\",\n                      index: annotationBlockIndex,\n                      content_block: {\n                        type: \"web_search_tool_result\",\n                        tool_use_id: `srvtoolu_${uuidv4()}`,\n                        content: [\n                          {\n                            type: \"web_search_result\",\n                            title: annotation.url_citation.title,\n                            url: annotation.url_citation.url,\n                          },\n                        ],\n                      },\n                    };\n                    safeEnqueue(\n                      encoder.encode(\n                        `event: content_block_start\\ndata: ${JSON.stringify(\n                          contentBlockStart\n                        )}\\n\\n`\n                      )\n                    );\n\n                    const contentBlockStop = {\n                      type: \"content_block_stop\",\n                      index: annotationBlockIndex,\n                    };\n                    safeEnqueue(\n                      encoder.encode(\n                        `event: content_block_stop\\ndata: ${JSON.stringify(\n                          contentBlockStop\n                        )}\\n\\n`\n                      )\n                    );\n                    currentContentBlockIndex = -1;\n                  });\n                }\n\n                if (choice?.delta?.tool_calls && !isClosed && !hasFinished) {\n                  toolCallChunks++;\n                  const processedInThisChunk = new Set<number>();\n\n                  for (const toolCall of choice.delta.tool_calls) {\n                    if (isClosed) break;\n                    const toolCallIndex = toolCall.index ?? 0;\n                    if (processedInThisChunk.has(toolCallIndex)) {\n                      continue;\n                    }\n                    processedInThisChunk.add(toolCallIndex);\n                    const isUnknownIndex =\n                      !toolCallIndexToContentBlockIndex.has(toolCallIndex);\n\n                    if (isUnknownIndex) {\n                      // Close any previous content block if open\n                      if (currentContentBlockIndex >= 0) {\n                        const contentBlockStop = {\n                          type: \"content_block_stop\",\n                          index: currentContentBlockIndex,\n                        };\n                        safeEnqueue(\n                          encoder.encode(\n                            `event: content_block_stop\\ndata: ${JSON.stringify(\n                              contentBlockStop\n                            )}\\n\\n`\n                          )\n                        );\n                        currentContentBlockIndex = -1;\n                      }\n\n                      const newContentBlockIndex = assignContentBlockIndex();\n                      toolCallIndexToContentBlockIndex.set(\n                        toolCallIndex,\n                        newContentBlockIndex\n                      );\n                      const toolCallId =\n                        toolCall.id || `call_${Date.now()}_${toolCallIndex}`;\n                      const toolCallName =\n                        toolCall.function?.name || `tool_${toolCallIndex}`;\n                      const contentBlockStart = {\n                        type: \"content_block_start\",\n                        index: newContentBlockIndex,\n                        content_block: {\n                          type: \"tool_use\",\n                          id: toolCallId,\n                          name: toolCallName,\n                          input: {},\n                        },\n                      };\n\n                      safeEnqueue(\n                        encoder.encode(\n                          `event: content_block_start\\ndata: ${JSON.stringify(\n                            contentBlockStart\n                          )}\\n\\n`\n                        )\n                      );\n                      currentContentBlockIndex = newContentBlockIndex;\n\n                      const toolCallInfo = {\n                        id: toolCallId,\n                        name: toolCallName,\n                        arguments: \"\",\n                        contentBlockIndex: newContentBlockIndex,\n                      };\n                      toolCalls.set(toolCallIndex, toolCallInfo);\n                    } else if (toolCall.id && toolCall.function?.name) {\n                      const existingToolCall = toolCalls.get(toolCallIndex)!;\n                      const wasTemporary =\n                        existingToolCall.id.startsWith(\"call_\") &&\n                        existingToolCall.name.startsWith(\"tool_\");\n\n                      if (wasTemporary) {\n                        existingToolCall.id = toolCall.id;\n                        existingToolCall.name = toolCall.function.name;\n                      }\n                    }\n\n                    if (\n                      toolCall.function?.arguments &&\n                      !isClosed &&\n                      !hasFinished\n                    ) {\n                      const blockIndex =\n                        toolCallIndexToContentBlockIndex.get(toolCallIndex);\n                      if (blockIndex === undefined) {\n                        continue;\n                      }\n                      const currentToolCall = toolCalls.get(toolCallIndex);\n                      if (currentToolCall) {\n                        currentToolCall.arguments +=\n                          toolCall.function.arguments;\n                      }\n\n                      try {\n                        const anthropicChunk = {\n                          type: \"content_block_delta\",\n                          index: blockIndex,\n                          delta: {\n                            type: \"input_json_delta\",\n                            partial_json: toolCall.function.arguments,\n                          },\n                        };\n                        safeEnqueue(\n                          encoder.encode(\n                            `event: content_block_delta\\ndata: ${JSON.stringify(\n                              anthropicChunk\n                            )}\\n\\n`\n                          )\n                        );\n                      } catch {\n                        try {\n                          const fixedArgument = toolCall.function.arguments\n                            .replace(/[\\x00-\\x1F\\x7F-\\x9F]/g, \"\")\n                            .replace(/\\\\/g, \"\\\\\\\\\")\n                            .replace(/\"/g, '\\\\\"');\n\n                          const fixedChunk = {\n                            type: \"content_block_delta\",\n                            index: blockIndex, // Use the correct content block index\n                            delta: {\n                              type: \"input_json_delta\",\n                              partial_json: fixedArgument,\n                            },\n                          };\n                          safeEnqueue(\n                            encoder.encode(\n                              `event: content_block_delta\\ndata: ${JSON.stringify(\n                                fixedChunk\n                              )}\\n\\n`\n                            )\n                          );\n                        } catch (fixError) {\n                          console.error(fixError);\n                        }\n                      }\n                    }\n                  }\n                }\n\n                if (choice?.finish_reason && !isClosed && !hasFinished) {\n                  if (contentChunks === 0 && toolCallChunks === 0) {\n                    console.error(\n                      \"Warning: No content in the stream response!\"\n                    );\n                  }\n\n                  // Close any remaining open content block\n                  if (currentContentBlockIndex >= 0) {\n                    const contentBlockStop = {\n                      type: \"content_block_stop\",\n                      index: currentContentBlockIndex,\n                    };\n                    safeEnqueue(\n                      encoder.encode(\n                        `event: content_block_stop\\ndata: ${JSON.stringify(\n                          contentBlockStop\n                        )}\\n\\n`\n                      )\n                    );\n                    currentContentBlockIndex = -1;\n                  }\n\n                  if (!isClosed) {\n                    const stopReasonMapping: Record<string, string> = {\n                      stop: \"end_turn\",\n                      length: \"max_tokens\",\n                      tool_calls: \"tool_use\",\n                      content_filter: \"stop_sequence\",\n                    };\n\n                    const anthropicStopReason =\n                      stopReasonMapping[choice.finish_reason] || \"end_turn\";\n\n                    stopReasonMessageDelta = {\n                      type: \"message_delta\",\n                      delta: {\n                        stop_reason: anthropicStopReason,\n                        stop_sequence: null,\n                      },\n                      usage: {\n                        input_tokens:\n                          (chunk.usage?.prompt_tokens || 0) -\n                          (chunk.usage?.prompt_tokens_details?.cached_tokens ||\n                            0),\n                        output_tokens: chunk.usage?.completion_tokens || 0,\n                        cache_read_input_tokens:\n                          chunk.usage?.prompt_tokens_details?.cached_tokens ||\n                          0,\n                      },\n                    };\n                  }\n\n                  break;\n                }\n              } catch (parseError: any) {\n                this.logger?.error(\n                  `parseError: ${parseError.name} message: ${parseError.message} stack: ${parseError.stack} data: ${data}`\n                );\n              }\n            }\n          }\n          safeClose();\n        } catch (error) {\n          if (!isClosed) {\n            try {\n              controller.error(error);\n            } catch (controllerError) {\n              console.error(controllerError);\n            }\n          }\n        } finally {\n          if (reader) {\n            try {\n              reader.releaseLock();\n            } catch (releaseError) {\n              console.error(releaseError);\n            }\n          }\n        }\n      },\n      cancel: (reason) => {\n        this.logger.debug(\n          {\n            reqId: context.req.id,\n          },\n          `cancle stream: ${reason}`\n        );\n      },\n    });\n\n    return readable;\n  }\n\n  private convertOpenAIResponseToAnthropic(\n    openaiResponse: ChatCompletion,\n    context: TransformerContext\n  ): any {\n    this.logger.debug(\n      {\n        reqId: context.req.id,\n        response: openaiResponse,\n      },\n      `Original OpenAI response`\n    );\n    try {\n      const choice = openaiResponse.choices[0];\n      if (!choice) {\n        throw new Error(\"No choices found in OpenAI response\");\n      }\n      const content: any[] = [];\n      if (choice.message.annotations) {\n        const id = `srvtoolu_${uuidv4()}`;\n        content.push({\n          type: \"server_tool_use\",\n          id,\n          name: \"web_search\",\n          input: {\n            query: \"\",\n          },\n        });\n        content.push({\n          type: \"web_search_tool_result\",\n          tool_use_id: id,\n          content: choice.message.annotations.map((item) => {\n            return {\n              type: \"web_search_result\",\n              url: item.url_citation.url,\n              title: item.url_citation.title,\n            };\n          }),\n        });\n      }\n      if (choice.message.content) {\n        content.push({\n          type: \"text\",\n          text: choice.message.content,\n        });\n      }\n      if (choice.message.tool_calls && choice.message.tool_calls.length > 0) {\n        choice.message.tool_calls.forEach((toolCall) => {\n          let parsedInput = {};\n          try {\n            const argumentsStr = toolCall.function.arguments || \"{}\";\n\n            if (typeof argumentsStr === \"object\") {\n              parsedInput = argumentsStr;\n            } else if (typeof argumentsStr === \"string\") {\n              parsedInput = JSON.parse(argumentsStr);\n            }\n          } catch {\n            parsedInput = { text: toolCall.function.arguments || \"\" };\n          }\n\n          content.push({\n            type: \"tool_use\",\n            id: toolCall.id,\n            name: toolCall.function.name,\n            input: parsedInput,\n          });\n        });\n      }\n      if ((choice.message as any)?.thinking?.content) {\n        content.push({\n          type: \"thinking\",\n          thinking: (choice.message as any).thinking.content,\n          signature: (choice.message as any).thinking.signature,\n        });\n      }\n      const result = {\n        id: openaiResponse.id,\n        type: \"message\",\n        role: \"assistant\",\n        model: openaiResponse.model,\n        content: content,\n        stop_reason:\n          choice.finish_reason === \"stop\"\n            ? \"end_turn\"\n            : choice.finish_reason === \"length\"\n            ? \"max_tokens\"\n            : choice.finish_reason === \"tool_calls\"\n            ? \"tool_use\"\n            : choice.finish_reason === \"content_filter\"\n            ? \"stop_sequence\"\n            : \"end_turn\",\n        stop_sequence: null,\n        usage: {\n          input_tokens:\n            (openaiResponse.usage?.prompt_tokens || 0) -\n            (openaiResponse.usage?.prompt_tokens_details?.cached_tokens || 0),\n          output_tokens: openaiResponse.usage?.completion_tokens || 0,\n          cache_read_input_tokens:\n            openaiResponse.usage?.prompt_tokens_details?.cached_tokens || 0,\n        },\n      };\n      this.logger.debug(\n        {\n          reqId: context.req.id,\n          result,\n        },\n        `Conversion complete, final Anthropic response`\n      );\n      return result;\n    } catch {\n      throw createApiError(\n        `Provider error: ${JSON.stringify(openaiResponse)}`,\n        500,\n        \"provider_error\"\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "packages/core/src/transformer/cerebras.transformer.ts",
    "content": "import { LLMProvider, UnifiedChatRequest, UnifiedMessage } from \"@/types/llm\";\nimport { Transformer } from \"@/types/transformer\";\n\n\n/**\n * Transformer class for Cerebras\n */\nexport class CerebrasTransformer implements Transformer {\n  name = \"cerebras\";\n\n  /**\n   * Transform the request from Claude Code format to Cerebras format\n   * @param request - The incoming request\n   * @param provider - The LLM provider information\n   * @returns The transformed request\n   */\n  async transformRequestIn(\n    request: UnifiedChatRequest,\n    provider: LLMProvider\n  ): Promise<Record<string, unknown>> {\n    // Deep clone the request to avoid modifying the original\n    const transformedRequest = JSON.parse(JSON.stringify(request));\n\n    if (transformedRequest.reasoning) {\n      delete transformedRequest.reasoning;\n    } else {\n      transformedRequest.disable_reasoning = false\n    }\n    \n    return {\n      body: transformedRequest,\n      config: {\n        headers: {\n          'Authorization': `Bearer ${provider.apiKey}`,\n          'Content-Type': 'application/json'\n        }\n      }\n    };\n  }\n\n  async transformResponseOut(response: Response): Promise<Response> {\n    \n    return response;\n  }\n}"
  },
  {
    "path": "packages/core/src/transformer/cleancache.transformer.ts",
    "content": "import { MessageContent, TextContent, UnifiedChatRequest } from \"@/types/llm\";\nimport { Transformer } from \"../types/transformer\";\n\nexport class CleancacheTransformer implements Transformer {\n  name = \"cleancache\";\n\n  async transformRequestIn(request: UnifiedChatRequest): Promise<UnifiedChatRequest> {\n    if (Array.isArray(request.messages)) {\n      request.messages.forEach((msg) => {\n        if (Array.isArray(msg.content)) {\n          (msg.content as MessageContent[]).forEach((item) => {\n            if ((item as TextContent).cache_control) {\n              delete (item as TextContent).cache_control;\n            }\n          });\n        } else if (msg.cache_control) {\n          delete msg.cache_control;\n        }\n      });\n    }\n    return request;\n  }\n}\n"
  },
  {
    "path": "packages/core/src/transformer/customparams.transformer.ts",
    "content": "import { UnifiedChatRequest } from \"../types/llm\";\nimport { Transformer, TransformerOptions } from \"../types/transformer\";\n\nexport interface CustomParamsOptions extends TransformerOptions {\n  /**\n   * Custom parameters to inject into the request body\n   * Any key-value pairs will be added to the request\n   * Supports: string, number, boolean, object, array\n   */\n  [key: string]: any;\n}\n\n/**\n * Transformer for injecting dynamic custom parameters into LLM requests\n * Allows runtime configuration of arbitrary parameters that get merged\n * into the request body using deep merge strategy\n */\nexport class CustomParamsTransformer implements Transformer {\n  static TransformerName = \"customparams\";\n  \n  private options: CustomParamsOptions;\n\n  constructor(options: CustomParamsOptions = {}) {\n    this.options = options;\n  }\n\n  async transformRequestIn(\n    request: UnifiedChatRequest\n  ): Promise<UnifiedChatRequest> {\n    // Create a copy of the request to avoid mutating the original\n    const modifiedRequest = { ...request } as any;\n    \n    // Inject custom parameters with deep merge\n    const parametersToInject = Object.entries(this.options);\n    \n    for (const [key, value] of parametersToInject) {\n      if (key in modifiedRequest) {\n        // Deep merge with existing parameter\n        if (typeof modifiedRequest[key] === 'object' && \n            typeof value === 'object' && \n            !Array.isArray(modifiedRequest[key]) && \n            !Array.isArray(value) &&\n            modifiedRequest[key] !== null &&\n            value !== null) {\n          // Deep merge objects\n          modifiedRequest[key] = this.deepMergeObjects(modifiedRequest[key], value);\n        } else {\n          // For non-objects, keep existing value (preserve original)\n          continue;\n        }\n      } else {\n        // Add new parameter\n        modifiedRequest[key] = this.cloneValue(value);\n      }\n    }\n\n    return modifiedRequest;\n  }\n\n  async transformResponseOut(response: Response): Promise<Response> {\n    // Pass through response unchanged\n    return response;\n  }\n\n\n\n  /**\n   * Deep merge two objects recursively\n   */\n  private deepMergeObjects(target: any, source: any): any {\n    const result = { ...target };\n    \n    for (const [key, value] of Object.entries(source)) {\n      if (key in result && \n          typeof result[key] === 'object' && \n          typeof value === 'object' &&\n          !Array.isArray(result[key]) && \n          !Array.isArray(value) &&\n          result[key] !== null &&\n          value !== null) {\n        result[key] = this.deepMergeObjects(result[key], value);\n      } else {\n        result[key] = this.cloneValue(value);\n      }\n    }\n    \n    return result;\n  }\n\n  /**\n   * Clone a value to prevent reference issues\n   */\n  private cloneValue(value: any): any {\n    if (value === null || typeof value !== 'object') {\n      return value;\n    }\n    \n    if (Array.isArray(value)) {\n      return value.map(item => this.cloneValue(item));\n    }\n    \n    const cloned: any = {};\n    for (const [key, val] of Object.entries(value)) {\n      cloned[key] = this.cloneValue(val);\n    }\n    return cloned;\n  }\n}"
  },
  {
    "path": "packages/core/src/transformer/deepseek.transformer.ts",
    "content": "import { UnifiedChatRequest } from \"../types/llm\";\nimport { Transformer } from \"../types/transformer\";\n\nexport class DeepseekTransformer implements Transformer {\n  name = \"deepseek\";\n\n  async transformRequestIn(request: UnifiedChatRequest): Promise<UnifiedChatRequest> {\n    if (request.max_tokens && request.max_tokens > 8192) {\n      request.max_tokens = 8192; // DeepSeek has a max token limit of 8192\n    }\n    return request;\n  }\n\n  async transformResponseOut(response: Response): Promise<Response> {\n    if (response.headers.get(\"Content-Type\")?.includes(\"application/json\")) {\n      const jsonResponse = await response.json();\n      // Handle non-streaming response if needed\n      return new Response(JSON.stringify(jsonResponse), {\n        status: response.status,\n        statusText: response.statusText,\n        headers: response.headers,\n      });\n    } else if (response.headers.get(\"Content-Type\")?.includes(\"stream\")) {\n      if (!response.body) {\n        return response;\n      }\n\n      const decoder = new TextDecoder();\n      const encoder = new TextEncoder();\n      let reasoningContent = \"\";\n      let isReasoningComplete = false;\n      let buffer = \"\"; // 用于缓冲不完整的数据\n\n      const stream = new ReadableStream({\n        async start(controller) {\n          const reader = response.body!.getReader();\n          const processBuffer = (\n            buffer: string,\n            controller: ReadableStreamDefaultController,\n            encoder: TextEncoder\n          ) => {\n            const lines = buffer.split(\"\\n\");\n            for (const line of lines) {\n              if (line.trim()) {\n                controller.enqueue(encoder.encode(line + \"\\n\"));\n              }\n            }\n          };\n\n          const processLine = (\n            line: string,\n            context: {\n              controller: ReadableStreamDefaultController;\n              encoder: TextEncoder;\n              reasoningContent: () => string;\n              appendReasoningContent: (content: string) => void;\n              isReasoningComplete: () => boolean;\n              setReasoningComplete: (val: boolean) => void;\n            }\n          ) => {\n            const { controller, encoder } = context;\n\n            if (\n              line.startsWith(\"data: \") &&\n              line.trim() !== \"data: [DONE]\"\n            ) {\n              try {\n                const data = JSON.parse(line.slice(6));\n\n                // Extract reasoning_content from delta\n                if (data.choices?.[0]?.delta?.reasoning_content) {\n                  context.appendReasoningContent(\n                    data.choices[0].delta.reasoning_content\n                  );\n                  const thinkingChunk = {\n                    ...data,\n                    choices: [\n                      {\n                        ...data.choices[0],\n                        delta: {\n                          ...data.choices[0].delta,\n                          thinking: {\n                            content: data.choices[0].delta.reasoning_content,\n                          },\n                        },\n                      },\n                    ],\n                  };\n                  delete thinkingChunk.choices[0].delta.reasoning_content;\n                  const thinkingLine = `data: ${JSON.stringify(\n                    thinkingChunk\n                  )}\\n\\n`;\n                  controller.enqueue(encoder.encode(thinkingLine));\n                  return;\n                }\n\n                // Check if reasoning is complete (when delta has content but no reasoning_content)\n                if (\n                  data.choices?.[0]?.delta?.content &&\n                  context.reasoningContent() &&\n                  !context.isReasoningComplete()\n                ) {\n                  context.setReasoningComplete(true);\n                  const signature = Date.now().toString();\n\n                  // Create a new chunk with thinking block\n                  const thinkingChunk = {\n                    ...data,\n                    choices: [\n                      {\n                        ...data.choices[0],\n                        delta: {\n                          ...data.choices[0].delta,\n                          content: null,\n                          thinking: {\n                            content: context.reasoningContent(),\n                            signature: signature,\n                          },\n                        },\n                      },\n                    ],\n                  };\n                  delete thinkingChunk.choices[0].delta.reasoning_content;\n                  // Send the thinking chunk\n                  const thinkingLine = `data: ${JSON.stringify(\n                    thinkingChunk\n                  )}\\n\\n`;\n                  controller.enqueue(encoder.encode(thinkingLine));\n                }\n\n                if (data.choices[0]?.delta?.reasoning_content) {\n                  delete data.choices[0].delta.reasoning_content;\n                }\n\n                // Send the modified chunk\n                if (\n                  data.choices?.[0]?.delta &&\n                  Object.keys(data.choices[0].delta).length > 0\n                ) {\n                  if (context.isReasoningComplete()) {\n                    data.choices[0].index++;\n                  }\n                  const modifiedLine = `data: ${JSON.stringify(data)}\\n\\n`;\n                  controller.enqueue(encoder.encode(modifiedLine));\n                }\n              } catch (e) {\n                // If JSON parsing fails, pass through the original line\n                controller.enqueue(encoder.encode(line + \"\\n\"));\n              }\n            } else {\n              // Pass through non-data lines (like [DONE])\n              controller.enqueue(encoder.encode(line + \"\\n\"));\n            }\n          };\n\n          try {\n            while (true) {\n              const { done, value } = await reader.read();\n              if (done) {\n                // 处理缓冲区中剩余的数据\n                if (buffer.trim()) {\n                  processBuffer(buffer, controller, encoder);\n                }\n                break;\n              }\n\n              const chunk = decoder.decode(value, { stream: true });\n              buffer += chunk;\n\n              // 处理缓冲区中完整的数据行\n              const lines = buffer.split(\"\\n\");\n              buffer = lines.pop() || \"\"; // 最后一行可能不完整，保留在缓冲区\n\n              for (const line of lines) {\n                if (!line.trim()) continue;\n\n                try {\n                  processLine(line, {\n                    controller,\n                    encoder,\n                    reasoningContent: () => reasoningContent,\n                    appendReasoningContent: (content) =>\n                      (reasoningContent += content),\n                    isReasoningComplete: () => isReasoningComplete,\n                    setReasoningComplete: (val) => (isReasoningComplete = val),\n                  });\n                } catch (error) {\n                  console.error(\"Error processing line:\", line, error);\n                  // 如果解析失败，直接传递原始行\n                  controller.enqueue(encoder.encode(line + \"\\n\"));\n                }\n              }\n            }\n          } catch (error) {\n            console.error(\"Stream error:\", error);\n            controller.error(error);\n          } finally {\n            try {\n              reader.releaseLock();\n            } catch (e) {\n              console.error(\"Error releasing reader lock:\", e);\n            }\n            controller.close();\n          }\n        },\n      });\n\n      return new Response(stream, {\n        status: response.status,\n        statusText: response.statusText,\n        headers: {\n          \"Content-Type\": response.headers.get(\"Content-Type\") || \"text/plain\",\n          \"Cache-Control\": \"no-cache\",\n          Connection: \"keep-alive\",\n        },\n      });\n    }\n\n    return response;\n  }\n}\n"
  },
  {
    "path": "packages/core/src/transformer/enhancetool.transformer.ts",
    "content": "import { Transformer } from \"@/types/transformer\";\nimport { parseToolArguments } from \"@/utils/toolArgumentsParser\";\n\nexport class EnhanceToolTransformer implements Transformer {\n  name = \"enhancetool\";\n\n  async transformResponseOut(response: Response): Promise<Response> {\n    if (response.headers.get(\"Content-Type\")?.includes(\"application/json\")) {\n      const jsonResponse = await response.json();\n      if (jsonResponse?.choices?.[0]?.message?.tool_calls?.length) {\n        // 处理非流式的工具调用参数解析\n        for (const toolCall of jsonResponse.choices[0].message.tool_calls) {\n          if (toolCall.function?.arguments) {\n            toolCall.function.arguments = parseToolArguments(\n              toolCall.function.arguments,\n              this.logger\n            );\n          }\n        }\n      }\n      return new Response(JSON.stringify(jsonResponse), {\n        status: response.status,\n        statusText: response.statusText,\n        headers: response.headers,\n      });\n    } else if (response.headers.get(\"Content-Type\")?.includes(\"stream\")) {\n      if (!response.body) {\n        return response;\n      }\n\n      const decoder = new TextDecoder();\n      const encoder = new TextEncoder();\n\n      // Define interface for tool call tracking\n      interface ToolCall {\n        index?: number;\n        name?: string;\n        id?: string;\n        arguments?: string;\n      }\n\n      let currentToolCall: ToolCall = {};\n\n      let hasTextContent = false;\n      let reasoningContent = \"\";\n      let isReasoningComplete = false;\n      let hasToolCall = false;\n      let buffer = \"\"; // 用于缓冲不完整的数据\n\n      const stream = new ReadableStream({\n        async start(controller) {\n          const reader = response.body!.getReader();\n          const processBuffer = (\n            buffer: string,\n            controller: ReadableStreamDefaultController,\n            encoder: TextEncoder\n          ) => {\n            const lines = buffer.split(\"\\n\");\n            for (const line of lines) {\n              if (line.trim()) {\n                controller.enqueue(encoder.encode(line + \"\\n\"));\n              }\n            }\n          };\n\n          // Helper function to process completed tool calls\n          const processCompletedToolCall = (\n            data: any,\n            controller: ReadableStreamDefaultController,\n            encoder: TextEncoder\n          ) => {\n            let finalArgs = \"\";\n            try {\n              finalArgs = parseToolArguments(currentToolCall.arguments || \"\", this.logger);\n            } catch (e: any) {\n              console.error(\n                `${e.message} ${\n                  e.stack\n                }  工具调用参数解析失败: ${JSON.stringify(\n                  currentToolCall\n                )}`\n              );\n              // Use original arguments if parsing fails\n              finalArgs = currentToolCall.arguments || \"\";\n            }\n\n            const delta = {\n              role: \"assistant\",\n              tool_calls: [\n                {\n                  function: {\n                    name: currentToolCall.name,\n                    arguments: finalArgs,\n                  },\n                  id: currentToolCall.id,\n                  index: currentToolCall.index,\n                  type: \"function\",\n                },\n              ],\n            };\n\n            // Remove content field entirely to prevent extra null values\n            const modifiedData = {\n              ...data,\n              choices: [\n                {\n                  ...data.choices[0],\n                  delta,\n                },\n              ],\n            };\n            // Remove content field if it exists\n            if (modifiedData.choices[0].delta.content !== undefined) {\n              delete modifiedData.choices[0].delta.content;\n            }\n\n            const modifiedLine = `data: ${JSON.stringify(modifiedData)}\\n\\n`;\n            controller.enqueue(encoder.encode(modifiedLine));\n          };\n\n          const processLine = (\n            line: string,\n            context: {\n              controller: ReadableStreamDefaultController;\n              encoder: TextEncoder;\n              hasTextContent: () => boolean;\n              setHasTextContent: (val: boolean) => void;\n              reasoningContent: () => string;\n              appendReasoningContent: (content: string) => void;\n              isReasoningComplete: () => boolean;\n              setReasoningComplete: (val: boolean) => void;\n            }\n          ) => {\n            const { controller, encoder } = context;\n\n            if (line.startsWith(\"data: \") && line.trim() !== \"data: [DONE]\") {\n              const jsonStr = line.slice(6);\n              try {\n                const data = JSON.parse(jsonStr);\n\n                // Handle tool calls in streaming mode\n                if (data.choices?.[0]?.delta?.tool_calls?.length) {\n                  const toolCallDelta = data.choices[0].delta.tool_calls[0];\n\n                  // Initialize currentToolCall if this is the first chunk for this tool call\n                  if (typeof currentToolCall.index === \"undefined\") {\n                    currentToolCall = {\n                      index: toolCallDelta.index,\n                      name: toolCallDelta.function?.name || \"\",\n                      id: toolCallDelta.id || \"\",\n                      arguments: toolCallDelta.function?.arguments || \"\"\n                    };\n                    if (toolCallDelta.function?.arguments) {\n                      toolCallDelta.function.arguments = ''\n                    }\n                    // Send the first chunk as-is\n                    const modifiedLine = `data: ${JSON.stringify(data)}\\n\\n`;\n                    controller.enqueue(encoder.encode(modifiedLine));\n                    return;\n                  }\n                  // Accumulate arguments if this is a continuation of the current tool call\n                  else if (currentToolCall.index === toolCallDelta.index) {\n                    if (toolCallDelta.function?.arguments) {\n                      currentToolCall.arguments += toolCallDelta.function.arguments;\n                    }\n                    // Don't send intermediate chunks that only contain arguments\n                    return;\n                  }\n                  // If we have a different tool call index, process the previous one and start a new one\n                  else {\n                    // Process the completed tool call using helper function\n                    processCompletedToolCall(data, controller, encoder);\n\n                    // Start tracking the new tool call\n                    currentToolCall = {\n                      index: toolCallDelta.index,\n                      name: toolCallDelta.function?.name || \"\",\n                      id: toolCallDelta.id || \"\",\n                      arguments: toolCallDelta.function?.arguments || \"\"\n                    };\n                    return;\n                  }\n                }\n\n                // Handle finish_reason for tool_calls\n                if (data.choices?.[0]?.finish_reason === \"tool_calls\" && currentToolCall.index !== undefined) {\n                  // Process the final tool call using helper function\n                  processCompletedToolCall(data, controller, encoder);\n                  currentToolCall = {};\n                  return;\n                }\n\n                // Handle text content alongside tool calls\n                if (\n                  data.choices?.[0]?.delta?.tool_calls?.length &&\n                  context.hasTextContent()\n                ) {\n                  if (typeof data.choices[0].index === \"number\") {\n                    data.choices[0].index += 1;\n                  } else {\n                    data.choices[0].index = 1;\n                  }\n                }\n\n                const modifiedLine = `data: ${JSON.stringify(data)}\\n\\n`;\n                controller.enqueue(encoder.encode(modifiedLine));\n              } catch (e) {\n                // 如果JSON解析失败，可能是数据不完整，将原始行传递下去\n                controller.enqueue(encoder.encode(line + \"\\n\"));\n              }\n            } else {\n              // Pass through non-data lines (like [DONE])\n              controller.enqueue(encoder.encode(line + \"\\n\"));\n            }\n          };\n\n          try {\n            while (true) {\n              const { done, value } = await reader.read();\n              if (done) {\n                // 处理缓冲区中剩余的数据\n                if (buffer.trim()) {\n                  processBuffer(buffer, controller, encoder);\n                }\n                break;\n              }\n\n              // 检查value是否有效\n              if (!value || value.length === 0) {\n                continue;\n              }\n\n              let chunk;\n              try {\n                chunk = decoder.decode(value, { stream: true });\n              } catch (decodeError) {\n                console.warn(\"Failed to decode chunk\", decodeError);\n                continue;\n              }\n\n              if (chunk.length === 0) {\n                continue;\n              }\n\n              buffer += chunk;\n\n              // 如果缓冲区过大，进行处理避免内存泄漏\n              if (buffer.length > 1000000) {\n                // 1MB 限制\n                console.warn(\n                  \"Buffer size exceeds limit, processing partial data\"\n                );\n                const lines = buffer.split(\"\\n\");\n                buffer = lines.pop() || \"\";\n\n                for (const line of lines) {\n                  if (line.trim()) {\n                    try {\n                      processLine(line, {\n                        controller,\n                        encoder,\n                        hasTextContent: () => hasTextContent,\n                        setHasTextContent: (val) => (hasTextContent = val),\n                        reasoningContent: () => reasoningContent,\n                        appendReasoningContent: (content) =>\n                          (reasoningContent += content),\n                        isReasoningComplete: () => isReasoningComplete,\n                        setReasoningComplete: (val) =>\n                          (isReasoningComplete = val),\n                      });\n                    } catch (error) {\n                      console.error(\"Error processing line:\", line, error);\n                      // 如果解析失败，直接传递原始行\n                      controller.enqueue(encoder.encode(line + \"\\n\"));\n                    }\n                  }\n                }\n                continue;\n              }\n\n              // 处理缓冲区中完整的数据行\n              const lines = buffer.split(\"\\n\");\n              buffer = lines.pop() || \"\"; // 最后一行可能不完整，保留在缓冲区\n\n              for (const line of lines) {\n                if (!line.trim()) continue;\n\n                try {\n                  processLine(line, {\n                    controller,\n                    encoder,\n                    hasTextContent: () => hasTextContent,\n                    setHasTextContent: (val) => (hasTextContent = val),\n                    reasoningContent: () => reasoningContent,\n                    appendReasoningContent: (content) =>\n                      (reasoningContent += content),\n                    isReasoningComplete: () => isReasoningComplete,\n                    setReasoningComplete: (val) => (isReasoningComplete = val),\n                  });\n                } catch (error) {\n                  console.error(\"Error processing line:\", line, error);\n                  // 如果解析失败，直接传递原始行\n                  controller.enqueue(encoder.encode(line + \"\\n\"));\n                }\n              }\n            }\n          } catch (error) {\n            console.error(\"Stream error:\", error);\n            controller.error(error);\n          } finally {\n            try {\n              reader.releaseLock();\n            } catch (e) {\n              console.error(\"Error releasing reader lock:\", e);\n            }\n            controller.close();\n          }\n        },\n      });\n\n      return new Response(stream, {\n        status: response.status,\n        statusText: response.statusText,\n        headers: {\n          \"Content-Type\": \"text/event-stream\",\n          \"Cache-Control\": \"no-cache\",\n          Connection: \"keep-alive\",\n        },\n      });\n    }\n\n    return response;\n  }\n}\n"
  },
  {
    "path": "packages/core/src/transformer/forcereasoning.transformer.ts",
    "content": "import { UnifiedChatRequest } from \"../types/llm\";\nimport { Transformer } from \"../types/transformer\";\n\nconst PROMPT = `Always think before answering. Even if the problem seems simple, always write down your reasoning process explicitly.\n\nOutput format:\n<reasoning_content>\nYour detailed thinking process goes here\n</reasoning_content>\nYour final answer must follow after the closing tag above.`;\n\nconst MAX_INTERLEAVED_TIMES = 10;\n\nexport class ForceReasoningTransformer implements Transformer {\n  name = \"forcereasoning\";\n\n  async transformRequestIn(\n    request: UnifiedChatRequest\n  ): Promise<UnifiedChatRequest> {\n    let times = 0\n    request.messages\n      .filter((msg) => msg.role === \"assistant\")\n      .reverse()\n      .forEach((message) => {\n        if (message.thinking) {\n          if (message.thinking.content) {\n            if (!message.content || times < MAX_INTERLEAVED_TIMES) {\n              times++;\n              message.content = `<reasoning_content>${message.thinking.content}</reasoning_content>\\n${message.content}`;\n            }\n          }\n          delete message.thinking;\n        }\n      });\n    const lastMessage = request.messages[request.messages.length - 1];\n    if (lastMessage.role === \"user\") {\n      if (Array.isArray(lastMessage.content)) {\n        lastMessage.content.push({\n          type: \"text\",\n          text: PROMPT,\n        });\n      } else {\n        lastMessage.content = [\n          {\n            type: \"text\",\n            text: PROMPT,\n          },\n          {\n            type: \"text\",\n            text: lastMessage.content || '',\n          },\n        ];\n      }\n    }\n    if (lastMessage.role === \"tool\") {\n      request.messages.push({\n        role: \"user\",\n        content: [\n          {\n            type: \"text\",\n            text: PROMPT,\n          },\n        ],\n      });\n    }\n    return request;\n  }\n\n  async transformResponseOut(response: Response): Promise<Response> {\n    const reasonStartTag = \"<reasoning_content>\";\n    const reasonStopTag = \"</reasoning_content>\";\n\n    if (response.headers.get(\"Content-Type\")?.includes(\"application/json\")) {\n      const jsonResponse: any = await response.json();\n      if (jsonResponse.choices[0]?.message.content) {\n        const regex = /<reasoning_content>(.*?)<\\/reasoning_content>/s;\n        const match = jsonResponse.choices[0]?.message.content.match(regex);\n        if (match && match[1]) {\n          jsonResponse.thinking = {\n            content: match[1],\n          };\n        }\n      }\n      return new Response(JSON.stringify(jsonResponse), {\n        status: response.status,\n        statusText: response.statusText,\n        headers: response.headers,\n      });\n    } else if (response.headers.get(\"Content-Type\")?.includes(\"stream\")) {\n      if (!response.body) {\n        return response;\n      }\n      let contentIndex = 0;\n\n      const decoder = new TextDecoder();\n      const encoder = new TextEncoder();\n\n      const stream = new ReadableStream({\n        async start(controller) {\n          const reader = response.body!.getReader();\n          let lineBuffer = \"\";\n\n          let fsmState: \"SEARCHING\" | \"REASONING\" | \"FINAL\" = \"SEARCHING\";\n          let tagBuffer = \"\";\n          let finalBuffer = \"\";\n\n          const processAndEnqueue = (\n            originalData: any,\n            content: string | null | undefined\n          ) => {\n            if (typeof content !== \"string\") {\n              if (\n                originalData.choices?.[0]?.delta &&\n                Object.keys(originalData.choices[0].delta).length > 0 &&\n                !originalData.choices[0].delta.content\n              ) {\n                originalData.choices[0].index = contentIndex\n                controller.enqueue(\n                  encoder.encode(`data: ${JSON.stringify(originalData)}\\n\\n`)\n                );\n              }\n              return;\n            }\n\n            let currentContent = tagBuffer + content;\n            tagBuffer = \"\";\n\n            while (currentContent.length > 0) {\n              if (fsmState === \"SEARCHING\") {\n                const startTagIndex = currentContent.indexOf(reasonStartTag);\n                if (startTagIndex !== -1) {\n                  currentContent = currentContent.substring(\n                    startTagIndex + reasonStartTag.length\n                  );\n                  fsmState = \"REASONING\";\n                } else {\n                  for (let i = reasonStartTag.length - 1; i > 0; i--) {\n                    if (\n                      currentContent.endsWith(reasonStartTag.substring(0, i))\n                    ) {\n                      tagBuffer = currentContent.substring(\n                        currentContent.length - i\n                      );\n                      break;\n                    }\n                  }\n                  currentContent = \"\";\n                }\n              } else if (fsmState === \"REASONING\") {\n                const endTagIndex = currentContent.indexOf(reasonStopTag);\n                if (endTagIndex !== -1) {\n                  const reasoningPart = currentContent.substring(\n                    0,\n                    endTagIndex\n                  );\n                  if (reasoningPart.length > 0) {\n                    const newDelta = {\n                      ...originalData.choices[0].delta,\n                      thinking: {\n                        content: reasoningPart,\n                      },\n                    };\n                    delete newDelta.content;\n                    const thinkingChunk = {\n                      ...originalData,\n                      choices: [\n                        { ...originalData.choices[0], delta: newDelta, index: contentIndex },\n                      ],\n                    };\n                    controller.enqueue(\n                      encoder.encode(\n                        `data: ${JSON.stringify(thinkingChunk)}\\n\\n`\n                      )\n                    );\n                  }\n\n                  // Send signature message\n                  const signatureDelta = {\n                    ...originalData.choices[0].delta,\n                    thinking: { signature: new Date().getTime().toString() },\n                  };\n                  delete signatureDelta.content;\n                  const signatureChunk = {\n                    ...originalData,\n                    choices: [\n                      { ...originalData.choices[0], delta: signatureDelta, index: contentIndex },\n                    ],\n                  };\n                  controller.enqueue(\n                    encoder.encode(\n                      `data: ${JSON.stringify(signatureChunk)}\\n\\n`\n                    )\n                  );\n                  contentIndex++;\n\n                  currentContent = currentContent.substring(\n                    endTagIndex + reasonStopTag.length\n                  );\n                  fsmState = \"FINAL\";\n                } else {\n                  let reasoningPart = currentContent;\n                  for (let i = reasonStopTag.length - 1; i > 0; i--) {\n                    if (\n                      currentContent.endsWith(reasonStopTag.substring(0, i))\n                    ) {\n                      tagBuffer = currentContent.substring(\n                        currentContent.length - i\n                      );\n                      reasoningPart = currentContent.substring(\n                        0,\n                        currentContent.length - i\n                      );\n                      break;\n                    }\n                  }\n                  if (reasoningPart.length > 0) {\n                    const newDelta = {\n                      ...originalData.choices[0].delta,\n                      thinking: { content: reasoningPart },\n                    };\n                    delete newDelta.content;\n                    const thinkingChunk = {\n                      ...originalData,\n                      choices: [\n                        { ...originalData.choices[0], delta: newDelta, index: contentIndex },\n                      ],\n                    };\n                    controller.enqueue(\n                      encoder.encode(\n                        `data: ${JSON.stringify(thinkingChunk)}\\n\\n`\n                      )\n                    );\n                  }\n                  currentContent = \"\";\n                }\n              } else if (fsmState === \"FINAL\") {\n                if (currentContent.length > 0) {\n                  // Check if content contains only newlines\n                  const isOnlyNewlines = /^\\s*$/.test(currentContent);\n\n                  if (isOnlyNewlines) {\n                    // If only newlines, add to buffer but don't send\n                    finalBuffer += currentContent;\n                  } else {\n                    // If non-whitespace content, send buffer and new content together\n                    const finalPart = finalBuffer + currentContent;\n                    const newDelta = {\n                      ...originalData.choices[0].delta,\n                      content: finalPart,\n                    };\n                    if (newDelta.thinking) delete newDelta.thinking;\n                    const finalChunk = {\n                      ...originalData,\n                      choices: [\n                        { ...originalData.choices[0], delta: newDelta },\n                      ],\n                    };\n                    controller.enqueue(\n                      encoder.encode(`data: ${JSON.stringify(finalChunk)}\\n\\n`)\n                    );\n                    // Clear buffer after sending\n                    finalBuffer = \"\";\n                  }\n                }\n                contentIndex++\n                currentContent = \"\";\n              }\n            }\n          };\n\n          try {\n            while (true) {\n              const { done, value } = await reader.read();\n              if (done) {\n                break;\n              }\n              const chunk = decoder.decode(value, { stream: true });\n              lineBuffer += chunk;\n              const lines = lineBuffer.split(\"\\n\");\n              lineBuffer = lines.pop() || \"\";\n\n              for (const line of lines) {\n                if (!line.trim()) continue;\n\n                if (line.trim() === \"data: [DONE]\") {\n                  controller.enqueue(encoder.encode(line + \"\\n\\n\"));\n                  break;\n                }\n\n                if (line.startsWith(\"data:\")) {\n                  try {\n                    const data = JSON.parse(line.slice(5));\n                    processAndEnqueue(data, data.choices?.[0]?.delta?.content);\n                  } catch (e) {\n                    controller.enqueue(encoder.encode(line + \"\\n\"));\n                  }\n                } else {\n                  controller.enqueue(encoder.encode(line + \"\\n\"));\n                }\n              }\n            }\n          } catch (error) {\n            console.error(\"Stream error:\", error);\n            controller.error(error);\n          } finally {\n            try {\n              reader.releaseLock();\n            } catch (e) {\n              console.error(\"Error releasing reader lock:\", e);\n            }\n\n            if (fsmState === \"REASONING\") {\n              const signatureDelta = {\n                thinking: { signature: new Date().getTime().toString() },\n              };\n              const signatureChunk = {\n                choices: [{ delta: signatureDelta }],\n              };\n              controller.enqueue(\n                encoder.encode(`data: ${JSON.stringify(signatureChunk)}\\n\\n`)\n              );\n            }\n\n            controller.close();\n          }\n        },\n      });\n\n      return new Response(stream, {\n        status: response.status,\n        statusText: response.statusText,\n        headers: {\n          \"Content-Type\": response.headers.get(\"Content-Type\") || \"text/plain\",\n          \"Cache-Control\": \"no-cache\",\n          Connection: \"keep-alive\",\n        },\n      });\n    }\n\n    return response;\n  }\n}\n"
  },
  {
    "path": "packages/core/src/transformer/gemini.transformer.ts",
    "content": "import { LLMProvider, UnifiedChatRequest } from \"../types/llm\";\nimport { Transformer } from \"../types/transformer\";\nimport {\n  buildRequestBody,\n  transformRequestOut,\n  transformResponseOut,\n} from \"../utils/gemini.util\";\n\nexport class GeminiTransformer implements Transformer {\n  name = \"gemini\";\n\n  endPoint = \"/v1beta/models/:modelAndAction\";\n\n  async transformRequestIn(\n    request: UnifiedChatRequest,\n    provider: LLMProvider\n  ): Promise<Record<string, any>> {\n    return {\n      body: buildRequestBody(request),\n      config: {\n        url: new URL(\n          `./${request.model}:${\n            request.stream ? \"streamGenerateContent?alt=sse\" : \"generateContent\"\n          }`,\n          provider.baseUrl\n        ),\n        headers: {\n          \"x-goog-api-key\": provider.apiKey,\n          Authorization: undefined,\n        },\n      },\n    };\n  }\n\n  transformRequestOut = transformRequestOut;\n\n  async transformResponseOut(response: Response): Promise<Response> {\n    return transformResponseOut(response, this.name, this.logger);\n  }\n}\n"
  },
  {
    "path": "packages/core/src/transformer/groq.transformer.ts",
    "content": "import { MessageContent, TextContent, UnifiedChatRequest } from \"@/types/llm\";\nimport { Transformer } from \"../types/transformer\";\nimport { v4 as uuidv4 } from \"uuid\"\n\nexport class GroqTransformer implements Transformer {\n  name = \"groq\";\n\n  async transformRequestIn(request: UnifiedChatRequest): Promise<UnifiedChatRequest> {\n    request.messages.forEach(msg => {\n      if (Array.isArray(msg.content)) {\n        (msg.content as MessageContent[]).forEach((item) => {\n          if ((item as TextContent).cache_control) {\n            delete (item as TextContent).cache_control;\n          }\n        });\n      } else if (msg.cache_control) {\n        delete msg.cache_control;\n      }\n    })\n    if (Array.isArray(request.tools)) {\n      request.tools.forEach(tool => {\n        delete tool.function.parameters.$schema;\n      })\n    }\n    return request\n  }\n\n  async transformResponseOut(response: Response): Promise<Response> {\n    if (response.headers.get(\"Content-Type\")?.includes(\"application/json\")) {\n      const jsonResponse = await response.json();\n      return new Response(JSON.stringify(jsonResponse), {\n        status: response.status,\n        statusText: response.statusText,\n        headers: response.headers,\n      });\n    } else if (response.headers.get(\"Content-Type\")?.includes(\"stream\")) {\n      if (!response.body) {\n        return response;\n      }\n\n      const decoder = new TextDecoder();\n      const encoder = new TextEncoder();\n\n      let hasTextContent = false;\n      let reasoningContent = \"\";\n      let isReasoningComplete = false;\n      let buffer = \"\"; // 用于缓冲不完整的数据\n\n      const stream = new ReadableStream({\n        async start(controller) {\n          const reader = response.body!.getReader();\n          const processBuffer = (buffer: string, controller: ReadableStreamDefaultController, encoder: InstanceType<typeof TextEncoder>) => {\n            const lines = buffer.split(\"\\n\");\n            for (const line of lines) {\n              if (line.trim()) {\n                controller.enqueue(encoder.encode(line + \"\\n\"));\n              }\n            }\n          };\n\n          const processLine = (line: string, context: {\n            controller: ReadableStreamDefaultController;\n            encoder: typeof TextEncoder;\n            hasTextContent: () => boolean;\n            setHasTextContent: (val: boolean) => void;\n            reasoningContent: () => string;\n            appendReasoningContent: (content: string) => void;\n            isReasoningComplete: () => boolean;\n            setReasoningComplete: (val: boolean) => void;\n          }) => {\n            const { controller, encoder } = context;\n\n            if (line.startsWith(\"data: \") && line.trim() !== \"data: [DONE]\") {\n              const jsonStr = line.slice(6);\n              try {\n                const data = JSON.parse(jsonStr);\n                if (data.error) {\n                  throw new Error(JSON.stringify(data));\n                }\n\n                if (data.choices?.[0]?.delta?.content && !context.hasTextContent()) {\n                  context.setHasTextContent(true);\n                }\n\n                if (\n                  data.choices?.[0]?.delta?.tool_calls?.length\n                ) {\n                  data.choices?.[0]?.delta?.tool_calls.forEach((tool: any) => {\n                    tool.id = `call_${uuidv4()}`;\n                  })\n                }\n\n                if (\n                  data.choices?.[0]?.delta?.tool_calls?.length &&\n                  context.hasTextContent()\n                ) {\n                  if (typeof data.choices[0].index === 'number') {\n                    data.choices[0].index += 1;\n                  } else {\n                    data.choices[0].index = 1;\n                  }\n                }\n\n                const modifiedLine = `data: ${JSON.stringify(data)}\\n\\n`;\n                controller.enqueue(encoder.encode(modifiedLine));\n              } catch (e) {\n                // 如果JSON解析失败，可能是数据不完整，将原始行传递下去\n                controller.enqueue(encoder.encode(line + \"\\n\"));\n              }\n            } else {\n              // Pass through non-data lines (like [DONE])\n              controller.enqueue(encoder.encode(line + \"\\n\"));\n            }\n          };\n\n          try {\n            while (true) {\n              const { done, value } = await reader.read();\n              if (done) {\n                // 处理缓冲区中剩余的数据\n                if (buffer.trim()) {\n                  processBuffer(buffer, controller, encoder);\n                }\n                break;\n              }\n\n              // 检查value是否有效\n              if (!value || value.length === 0) {\n                continue;\n              }\n\n              let chunk;\n              try {\n                chunk = decoder.decode(value, { stream: true });\n              } catch (decodeError) {\n                console.warn(\"Failed to decode chunk\", decodeError);\n                continue;\n              }\n\n              if (chunk.length === 0) {\n                continue;\n              }\n\n              buffer += chunk;\n\n              // 如果缓冲区过大，进行处理避免内存泄漏\n              if (buffer.length > 1000000) { // 1MB 限制\n                console.warn(\"Buffer size exceeds limit, processing partial data\");\n                const lines = buffer.split(\"\\n\");\n                buffer = lines.pop() || \"\";\n\n                for (const line of lines) {\n                  if (line.trim()) {\n                    try {\n                      processLine(line, {\n                        controller,\n                        encoder,\n                        hasTextContent: () => hasTextContent,\n                        setHasTextContent: (val) => hasTextContent = val,\n                        reasoningContent: () => reasoningContent,\n                        appendReasoningContent: (content) => reasoningContent += content,\n                        isReasoningComplete: () => isReasoningComplete,\n                        setReasoningComplete: (val) => isReasoningComplete = val\n                      });\n                    } catch (error) {\n                      console.error(\"Error processing line:\", line, error);\n                      // 如果解析失败，直接传递原始行\n                      controller.enqueue(encoder.encode(line + \"\\n\"));\n                    }\n                  }\n                }\n                continue;\n              }\n\n              // 处理缓冲区中完整的数据行\n              const lines = buffer.split(\"\\n\");\n              buffer = lines.pop() || \"\"; // 最后一行可能不完整，保留在缓冲区\n\n              for (const line of lines) {\n                if (!line.trim()) continue;\n\n                try {\n                  processLine(line, {\n                    controller,\n                    encoder,\n                    hasTextContent: () => hasTextContent,\n                    setHasTextContent: (val) => hasTextContent = val,\n                    reasoningContent: () => reasoningContent,\n                    appendReasoningContent: (content) => reasoningContent += content,\n                    isReasoningComplete: () => isReasoningComplete,\n                    setReasoningComplete: (val) => isReasoningComplete = val\n                  });\n                } catch (error) {\n                  console.error(\"Error processing line:\", line, error);\n                  // 如果解析失败，直接传递原始行\n                  controller.enqueue(encoder.encode(line + \"\\n\"));\n                }\n              }\n            }\n          } catch (error) {\n            console.error(\"Stream error:\", error);\n            controller.error(error);\n          } finally {\n            try {\n              reader.releaseLock();\n            } catch (e) {\n              console.error(\"Error releasing reader lock:\", e);\n            }\n            controller.close();\n          }\n        },\n\n      });\n\n      return new Response(stream, {\n        status: response.status,\n        statusText: response.statusText,\n        headers: {\n          \"Content-Type\": \"text/event-stream\",\n          \"Cache-Control\": \"no-cache\",\n          Connection: \"keep-alive\",\n        },\n      });\n    }\n\n    return response;\n  }\n}"
  },
  {
    "path": "packages/core/src/transformer/index.ts",
    "content": "import { AnthropicTransformer } from \"./anthropic.transformer\";\nimport { GeminiTransformer } from \"./gemini.transformer\";\nimport { VertexGeminiTransformer } from \"./vertex-gemini.transformer\";\nimport { DeepseekTransformer } from \"./deepseek.transformer\";\nimport { TooluseTransformer } from \"./tooluse.transformer\";\nimport { OpenrouterTransformer } from \"./openrouter.transformer\";\nimport { MaxTokenTransformer } from \"./maxtoken.transformer\";\nimport { GroqTransformer } from \"./groq.transformer\";\nimport { CleancacheTransformer } from \"./cleancache.transformer\";\nimport { EnhanceToolTransformer } from \"./enhancetool.transformer\";\nimport { ReasoningTransformer } from \"./reasoning.transformer\";\nimport { SamplingTransformer } from \"./sampling.transformer\";\nimport { MaxCompletionTokens } from \"./maxcompletiontokens.transformer\";\nimport { VertexClaudeTransformer } from \"./vertex-claude.transformer\";\nimport { CerebrasTransformer } from \"./cerebras.transformer\";\nimport { StreamOptionsTransformer } from \"./streamoptions.transformer\";\nimport { OpenAITransformer } from \"./openai.transformer\";\nimport { CustomParamsTransformer } from \"./customparams.transformer\";\nimport { VercelTransformer } from \"./vercel.transformer\";\nimport { OpenAIResponsesTransformer } from \"./openai.responses.transformer\";\nimport { ForceReasoningTransformer } from \"./forcereasoning.transformer\"\n\nexport default {\n  AnthropicTransformer,\n  GeminiTransformer,\n  VertexGeminiTransformer,\n  VertexClaudeTransformer,\n  DeepseekTransformer,\n  TooluseTransformer,\n  OpenrouterTransformer,\n  OpenAITransformer,\n  MaxTokenTransformer,\n  GroqTransformer,\n  CleancacheTransformer,\n  EnhanceToolTransformer,\n  ReasoningTransformer,\n  SamplingTransformer,\n  MaxCompletionTokens,\n  CerebrasTransformer,\n  StreamOptionsTransformer,\n  CustomParamsTransformer,\n  VercelTransformer,\n  OpenAIResponsesTransformer,\n  ForceReasoningTransformer\n};\n"
  },
  {
    "path": "packages/core/src/transformer/maxcompletiontokens.transformer.ts",
    "content": "import { UnifiedChatRequest } from \"../types/llm\";\r\nimport { Transformer } from \"../types/transformer\";\r\n\r\nexport class MaxCompletionTokens implements Transformer {\r\n  static TransformerName = \"maxcompletiontokens\";\r\n\r\n  async transformRequestIn(\r\n    request: UnifiedChatRequest\r\n  ): Promise<UnifiedChatRequest> {\r\n    if (request.max_tokens) {\r\n      request.max_completion_tokens = request.max_tokens;\r\n      delete request.max_tokens;\r\n    }\r\n    return request;\r\n  }\r\n}\r\n"
  },
  {
    "path": "packages/core/src/transformer/maxtoken.transformer.ts",
    "content": "import { UnifiedChatRequest } from \"../types/llm\";\nimport { Transformer, TransformerOptions } from \"../types/transformer\";\n\nexport class MaxTokenTransformer implements Transformer {\n  static TransformerName = \"maxtoken\";\n  max_tokens: number;\n\n  constructor(private readonly options?: TransformerOptions) {\n    this.max_tokens = this.options?.max_tokens;\n  }\n\n  async transformRequestIn(request: UnifiedChatRequest): Promise<UnifiedChatRequest> {\n    if (request.max_tokens && request.max_tokens > this.max_tokens) {\n      request.max_tokens = this.max_tokens;\n    }\n    return request;\n  }\n}\n"
  },
  {
    "path": "packages/core/src/transformer/openai.responses.transformer.ts",
    "content": "import { UnifiedChatRequest, MessageContent } from \"@/types/llm\";\nimport { Transformer } from \"@/types/transformer\";\n\ninterface ResponsesAPIOutputItem {\n  type: string;\n  id?: string;\n  call_id?: string;\n  name?: string;\n  arguments?: string;\n  content?: Array<{\n    type: string;\n    text?: string;\n    image_url?: string;\n    mime_type?: string;\n    image_base64?: string;\n  }>;\n  reasoning?: string;\n}\n\ninterface ResponsesAPIPayload {\n  id: string;\n  object: string;\n  model: string;\n  created_at: number;\n  output: ResponsesAPIOutputItem[];\n  usage?: {\n    input_tokens: number;\n    output_tokens: number;\n    total_tokens: number;\n  };\n}\n\ninterface ResponsesStreamEvent {\n  type: string;\n  item_id?: string;\n  output_index?: number;\n  delta?:\n    | string\n    | {\n        url?: string;\n        b64_json?: string;\n        mime_type?: string;\n      };\n  item?: {\n    id?: string;\n    type?: string;\n    call_id?: string;\n    name?: string;\n    content?: Array<{\n      type: string;\n      text?: string;\n      image_url?: string;\n      mime_type?: string;\n    }>;\n    reasoning?: string; // 添加 reasoning 字段支持\n  };\n  response?: {\n    id?: string;\n    model?: string;\n    output?: Array<{\n      type: string;\n    }>;\n  };\n  reasoning_summary?: string; // 添加推理摘要支持\n}\n\nexport class OpenAIResponsesTransformer implements Transformer {\n  name = \"openai-responses\";\n  endPoint = \"/v1/responses\";\n\n  async transformRequestIn(\n    request: UnifiedChatRequest\n  ): Promise<UnifiedChatRequest> {\n    delete request.temperature;\n    delete request.max_tokens;\n\n    // 处理 reasoning 参数\n    if (request.reasoning) {\n      (request as any).reasoning = {\n        effort: request.reasoning.effort,\n        summary: \"detailed\",\n      };\n    }\n\n    const input: any[] = [];\n\n    const systemMessages = request.messages.filter(\n      (msg) => msg.role === \"system\"\n    );\n    if (systemMessages.length > 0) {\n      const firstSystem = systemMessages[0];\n      if (Array.isArray(firstSystem.content)) {\n        firstSystem.content.forEach((item) => {\n          let text = \"\";\n          if (typeof item === \"string\") {\n            text = item;\n          } else if (item && typeof item === \"object\" && \"text\" in item) {\n            text = (item as { text: string }).text;\n          }\n          input.push({\n            role: \"system\",\n            content: text,\n          });\n        });\n      } else {\n        (request as any).instructions = firstSystem.content;\n      }\n    }\n\n    request.messages.forEach((message) => {\n      if (message.role === \"system\") return;\n\n      if (Array.isArray(message.content)) {\n        const convertedContent = message.content\n          .map((content) => this.normalizeRequestContent(content, message.role))\n          .filter(\n            (content): content is Record<string, unknown> => content !== null\n          );\n\n        if (convertedContent.length > 0) {\n          (message as any).content = convertedContent;\n        } else {\n          delete (message as any).content;\n        }\n      }\n\n      if (message.role === \"tool\") {\n        const toolMessage: any = { ...message };\n        toolMessage.type = \"function_call_output\";\n        toolMessage.call_id = message.tool_call_id;\n        toolMessage.output = message.content;\n        delete toolMessage.cache_control;\n        delete toolMessage.role;\n        delete toolMessage.tool_call_id;\n        delete toolMessage.content;\n        input.push(toolMessage);\n        return;\n      }\n\n      if (message.role === \"assistant\" && Array.isArray(message.tool_calls)) {\n        message.tool_calls.forEach((tool) => {\n          input.push({\n            type: \"function_call\",\n            arguments: tool.function.arguments,\n            name: tool.function.name,\n            call_id: tool.id,\n          });\n        });\n        return;\n      }\n\n      input.push(message);\n    });\n\n    (request as any).input = input;\n    delete (request as any).messages;\n\n    if (Array.isArray(request.tools)) {\n      const webSearch = request.tools.find(\n        (tool) => tool.function.name === \"web_search\"\n      );\n\n      (request as any).tools = request.tools\n        .filter((tool) => tool.function.name !== \"web_search\")\n        .map((tool) => {\n          if (tool.function.name === \"WebSearch\") {\n            delete tool.function.parameters.properties.allowed_domains;\n          }\n          if (tool.function.name === \"Edit\") {\n            return {\n              type: tool.type,\n              name: tool.function.name,\n              description: tool.function.description,\n              parameters: {\n                ...tool.function.parameters,\n                required: [\n                  \"file_path\",\n                  \"old_string\",\n                  \"new_string\",\n                  \"replace_all\",\n                ],\n              },\n              strict: true,\n            };\n          }\n          return {\n            type: tool.type,\n            name: tool.function.name,\n            description: tool.function.description,\n            parameters: tool.function.parameters,\n          };\n        });\n\n      if (webSearch) {\n        (request as any).tools.push({\n          type: \"web_search\",\n        });\n      }\n    }\n\n    request.parallel_tool_calls = false;\n\n    return request;\n  }\n\n  async transformResponseOut(response: Response): Promise<Response> {\n    const contentType = response.headers.get(\"Content-Type\") || \"\";\n\n    if (contentType.includes(\"application/json\")) {\n      const jsonResponse: any = await response.json();\n\n      // 检查是否为responses API格式的JSON响应\n      if (jsonResponse.object === \"response\" && jsonResponse.output) {\n        // 将responses格式转换为chat格式\n        const chatResponse = this.convertResponseToChat(jsonResponse);\n        return new Response(JSON.stringify(chatResponse), {\n          status: response.status,\n          statusText: response.statusText,\n          headers: response.headers,\n        });\n      }\n\n      // 不是responses API格式，保持原样\n      return new Response(JSON.stringify(jsonResponse), {\n        status: response.status,\n        statusText: response.statusText,\n        headers: response.headers,\n      });\n    } else if (contentType.includes(\"text/event-stream\")) {\n      if (!response.body) {\n        return response;\n      }\n\n      const decoder = new TextDecoder();\n      const encoder = new TextEncoder();\n      let buffer = \"\"; // 用于缓冲不完整的数据\n      let isStreamEnded = false;\n\n      const transformer = this;\n      const stream = new ReadableStream({\n        async start(controller) {\n          const reader = response.body!.getReader();\n\n          // 索引跟踪变量，只有在事件类型切换时才增加索引\n          let currentIndex = -1;\n          let lastEventType = \"\";\n\n          // 获取当前应该使用的索引的函数\n          const getCurrentIndex = (eventType: string) => {\n            if (eventType !== lastEventType) {\n              currentIndex++;\n              lastEventType = eventType;\n            }\n            return currentIndex;\n          };\n\n          try {\n            while (true) {\n              const { done, value } = await reader.read();\n              if (done) {\n                if (!isStreamEnded) {\n                  // 发送结束标记\n                  const doneChunk = `data: [DONE]\\n\\n`;\n                  controller.enqueue(encoder.encode(doneChunk));\n                }\n                break;\n              }\n\n              const chunk = decoder.decode(value, { stream: true });\n              buffer += chunk;\n\n              // 处理缓冲区中完整的数据行\n              let lines = buffer.split(/\\r?\\n/);\n              buffer = lines.pop() || \"\"; // 最后一行可能不完整，保留在缓冲区\n\n              for (const line of lines) {\n                if (!line.trim()) continue;\n\n                try {\n                  if (line.startsWith(\"event: \")) {\n                    // 处理事件行，暂存以便与下一行数据配对\n                    continue;\n                  } else if (line.startsWith(\"data: \")) {\n                    const dataStr = line.slice(5).trim(); // 移除 \"data: \" 前缀\n                    if (dataStr === \"[DONE]\") {\n                      isStreamEnded = true;\n                      controller.enqueue(encoder.encode(`data: [DONE]\\n\\n`));\n                      continue;\n                    }\n\n                    try {\n                      const data: ResponsesStreamEvent = JSON.parse(dataStr);\n\n                      // 根据不同的事件类型转换为chat格式\n                      if (data.type === \"response.output_text.delta\") {\n                        // 将output_text.delta转换为chat格式\n                        const chatChunk = {\n                          id: data.item_id || \"chatcmpl-\" + Date.now(),\n                          object: \"chat.completion.chunk\",\n                          created: Math.floor(Date.now() / 1000),\n                          model: data.response?.model,\n                          choices: [\n                            {\n                              index: getCurrentIndex(data.type),\n                              delta: {\n                                content: data.delta || \"\",\n                              },\n                              finish_reason: null,\n                            },\n                          ],\n                        };\n\n                        controller.enqueue(\n                          encoder.encode(\n                            `data: ${JSON.stringify(chatChunk)}\\n\\n`\n                          )\n                        );\n                      } else if (\n                        data.type === \"response.output_item.added\" &&\n                        data.item?.type === \"function_call\"\n                      ) {\n                        // 处理function call开始 - 创建初始的tool call chunk\n                        const functionCallChunk = {\n                          id:\n                            data.item.call_id ||\n                            data.item.id ||\n                            \"chatcmpl-\" + Date.now(),\n                          object: \"chat.completion.chunk\",\n                          created: Math.floor(Date.now() / 1000),\n                          model: data.response?.model || \"gpt-5-codex-\",\n                          choices: [\n                            {\n                              index: getCurrentIndex(data.type),\n                              delta: {\n                                role: \"assistant\",\n                                tool_calls: [\n                                  {\n                                    index: 0,\n                                    id: data.item.call_id || data.item.id,\n                                    function: {\n                                      name: data.item.name || \"\",\n                                      arguments: \"\",\n                                    },\n                                    type: \"function\",\n                                  },\n                                ],\n                              },\n                              finish_reason: null,\n                            },\n                          ],\n                        };\n\n                        controller.enqueue(\n                          encoder.encode(\n                            `data: ${JSON.stringify(functionCallChunk)}\\n\\n`\n                          )\n                        );\n                      } else if (\n                        data.type === \"response.output_item.added\" &&\n                        data.item?.type === \"message\"\n                      ) {\n                        // 处理message item added事件\n                        const contentItems: MessageContent[] = [];\n                        (data.item.content || []).forEach((item: any) => {\n                          if (item.type === \"output_text\") {\n                            contentItems.push({\n                              type: \"text\",\n                              text: item.text || \"\",\n                            });\n                          }\n                        });\n\n                        const delta: any = { role: \"assistant\" };\n                        if (\n                          contentItems.length === 1 &&\n                          contentItems[0].type === \"text\"\n                        ) {\n                          delta.content = contentItems[0].text;\n                        } else if (contentItems.length > 0) {\n                          delta.content = contentItems;\n                        }\n                        if (delta.content) {\n                          const messageChunk = {\n                            id: data.item.id || \"chatcmpl-\" + Date.now(),\n                            object: \"chat.completion.chunk\",\n                            created: Math.floor(Date.now() / 1000),\n                            model: data.response?.model,\n                            choices: [\n                              {\n                                index: getCurrentIndex(data.type),\n                                delta,\n                                finish_reason: null,\n                              },\n                            ],\n                          };\n\n                          controller.enqueue(\n                            encoder.encode(\n                              `data: ${JSON.stringify(messageChunk)}\\n\\n`\n                            )\n                          );\n                        }\n                      } else if (\n                        data.type === \"response.output_text.annotation.added\"\n                      ) {\n                        const annotationChunk = {\n                          id: data.item_id || \"chatcmpl-\" + Date.now(),\n                          object: \"chat.completion.chunk\",\n                          created: Math.floor(Date.now() / 1000),\n                          model: data.response?.model || \"gpt-5-codex\",\n                          choices: [\n                            {\n                              index: getCurrentIndex(data.type),\n                              delta: {\n                                annotations: [\n                                  {\n                                    type: \"url_citation\",\n                                    url_citation: {\n                                      url: data.annotation?.url || \"\",\n                                      title: data.annotation?.title || \"\",\n                                      content: \"\",\n                                      start_index:\n                                        data.annotation?.start_index || 0,\n                                      end_index:\n                                        data.annotation?.end_index || 0,\n                                    },\n                                  },\n                                ],\n                              },\n                              finish_reason: null,\n                            },\n                          ],\n                        };\n\n                        controller.enqueue(\n                          encoder.encode(\n                            `data: ${JSON.stringify(annotationChunk)}\\n\\n`\n                          )\n                        );\n                      } else if (\n                        data.type === \"response.function_call_arguments.delta\"\n                      ) {\n                        // 处理function call参数增量\n                        const functionCallChunk = {\n                          id: data.item_id || \"chatcmpl-\" + Date.now(),\n                          object: \"chat.completion.chunk\",\n                          created: Math.floor(Date.now() / 1000),\n                          model: data.response?.model || \"gpt-5-codex-\",\n                          choices: [\n                            {\n                              index: getCurrentIndex(data.type),\n                              delta: {\n                                tool_calls: [\n                                  {\n                                    index: 0,\n                                    function: {\n                                      arguments: data.delta || \"\",\n                                    },\n                                  },\n                                ],\n                              },\n                              finish_reason: null,\n                            },\n                          ],\n                        };\n\n                        controller.enqueue(\n                          encoder.encode(\n                            `data: ${JSON.stringify(functionCallChunk)}\\n\\n`\n                          )\n                        );\n                      } else if (data.type === \"response.completed\") {\n                        // 发送结束标记 - 检查是否是tool_calls完成\n                        const finishReason = data.response?.output?.some(\n                          (item: any) => item.type === \"function_call\"\n                        )\n                          ? \"tool_calls\"\n                          : \"stop\";\n\n                        const endChunk = {\n                          id: data.response?.id || \"chatcmpl-\" + Date.now(),\n                          object: \"chat.completion.chunk\",\n                          created: Math.floor(Date.now() / 1000),\n                          model: data.response?.model || \"gpt-5-codex-\",\n                          choices: [\n                            {\n                              index: 0,\n                              delta: {},\n                              finish_reason: finishReason,\n                            },\n                          ],\n                        };\n\n                        controller.enqueue(\n                          encoder.encode(\n                            `data: ${JSON.stringify(endChunk)}\\n\\n`\n                          )\n                        );\n                        isStreamEnded = true;\n                      } else if (\n                        data.type === \"response.reasoning_summary_text.delta\"\n                      ) {\n                        // 处理推理文本，将其转换为 thinking delta 格式\n                        const thinkingChunk = {\n                          id: data.item_id || \"chatcmpl-\" + Date.now(),\n                          object: \"chat.completion.chunk\",\n                          created: Math.floor(Date.now() / 1000),\n                          model: data.response?.model,\n                          choices: [\n                            {\n                              index: getCurrentIndex(data.type),\n                              delta: {\n                                thinking: {\n                                  content: data.delta || \"\",\n                                },\n                              },\n                              finish_reason: null,\n                            },\n                          ],\n                        };\n\n                        controller.enqueue(\n                          encoder.encode(\n                            `data: ${JSON.stringify(thinkingChunk)}\\n\\n`\n                          )\n                        );\n                      } else if (\n                        data.type === \"response.reasoning_summary_part.done\" &&\n                        data.part\n                      ) {\n                        const thinkingChunk = {\n                          id: data.item_id || \"chatcmpl-\" + Date.now(),\n                          object: \"chat.completion.chunk\",\n                          created: Math.floor(Date.now() / 1000),\n                          model: data.response?.model,\n                          choices: [\n                            {\n                              index: currentIndex,\n                              delta: {\n                                thinking: {\n                                  signature: data.item_id,\n                                },\n                              },\n                              finish_reason: null,\n                            },\n                          ],\n                        };\n\n                        controller.enqueue(\n                          encoder.encode(\n                            `data: ${JSON.stringify(thinkingChunk)}\\n\\n`\n                          )\n                        );\n                      }\n                    } catch (e) {\n                      // 如果JSON解析失败，传递原始行\n                      controller.enqueue(encoder.encode(line + \"\\n\"));\n                    }\n                  } else {\n                    // 传递其他行\n                    controller.enqueue(encoder.encode(line + \"\\n\"));\n                  }\n                } catch (error) {\n                  console.error(\"Error processing line:\", line, error);\n                  // 如果解析失败，直接传递原始行\n                  controller.enqueue(encoder.encode(line + \"\\n\"));\n                }\n              }\n            }\n\n            // 处理缓冲区中剩余的数据\n            if (buffer.trim()) {\n              controller.enqueue(encoder.encode(buffer + \"\\n\"));\n            }\n\n            // 确保流结束时发送结束标记\n            if (!isStreamEnded) {\n              const doneChunk = `data: [DONE]\\n\\n`;\n              controller.enqueue(encoder.encode(doneChunk));\n            }\n          } catch (error) {\n            console.error(\"Stream error:\", error);\n            controller.error(error);\n          } finally {\n            try {\n              reader.releaseLock();\n            } catch (e) {\n              console.error(\"Error releasing reader lock:\", e);\n            }\n            controller.close();\n          }\n        },\n      });\n\n      return new Response(stream, {\n        status: response.status,\n        statusText: response.statusText,\n        headers: {\n          \"Content-Type\": \"text/event-stream\",\n          \"Cache-Control\": \"no-cache\",\n          Connection: \"keep-alive\",\n          \"Access-Control-Allow-Origin\": \"*\",\n        },\n      });\n    }\n\n    return response;\n  }\n\n  private normalizeRequestContent(content: any, role: string | undefined) {\n    // 克隆内容对象并删除缓存控制字段\n    const clone = { ...content };\n    delete clone.cache_control;\n\n    if (content.type === \"text\") {\n      return {\n        type: role === \"assistant\" ? \"output_text\" : \"input_text\",\n        text: content.text,\n      };\n    }\n\n    if (content.type === \"image_url\") {\n      console.log(content);\n      const imagePayload: Record<string, unknown> = {\n        type: role === \"assistant\" ? \"output_image\" : \"input_image\",\n      };\n\n      if (typeof content.image_url?.url === \"string\") {\n        imagePayload.image_url = content.image_url.url;\n      }\n\n      return imagePayload;\n    }\n\n    return null;\n  }\n\n  private convertResponseToChat(responseData: ResponsesAPIPayload): any {\n    // 从output数组中提取不同类型的输出\n    const messageOutput = responseData.output?.find(\n      (item) => item.type === \"message\"\n    );\n    const functionCallOutput = responseData.output?.find(\n      (item) => item.type === \"function_call\"\n    );\n    let annotations;\n    if (\n      messageOutput?.content?.length &&\n      messageOutput?.content[0].annotations\n    ) {\n      annotations = messageOutput.content[0].annotations.map((item) => {\n        return {\n          type: \"url_citation\",\n          url_citation: {\n            url: item.url || \"\",\n            title: item.title || \"\",\n            content: \"\",\n            start_index: item.start_index || 0,\n            end_index: item.end_index || 0,\n          },\n        };\n      });\n    }\n\n    this.logger.debug({\n      data: annotations,\n      type: \"url_citation\",\n    });\n\n    let messageContent: string | MessageContent[] | null = null;\n    let toolCalls = null;\n    let thinking = null;\n\n    // 处理推理内容\n    if (messageOutput && messageOutput.reasoning) {\n      thinking = {\n        content: messageOutput.reasoning,\n      };\n    }\n\n    if (messageOutput && messageOutput.content) {\n      // 分离文本和图片内容\n      const textParts: string[] = [];\n      const imageParts: MessageContent[] = [];\n\n      messageOutput.content.forEach((item: any) => {\n        if (item.type === \"output_text\") {\n          textParts.push(item.text || \"\");\n        } else if (item.type === \"output_image\") {\n          const imageContent = this.buildImageContent({\n            url: item.image_url,\n            mime_type: item.mime_type,\n          });\n          if (imageContent) {\n            imageParts.push(imageContent);\n          }\n        } else if (item.type === \"output_image_base64\") {\n          const imageContent = this.buildImageContent({\n            b64_json: item.image_base64,\n            mime_type: item.mime_type,\n          });\n          if (imageContent) {\n            imageParts.push(imageContent);\n          }\n        }\n      });\n\n      // 构建最终内容\n      if (imageParts.length > 0) {\n        // 如果有图片，将所有内容组合成数组\n        const contentArray: MessageContent[] = [];\n        if (textParts.length > 0) {\n          contentArray.push({\n            type: \"text\",\n            text: textParts.join(\"\"),\n          });\n        }\n        contentArray.push(...imageParts);\n        messageContent = contentArray;\n      } else {\n        // 如果只有文本，返回字符串\n        messageContent = textParts.join(\"\");\n      }\n    }\n\n    if (functionCallOutput) {\n      // 处理function_call类型的输出\n      toolCalls = [\n        {\n          id: functionCallOutput.call_id || functionCallOutput.id,\n          function: {\n            name: functionCallOutput.name,\n            arguments: functionCallOutput.arguments,\n          },\n          type: \"function\",\n        },\n      ];\n    }\n\n    // 构建chat格式的响应\n    const chatResponse = {\n      id: responseData.id || \"chatcmpl-\" + Date.now(),\n      object: \"chat.completion\",\n      created: responseData.created_at,\n      model: responseData.model,\n      choices: [\n        {\n          index: 0,\n          message: {\n            role: \"assistant\",\n            content: messageContent || null,\n            tool_calls: toolCalls,\n            thinking: thinking,\n            annotations: annotations,\n          },\n          logprobs: null,\n          finish_reason: toolCalls ? \"tool_calls\" : \"stop\",\n        },\n      ],\n      usage: responseData.usage\n        ? {\n            prompt_tokens: responseData.usage.input_tokens || 0,\n            completion_tokens: responseData.usage.output_tokens || 0,\n            total_tokens: responseData.usage.total_tokens || 0,\n          }\n        : null,\n    };\n\n    return chatResponse;\n  }\n\n  private buildImageContent(source: {\n    url?: string;\n    b64_json?: string;\n    mime_type?: string;\n  }): MessageContent | null {\n    if (!source) return null;\n\n    if (source.url || source.b64_json) {\n      return {\n        type: \"image_url\",\n        image_url: {\n          url: source.url || \"\",\n          b64_json: source.b64_json,\n        },\n        media_type: source.mime_type,\n      } as MessageContent;\n    }\n\n    return null;\n  }\n}\n"
  },
  {
    "path": "packages/core/src/transformer/openai.transformer.ts",
    "content": "import { Transformer } from \"@/types/transformer\";\n\nexport class OpenAITransformer implements Transformer {\n  name = \"OpenAI\";\n  endPoint = \"/v1/chat/completions\";\n}\n"
  },
  {
    "path": "packages/core/src/transformer/openrouter.transformer.ts",
    "content": "import { UnifiedChatRequest } from \"@/types/llm\";\nimport { Transformer, TransformerOptions } from \"../types/transformer\";\nimport { v4 as uuidv4 } from \"uuid\";\n\nexport class OpenrouterTransformer implements Transformer {\n  static TransformerName = \"openrouter\";\n\n  constructor(private readonly options?: TransformerOptions) {}\n\n  async transformRequestIn(\n    request: UnifiedChatRequest\n  ): Promise<UnifiedChatRequest> {\n    if (!request.model.includes(\"claude\")) {\n      request.messages.forEach((msg) => {\n        if (Array.isArray(msg.content)) {\n          msg.content.forEach((item: any) => {\n            if (item.cache_control) {\n              delete item.cache_control;\n            }\n            if (item.type === \"image_url\") {\n              if (!item.image_url.url.startsWith(\"http\")) {\n                item.image_url.url = `${item.image_url.url}`;\n              }\n              delete item.media_type;\n            }\n          });\n        } else if (msg.cache_control) {\n          delete msg.cache_control;\n        }\n      });\n    } else {\n      request.messages.forEach((msg) => {\n        if (Array.isArray(msg.content)) {\n          msg.content.forEach((item: any) => {\n            if (item.type === \"image_url\") {\n              if (!item.image_url.url.startsWith(\"http\")) {\n                item.image_url.url = `data:${item.media_type};base64,${item.image_url.url}`;\n              }\n              delete item.media_type;\n            }\n          });\n        }\n      });\n    }\n    Object.assign(request, this.options || {});\n    return request;\n  }\n\n  async transformResponseOut(response: Response): Promise<Response> {\n    if (response.headers.get(\"Content-Type\")?.includes(\"application/json\")) {\n      const jsonResponse = await response.json();\n      return new Response(JSON.stringify(jsonResponse), {\n        status: response.status,\n        statusText: response.statusText,\n        headers: response.headers,\n      });\n    } else if (response.headers.get(\"Content-Type\")?.includes(\"stream\")) {\n      if (!response.body) {\n        return response;\n      }\n\n      const decoder = new TextDecoder();\n      const encoder = new TextEncoder();\n\n      let hasTextContent = false;\n      let reasoningContent = \"\";\n      let isReasoningComplete = false;\n      let hasToolCall = false;\n      let buffer = \"\"; // 用于缓冲不完整的数据\n\n      const stream = new ReadableStream({\n        async start(controller) {\n          const reader = response.body!.getReader();\n          const processBuffer = (\n            buffer: string,\n            controller: ReadableStreamDefaultController,\n            encoder: TextEncoder\n          ) => {\n            const lines = buffer.split(\"\\n\");\n            for (const line of lines) {\n              if (line.trim()) {\n                controller.enqueue(encoder.encode(line + \"\\n\"));\n              }\n            }\n          };\n\n          const processLine = (\n            line: string,\n            context: {\n              controller: ReadableStreamDefaultController;\n              encoder: TextEncoder;\n              hasTextContent: () => boolean;\n              setHasTextContent: (val: boolean) => void;\n              reasoningContent: () => string;\n              appendReasoningContent: (content: string) => void;\n              isReasoningComplete: () => boolean;\n              setReasoningComplete: (val: boolean) => void;\n            }\n          ) => {\n            const { controller, encoder } = context;\n\n            if (line.startsWith(\"data: \") && line.trim() !== \"data: [DONE]\") {\n              const jsonStr = line.slice(6);\n              try {\n                const data = JSON.parse(jsonStr);\n                if (data.usage) {\n                  this.logger?.debug(\n                    { usage: data.usage, hasToolCall },\n                    \"usage\"\n                  );\n                  data.choices[0].finish_reason = hasToolCall\n                    ? \"tool_calls\"\n                    : \"stop\";\n                }\n\n                if (data.choices?.[0]?.finish_reason === \"error\") {\n                  controller.enqueue(\n                    encoder.encode(\n                      `data: ${JSON.stringify({\n                        error: data.choices?.[0].error,\n                      })}\\n\\n`\n                    )\n                  );\n                }\n\n                if (\n                  data.choices?.[0]?.delta?.content &&\n                  !context.hasTextContent()\n                ) {\n                  context.setHasTextContent(true);\n                }\n\n                // Extract reasoning_content from delta\n                if (data.choices?.[0]?.delta?.reasoning) {\n                  context.appendReasoningContent(\n                    data.choices[0].delta.reasoning\n                  );\n                  const thinkingChunk = {\n                    ...data,\n                    choices: [\n                      {\n                        ...data.choices?.[0],\n                        delta: {\n                          ...data.choices[0].delta,\n                          thinking: {\n                            content: data.choices[0].delta.reasoning,\n                          },\n                        },\n                      },\n                    ],\n                  };\n                  if (thinkingChunk.choices?.[0]?.delta) {\n                    delete thinkingChunk.choices[0].delta.reasoning;\n                  }\n                  const thinkingLine = `data: ${JSON.stringify(\n                    thinkingChunk\n                  )}\\n\\n`;\n                  controller.enqueue(encoder.encode(thinkingLine));\n                  return;\n                }\n\n                // Check if reasoning is complete\n                if (\n                  data.choices?.[0]?.delta?.content &&\n                  context.reasoningContent() &&\n                  !context.isReasoningComplete()\n                ) {\n                  context.setReasoningComplete(true);\n                  const signature = Date.now().toString();\n\n                  const thinkingChunk = {\n                    ...data,\n                    choices: [\n                      {\n                        ...data.choices?.[0],\n                        delta: {\n                          ...data.choices[0].delta,\n                          content: null,\n                          thinking: {\n                            content: context.reasoningContent(),\n                            signature: signature,\n                          },\n                        },\n                      },\n                    ],\n                  };\n                  if (thinkingChunk.choices?.[0]?.delta) {\n                    delete thinkingChunk.choices[0].delta.reasoning;\n                  }\n                  const thinkingLine = `data: ${JSON.stringify(\n                    thinkingChunk\n                  )}\\n\\n`;\n                  controller.enqueue(encoder.encode(thinkingLine));\n                }\n\n                if (data.choices?.[0]?.delta?.reasoning) {\n                  delete data.choices[0].delta.reasoning;\n                }\n                if (\n                  data.choices?.[0]?.delta?.tool_calls?.length &&\n                  !Number.isNaN(\n                    parseInt(data.choices?.[0]?.delta?.tool_calls[0].id, 10)\n                  )\n                ) {\n                  data.choices?.[0]?.delta?.tool_calls.forEach((tool: any) => {\n                    tool.id = `call_${uuidv4()}`;\n                  });\n                }\n\n                if (\n                  data.choices?.[0]?.delta?.tool_calls?.length &&\n                  !hasToolCall\n                ) {\n                  hasToolCall = true;\n                }\n\n                if (\n                  data.choices?.[0]?.delta?.tool_calls?.length &&\n                  context.hasTextContent()\n                ) {\n                  if (typeof data.choices[0].index === \"number\") {\n                    data.choices[0].index += 1;\n                  } else {\n                    data.choices[0].index = 1;\n                  }\n                }\n\n                const modifiedLine = `data: ${JSON.stringify(data)}\\n\\n`;\n                controller.enqueue(encoder.encode(modifiedLine));\n              } catch (e) {\n                // 如果JSON解析失败，可能是数据不完整，将原始行传递下去\n                controller.enqueue(encoder.encode(line + \"\\n\"));\n              }\n            } else {\n              // Pass through non-data lines (like [DONE])\n              controller.enqueue(encoder.encode(line + \"\\n\"));\n            }\n          };\n\n          try {\n            while (true) {\n              const { done, value } = await reader.read();\n              if (done) {\n                // 处理缓冲区中剩余的数据\n                if (buffer.trim()) {\n                  processBuffer(buffer, controller, encoder);\n                }\n                break;\n              }\n\n              // 检查value是否有效\n              if (!value || value.length === 0) {\n                continue;\n              }\n\n              let chunk;\n              try {\n                chunk = decoder.decode(value, { stream: true });\n              } catch (decodeError) {\n                console.warn(\"Failed to decode chunk\", decodeError);\n                continue;\n              }\n\n              if (chunk.length === 0) {\n                continue;\n              }\n\n              buffer += chunk;\n\n              // 如果缓冲区过大，进行处理避免内存泄漏\n              if (buffer.length > 1000000) {\n                // 1MB 限制\n                console.warn(\n                  \"Buffer size exceeds limit, processing partial data\"\n                );\n                const lines = buffer.split(\"\\n\");\n                buffer = lines.pop() || \"\";\n\n                for (const line of lines) {\n                  if (line.trim()) {\n                    try {\n                      processLine(line, {\n                        controller,\n                        encoder,\n                        hasTextContent: () => hasTextContent,\n                        setHasTextContent: (val) => (hasTextContent = val),\n                        reasoningContent: () => reasoningContent,\n                        appendReasoningContent: (content) =>\n                          (reasoningContent += content),\n                        isReasoningComplete: () => isReasoningComplete,\n                        setReasoningComplete: (val) =>\n                          (isReasoningComplete = val),\n                      });\n                    } catch (error) {\n                      console.error(\"Error processing line:\", line, error);\n                      // 如果解析失败，直接传递原始行\n                      controller.enqueue(encoder.encode(line + \"\\n\"));\n                    }\n                  }\n                }\n                continue;\n              }\n\n              // 处理缓冲区中完整的数据行\n              const lines = buffer.split(\"\\n\");\n              buffer = lines.pop() || \"\"; // 最后一行可能不完整，保留在缓冲区\n\n              for (const line of lines) {\n                if (!line.trim()) continue;\n\n                try {\n                  processLine(line, {\n                    controller,\n                    encoder,\n                    hasTextContent: () => hasTextContent,\n                    setHasTextContent: (val) => (hasTextContent = val),\n                    reasoningContent: () => reasoningContent,\n                    appendReasoningContent: (content) =>\n                      (reasoningContent += content),\n                    isReasoningComplete: () => isReasoningComplete,\n                    setReasoningComplete: (val) => (isReasoningComplete = val),\n                  });\n                } catch (error) {\n                  console.error(\"Error processing line:\", line, error);\n                  // 如果解析失败，直接传递原始行\n                  controller.enqueue(encoder.encode(line + \"\\n\"));\n                }\n              }\n            }\n          } catch (error) {\n            console.error(\"Stream error:\", error);\n            controller.error(error);\n          } finally {\n            try {\n              reader.releaseLock();\n            } catch (e) {\n              console.error(\"Error releasing reader lock:\", e);\n            }\n            controller.close();\n          }\n        },\n      });\n\n      return new Response(stream, {\n        status: response.status,\n        statusText: response.statusText,\n        headers: {\n          \"Content-Type\": \"text/event-stream\",\n          \"Cache-Control\": \"no-cache\",\n          Connection: \"keep-alive\",\n        },\n      });\n    }\n\n    return response;\n  }\n}\n"
  },
  {
    "path": "packages/core/src/transformer/reasoning.transformer.ts",
    "content": "import { UnifiedChatRequest } from \"@/types/llm\";\nimport { Transformer, TransformerOptions } from \"../types/transformer\";\n\nexport class ReasoningTransformer implements Transformer {\n  static TransformerName = \"reasoning\";\n  enable: any;\n\n  constructor(private readonly options?: TransformerOptions) {\n    this.enable = this.options?.enable ?? true;\n  }\n\n  async transformRequestIn(\n    request: UnifiedChatRequest\n  ): Promise<UnifiedChatRequest> {\n    if (!this.enable) {\n      request.thinking = {\n        type: \"disabled\",\n        budget_tokens: -1,\n      };\n      request.enable_thinking = false;\n      return request;\n    }\n    if (request.reasoning) {\n      request.thinking = {\n        type: \"enabled\",\n        budget_tokens: request.reasoning.max_tokens,\n      };\n      request.enable_thinking = true;\n    }\n    return request;\n  }\n\n  async transformResponseOut(response: Response): Promise<Response> {\n    if (!this.enable) return response;\n    if (response.headers.get(\"Content-Type\")?.includes(\"application/json\")) {\n      const jsonResponse = await response.json();\n      if (jsonResponse.choices[0]?.message.reasoning_content) {\n        jsonResponse.thinking = {\n          content: jsonResponse.choices[0]?.message.reasoning_content\n        }\n      }\n      // Handle non-streaming response if needed\n      return new Response(JSON.stringify(jsonResponse), {\n        status: response.status,\n        statusText: response.statusText,\n        headers: response.headers,\n      });\n    } else if (response.headers.get(\"Content-Type\")?.includes(\"stream\")) {\n      if (!response.body) {\n        return response;\n      }\n\n      const decoder = new TextDecoder();\n      const encoder = new TextEncoder();\n      let reasoningContent = \"\";\n      let isReasoningComplete = false;\n      let buffer = \"\"; // Buffer for incomplete data\n\n      const stream = new ReadableStream({\n        async start(controller) {\n          const reader = response.body!.getReader();\n\n          // Process buffer function\n          const processBuffer = (\n            buffer: string,\n            controller: ReadableStreamDefaultController,\n            encoder: TextEncoder\n          ) => {\n            const lines = buffer.split(\"\\n\");\n            for (const line of lines) {\n              if (line.trim()) {\n                controller.enqueue(encoder.encode(line + \"\\n\"));\n              }\n            }\n          };\n\n          // Process line function\n          const processLine = (\n            line: string,\n            context: {\n              controller: ReadableStreamDefaultController;\n              encoder: typeof TextEncoder;\n              reasoningContent: () => string;\n              appendReasoningContent: (content: string) => void;\n              isReasoningComplete: () => boolean;\n              setReasoningComplete: (val: boolean) => void;\n            }\n          ) => {\n            const { controller, encoder } = context;\n\n            this.logger?.debug({ line }, `Processing reason line`);\n\n            if (line.startsWith(\"data: \") && line.trim() !== \"data: [DONE]\") {\n              try {\n                const data = JSON.parse(line.slice(6));\n                console.log(JSON.stringify(data))\n\n                // Extract reasoning_content from delta\n                if (data.choices?.[0]?.delta?.reasoning_content) {\n                  context.appendReasoningContent(\n                    data.choices[0].delta.reasoning_content\n                  );\n                  const thinkingChunk = {\n                    ...data,\n                    choices: [\n                      {\n                        ...data.choices[0],\n                        delta: {\n                          ...data.choices[0].delta,\n                          thinking: {\n                            content: data.choices[0].delta.reasoning_content,\n                          },\n                        },\n                      },\n                    ],\n                  };\n                  delete thinkingChunk.choices[0].delta.reasoning_content;\n                  const thinkingLine = `data: ${JSON.stringify(\n                    thinkingChunk\n                  )}\\n\\n`;\n                  controller.enqueue(encoder.encode(thinkingLine));\n                  return;\n                }\n\n                // Check if reasoning is complete (when delta has content but no reasoning_content)\n                if (\n                  (data.choices?.[0]?.delta?.content ||\n                    data.choices?.[0]?.delta?.tool_calls) &&\n                  context.reasoningContent() &&\n                  !context.isReasoningComplete()\n                ) {\n                  context.setReasoningComplete(true);\n                  const signature = Date.now().toString();\n\n                  // Create a new chunk with thinking block\n                  const thinkingChunk = {\n                    ...data,\n                    choices: [\n                      {\n                        ...data.choices[0],\n                        delta: {\n                          ...data.choices[0].delta,\n                          content: null,\n                          thinking: {\n                            content: context.reasoningContent(),\n                            signature: signature,\n                          },\n                        },\n                      },\n                    ],\n                  };\n                  delete thinkingChunk.choices[0].delta.reasoning_content;\n                  // Send the thinking chunk\n                  const thinkingLine = `data: ${JSON.stringify(\n                    thinkingChunk\n                  )}\\n\\n`;\n                  controller.enqueue(encoder.encode(thinkingLine));\n                }\n\n                if (data.choices?.[0]?.delta?.reasoning_content) {\n                  delete data.choices[0].delta.reasoning_content;\n                }\n\n                // Send the modified chunk\n                if (\n                  data.choices?.[0]?.delta &&\n                  Object.keys(data.choices[0].delta).length > 0\n                ) {\n                  if (context.isReasoningComplete()) {\n                    data.choices[0].index++;\n                  }\n                  const modifiedLine = `data: ${JSON.stringify(data)}\\n\\n`;\n                  controller.enqueue(encoder.encode(modifiedLine));\n                }\n              } catch (e) {\n                // If JSON parsing fails, pass through the original line\n                controller.enqueue(encoder.encode(line + \"\\n\"));\n              }\n            } else {\n              // Pass through non-data lines (like [DONE])\n              controller.enqueue(encoder.encode(line + \"\\n\"));\n            }\n          };\n\n          try {\n            while (true) {\n              const { done, value } = await reader.read();\n              if (done) {\n                // Process remaining data in buffer\n                if (buffer.trim()) {\n                  processBuffer(buffer, controller, encoder);\n                }\n                break;\n              }\n\n              const chunk = decoder.decode(value, { stream: true });\n              buffer += chunk;\n\n              // Process complete lines from buffer\n              const lines = buffer.split(\"\\n\");\n              buffer = lines.pop() || \"\"; // Keep incomplete line in buffer\n\n              for (const line of lines) {\n                if (!line.trim()) continue;\n\n                try {\n                  processLine(line, {\n                    controller,\n                    encoder: encoder,\n                    reasoningContent: () => reasoningContent,\n                    appendReasoningContent: (content) =>\n                      (reasoningContent += content),\n                    isReasoningComplete: () => isReasoningComplete,\n                    setReasoningComplete: (val) => (isReasoningComplete = val),\n                  });\n                } catch (error) {\n                  console.error(\"Error processing line:\", line, error);\n                  // Pass through original line if parsing fails\n                  controller.enqueue(encoder.encode(line + \"\\n\"));\n                }\n              }\n            }\n          } catch (error) {\n            console.error(\"Stream error:\", error);\n            controller.error(error);\n          } finally {\n            try {\n              reader.releaseLock();\n            } catch (e) {\n              console.error(\"Error releasing reader lock:\", e);\n            }\n            controller.close();\n          }\n        },\n      });\n\n      return new Response(stream, {\n        status: response.status,\n        statusText: response.statusText,\n        headers: {\n          \"Content-Type\": \"text/event-stream\",\n          \"Cache-Control\": \"no-cache\",\n          Connection: \"keep-alive\",\n        },\n      });\n    }\n\n    return response;\n  }\n}\n"
  },
  {
    "path": "packages/core/src/transformer/sampling.transformer.ts",
    "content": "import { UnifiedChatRequest } from \"../types/llm\";\nimport { Transformer, TransformerOptions } from \"../types/transformer\";\n\nexport class SamplingTransformer implements Transformer {\n  static TransformerName = \"sampling\";\n\n  max_tokens: number;\n  temperature: number;\n  top_p: number;\n  top_k: number;\n  repetition_penalty: number;\n\n  constructor(private readonly options?: TransformerOptions) {\n    this.max_tokens = this.options?.max_tokens;\n    this.temperature = this.options?.temperature;\n    this.top_p = this.options?.top_p;\n    this.top_k = this.options?.top_k;\n    this.repetition_penalty = this.options?.repetition_penalty;\n  }\n\n  async transformRequestIn(\n    request: UnifiedChatRequest\n  ): Promise<UnifiedChatRequest> {\n    if (request.max_tokens && request.max_tokens > this.max_tokens) {\n      request.max_tokens = this.max_tokens;\n    }\n    if (typeof this.temperature !== \"undefined\") {\n      request.temperature = this.temperature;\n    }\n    if (typeof this.top_p !== \"undefined\") {\n      request.top_p = this.top_p;\n    }\n    if (typeof this.top_k !== \"undefined\") {\n      request.top_k = this.top_k;\n    }\n    if (typeof this.repetition_penalty !== \"undefined\") {\n      request.repetition_penalty = this.repetition_penalty;\n    }\n    return request;\n  }\n}\n"
  },
  {
    "path": "packages/core/src/transformer/streamoptions.transformer.ts",
    "content": "import { UnifiedChatRequest } from \"../types/llm\";\nimport { Transformer, TransformerOptions } from \"../types/transformer\";\n\nexport class StreamOptionsTransformer implements Transformer {\n  name = \"streamoptions\";\n\n  async transformRequestIn(\n    request: UnifiedChatRequest\n  ): Promise<UnifiedChatRequest> {\n    if (!request.stream) return request;\n    request.stream_options = {\n      include_usage: true,\n    };\n    return request;\n  }\n}\n"
  },
  {
    "path": "packages/core/src/transformer/tooluse.transformer.ts",
    "content": "import { UnifiedChatRequest } from \"../types/llm\";\nimport { Transformer } from \"../types/transformer\";\n\nexport class TooluseTransformer implements Transformer {\n  name = \"tooluse\";\n\n  transformRequestIn(request: UnifiedChatRequest): UnifiedChatRequest {\n    request.messages.push({\n      role: \"system\",\n      content: `<system-reminder>Tool mode is active. The user expects you to proactively execute the most suitable tool to help complete the task. \nBefore invoking a tool, you must carefully evaluate whether it matches the current task. If no available tool is appropriate for the task, you MUST call the \\`ExitTool\\` to exit tool mode — this is the only valid way to terminate tool mode.\nAlways prioritize completing the user's task effectively and efficiently by using tools whenever appropriate.</system-reminder>`,\n    });\n    if (request.tools?.length) {\n      request.tool_choice = \"required\";\n      request.tools.push({\n        type: \"function\",\n        function: {\n          name: \"ExitTool\",\n          description: `Use this tool when you are in tool mode and have completed the task. This is the only valid way to exit tool mode.\nIMPORTANT: Before using this tool, ensure that none of the available tools are applicable to the current task. You must evaluate all available options — only if no suitable tool can help you complete the task should you use ExitTool to terminate tool mode.\nExamples:\n1. Task: \"Use a tool to summarize this document\" — Do not use ExitTool if a summarization tool is available.\n2. Task: \"What’s the weather today?\" — If no tool is available to answer, use ExitTool after reasoning that none can fulfill the task.`,\n          parameters: {\n            type: \"object\",\n            properties: {\n              response: {\n                type: \"string\",\n                description:\n                  \"Your response will be forwarded to the user exactly as returned — the tool will not modify or post-process it in any way.\",\n              },\n            },\n            required: [\"response\"],\n          },\n        },\n      });\n    }\n    return request;\n  }\n\n  async transformResponseOut(response: Response): Promise<Response> {\n    if (response.headers.get(\"Content-Type\")?.includes(\"application/json\")) {\n      const jsonResponse = await response.json();\n      if (\n        jsonResponse?.choices?.[0]?.message.tool_calls?.length &&\n        jsonResponse?.choices?.[0]?.message.tool_calls[0]?.function?.name ===\n          \"ExitTool\"\n      ) {\n        const toolCall = jsonResponse?.choices[0]?.message.tool_calls[0];\n        const toolArguments = JSON.parse(toolCall.function.arguments || \"{}\");\n        jsonResponse.choices[0].message.content = toolArguments.response || \"\";\n        delete jsonResponse.choices[0].message.tool_calls;\n      }\n\n      // Handle non-streaming response if needed\n      return new Response(JSON.stringify(jsonResponse), {\n        status: response.status,\n        statusText: response.statusText,\n        headers: response.headers,\n      });\n    } else if (response.headers.get(\"Content-Type\")?.includes(\"stream\")) {\n      if (!response.body) {\n        return response;\n      }\n\n      const decoder = new TextDecoder();\n      const encoder = new TextEncoder();\n      let exitToolIndex = -1;\n      let exitToolResponse = \"\";\n      let buffer = \"\"; // Buffer for incomplete data\n\n      const stream = new ReadableStream({\n        async start(controller) {\n          const reader = response.body!.getReader();\n\n          const processBuffer = (\n            buffer: string,\n            controller: ReadableStreamDefaultController,\n            encoder: TextEncoder\n          ) => {\n            const lines = buffer.split(\"\\n\");\n            for (const line of lines) {\n              if (line.trim()) {\n                controller.enqueue(encoder.encode(line + \"\\n\"));\n              }\n            }\n          };\n\n          const processLine = (\n            line: string,\n            context: {\n              controller: ReadableStreamDefaultController;\n              encoder: TextEncoder;\n              exitToolIndex: () => number;\n              setExitToolIndex: (val: number) => void;\n              exitToolResponse: () => string;\n              appendExitToolResponse: (content: string) => void;\n            }\n          ) => {\n            const {\n              controller,\n              encoder,\n              exitToolIndex,\n              setExitToolIndex,\n              appendExitToolResponse,\n            } = context;\n\n            if (\n              line.startsWith(\"data: \") &&\n              line.trim() !== \"data: [DONE]\"\n            ) {\n              try {\n                const data = JSON.parse(line.slice(6));\n\n                if (data.choices[0]?.delta?.tool_calls?.length) {\n                  const toolCall = data.choices[0].delta.tool_calls[0];\n\n                  if (toolCall.function?.name === \"ExitTool\") {\n                    setExitToolIndex(toolCall.index);\n                    return;\n                  } else if (\n                    exitToolIndex() > -1 &&\n                    toolCall.index === exitToolIndex() &&\n                    toolCall.function.arguments\n                  ) {\n                    appendExitToolResponse(toolCall.function.arguments);\n                    try {\n                      const response = JSON.parse(context.exitToolResponse());\n                      data.choices = [\n                        {\n                          delta: {\n                            role: \"assistant\",\n                            content: response.response || \"\",\n                          },\n                        },\n                      ];\n                      const modifiedLine = `data: ${JSON.stringify(\n                        data\n                      )}\\n\\n`;\n                      controller.enqueue(encoder.encode(modifiedLine));\n                    } catch (e) {}\n                    return;\n                  }\n                }\n\n                if (\n                  data.choices?.[0]?.delta &&\n                  Object.keys(data.choices[0].delta).length > 0\n                ) {\n                  const modifiedLine = `data: ${JSON.stringify(data)}\\n\\n`;\n                  controller.enqueue(encoder.encode(modifiedLine));\n                }\n              } catch (e) {\n                // If JSON parsing fails, pass through the original line\n                controller.enqueue(encoder.encode(line + \"\\n\"));\n              }\n            } else {\n              // Pass through non-data lines (like [DONE])\n              controller.enqueue(encoder.encode(line + \"\\n\"));\n            }\n          };\n\n          try {\n            while (true) {\n              const { done, value } = await reader.read();\n              if (done) {\n                if (buffer.trim()) {\n                  processBuffer(buffer, controller, encoder);\n                }\n                break;\n              }\n              const chunk = decoder.decode(value, { stream: true });\n              buffer += chunk;\n              const lines = buffer.split(\"\\n\");\n              buffer = lines.pop() || \"\";\n              for (const line of lines) {\n                if (!line.trim()) continue;\n                try {\n                  processLine(line, {\n                    controller,\n                    encoder,\n                    exitToolIndex: () => exitToolIndex,\n                    setExitToolIndex: (val) => (exitToolIndex = val),\n                    exitToolResponse: () => exitToolResponse,\n                    appendExitToolResponse: (content) =>\n                      (exitToolResponse += content),\n                  });\n                } catch (error) {\n                  console.error(\"Error processing line:\", line, error);\n                  // If parsing fails, pass through the original line\n                  controller.enqueue(encoder.encode(line + \"\\n\"));\n                }\n              }\n            }\n          } catch (error) {\n            console.error(\"Stream error:\", error);\n            controller.error(error);\n          } finally {\n            try {\n              reader.releaseLock();\n            } catch (e) {\n              console.error(\"Error releasing reader lock:\", e);\n            }\n            controller.close();\n          }\n        },\n      });\n\n      return new Response(stream, {\n        status: response.status,\n        statusText: response.statusText,\n        headers: {\n          \"Content-Type\": \"text/event-stream\",\n          \"Cache-Control\": \"no-cache\",\n          Connection: \"keep-alive\",\n        },\n      });\n    }\n\n    return response;\n  }\n}\n"
  },
  {
    "path": "packages/core/src/transformer/vercel.transformer.ts",
    "content": "import { UnifiedChatRequest } from \"@/types/llm\";\nimport { Transformer, TransformerOptions } from \"../types/transformer\";\nimport { v4 as uuidv4 } from \"uuid\";\n\nexport class VercelTransformer implements Transformer {\n  static TransformerName = \"vercel\";\n  endPoint = \"/v1/chat/completions\";\n\n  constructor(private readonly options?: TransformerOptions) {}\n\n  async transformRequestIn(\n    request: UnifiedChatRequest\n  ): Promise<UnifiedChatRequest> {\n    if (!request.model.includes(\"claude\")) {\n      request.messages.forEach((msg) => {\n        if (Array.isArray(msg.content)) {\n          msg.content.forEach((item: any) => {\n            if (item.cache_control) {\n              delete item.cache_control;\n            }\n            if (item.type === \"image_url\") {\n              if (!item.image_url.url.startsWith(\"http\")) {\n                item.image_url.url = `data:${item.media_type};base64,${item.image_url.url}`;\n              }\n              delete item.media_type;\n            }\n          });\n        } else if (msg.cache_control) {\n          delete msg.cache_control;\n        }\n      });\n    } else {\n      request.messages.forEach((msg) => {\n        if (Array.isArray(msg.content)) {\n          msg.content.forEach((item: any) => {\n            if (item.type === \"image_url\") {\n              if (!item.image_url.url.startsWith(\"http\")) {\n                item.image_url.url = `data:${item.media_type};base64,${item.image_url.url}`;\n              }\n              delete item.media_type;\n            }\n          });\n        }\n      });\n    }\n    Object.assign(request, this.options || {});\n    return request;\n  }\n\n  async transformResponseOut(response: Response): Promise<Response> {\n    if (response.headers.get(\"Content-Type\")?.includes(\"application/json\")) {\n      const jsonResponse = await response.json();\n      return new Response(JSON.stringify(jsonResponse), {\n        status: response.status,\n        statusText: response.statusText,\n        headers: response.headers,\n      });\n    } else if (response.headers.get(\"Content-Type\")?.includes(\"stream\")) {\n      if (!response.body) {\n        return response;\n      }\n\n      const decoder = new TextDecoder();\n      const encoder = new TextEncoder();\n\n      let hasTextContent = false;\n      let reasoningContent = \"\";\n      let isReasoningComplete = false;\n      let hasToolCall = false;\n      let buffer = \"\"; // Buffer for incomplete data\n\n      const stream = new ReadableStream({\n        async start(controller) {\n          const reader = response.body!.getReader();\n          const processBuffer = (\n            buffer: string,\n            controller: ReadableStreamDefaultController,\n            encoder: TextEncoder\n          ) => {\n            const lines = buffer.split(\"\\n\");\n            for (const line of lines) {\n              if (line.trim()) {\n                controller.enqueue(encoder.encode(line + \"\\n\"));\n              }\n            }\n          };\n\n          const processLine = (\n            line: string,\n            context: {\n              controller: ReadableStreamDefaultController;\n              encoder: TextEncoder;\n              hasTextContent: () => boolean;\n              setHasTextContent: (val: boolean) => void;\n              reasoningContent: () => string;\n              appendReasoningContent: (content: string) => void;\n              isReasoningComplete: () => boolean;\n              setReasoningComplete: (val: boolean) => void;\n            }\n          ) => {\n            const { controller, encoder } = context;\n\n            if (line.startsWith(\"data: \") && line.trim() !== \"data: [DONE]\") {\n              const jsonStr = line.slice(6);\n              try {\n                const data = JSON.parse(jsonStr);\n                if (data.usage) {\n                  this.logger?.debug(\n                    { usage: data.usage, hasToolCall },\n                    \"usage\"\n                  );\n                  data.choices[0].finish_reason = hasToolCall\n                    ? \"tool_calls\"\n                    : \"stop\";\n                }\n\n                if (data.choices?.[0]?.finish_reason === \"error\") {\n                  controller.enqueue(\n                    encoder.encode(\n                      `data: ${JSON.stringify({\n                        error: data.choices?.[0].error,\n                      })}\\n\\n`\n                    )\n                  );\n                }\n\n                if (\n                  data.choices?.[0]?.delta?.content &&\n                  !context.hasTextContent()\n                ) {\n                  context.setHasTextContent(true);\n                }\n\n                // Extract reasoning_content from delta\n                if (data.choices?.[0]?.delta?.reasoning) {\n                  context.appendReasoningContent(\n                    data.choices[0].delta.reasoning\n                  );\n                  const thinkingChunk = {\n                    ...data,\n                    choices: [\n                      {\n                        ...data.choices?.[0],\n                        delta: {\n                          ...data.choices[0].delta,\n                          thinking: {\n                            content: data.choices[0].delta.reasoning,\n                          },\n                        },\n                      },\n                    ],\n                  };\n                  if (thinkingChunk.choices?.[0]?.delta) {\n                    delete thinkingChunk.choices[0].delta.reasoning;\n                  }\n                  const thinkingLine = `data: ${JSON.stringify(\n                    thinkingChunk\n                  )}\\n\\n`;\n                  controller.enqueue(encoder.encode(thinkingLine));\n                  return;\n                }\n\n                // Check if reasoning is complete\n                if (\n                  data.choices?.[0]?.delta?.content &&\n                  context.reasoningContent() &&\n                  !context.isReasoningComplete()\n                ) {\n                  context.setReasoningComplete(true);\n                  const signature = Date.now().toString();\n\n                  const thinkingChunk = {\n                    ...data,\n                    choices: [\n                      {\n                        ...data.choices?.[0],\n                        delta: {\n                          ...data.choices[0].delta,\n                          content: null,\n                          thinking: {\n                            content: context.reasoningContent(),\n                            signature: signature,\n                          },\n                        },\n                      },\n                    ],\n                  };\n                  if (thinkingChunk.choices?.[0]?.delta) {\n                    delete thinkingChunk.choices[0].delta.reasoning;\n                  }\n                  const thinkingLine = `data: ${JSON.stringify(\n                    thinkingChunk\n                  )}\\n\\n`;\n                  controller.enqueue(encoder.encode(thinkingLine));\n                }\n\n                if (data.choices?.[0]?.delta?.reasoning) {\n                  delete data.choices[0].delta.reasoning;\n                }\n                if (\n                  data.choices?.[0]?.delta?.tool_calls?.length &&\n                  !Number.isNaN(\n                    parseInt(data.choices?.[0]?.delta?.tool_calls[0].id, 10)\n                  )\n                ) {\n                  data.choices?.[0]?.delta?.tool_calls.forEach((tool: any) => {\n                    tool.id = `call_${uuidv4()}`;\n                  });\n                }\n\n                if (\n                  data.choices?.[0]?.delta?.tool_calls?.length &&\n                  !hasToolCall\n                ) {\n                  hasToolCall = true;\n                }\n\n                if (\n                  data.choices?.[0]?.delta?.tool_calls?.length &&\n                  context.hasTextContent()\n                ) {\n                  if (typeof data.choices[0].index === \"number\") {\n                    data.choices[0].index += 1;\n                  } else {\n                    data.choices[0].index = 1;\n                  }\n                }\n\n                const modifiedLine = `data: ${JSON.stringify(data)}\\n\\n`;\n                controller.enqueue(encoder.encode(modifiedLine));\n              } catch (e) {\n                // If JSON parsing fails, data might be incomplete, pass through the original line\n                controller.enqueue(encoder.encode(line + \"\\n\"));\n              }\n            } else {\n              // Pass through non-data lines (like [DONE])\n              controller.enqueue(encoder.encode(line + \"\\n\"));\n            }\n          };\n\n          try {\n            while (true) {\n              const { done, value } = await reader.read();\n              if (done) {\n                // Process remaining data in buffer\n                if (buffer.trim()) {\n                  processBuffer(buffer, controller, encoder);\n                }\n                break;\n              }\n\n              // Check if value is valid\n              if (!value || value.length === 0) {\n                continue;\n              }\n\n              let chunk;\n              try {\n                chunk = decoder.decode(value, { stream: true });\n              } catch (decodeError) {\n                console.warn(\"Failed to decode chunk\", decodeError);\n                continue;\n              }\n\n              if (chunk.length === 0) {\n                continue;\n              }\n\n              buffer += chunk;\n\n              // Process buffer if it gets too large to avoid memory leaks\n              if (buffer.length > 1000000) {\n                // 1MB limit\n                console.warn(\n                  \"Buffer size exceeds limit, processing partial data\"\n                );\n                const lines = buffer.split(\"\\n\");\n                buffer = lines.pop() || \"\";\n\n                for (const line of lines) {\n                  if (line.trim()) {\n                    try {\n                      processLine(line, {\n                        controller,\n                        encoder,\n                        hasTextContent: () => hasTextContent,\n                        setHasTextContent: (val) => (hasTextContent = val),\n                        reasoningContent: () => reasoningContent,\n                        appendReasoningContent: (content) =>\n                          (reasoningContent += content),\n                        isReasoningComplete: () => isReasoningComplete,\n                        setReasoningComplete: (val) =>\n                          (isReasoningComplete = val),\n                      });\n                    } catch (error) {\n                      console.error(\"Error processing line:\", line, error);\n                      // If parsing fails, pass through the original line\n                      controller.enqueue(encoder.encode(line + \"\\n\"));\n                    }\n                  }\n                }\n                continue;\n              }\n\n              // Process complete lines in buffer\n              const lines = buffer.split(\"\\n\");\n              buffer = lines.pop() || \"\"; // Last line might be incomplete, keep in buffer\n\n              for (const line of lines) {\n                if (!line.trim()) continue;\n\n                try {\n                  processLine(line, {\n                    controller,\n                    encoder,\n                    hasTextContent: () => hasTextContent,\n                    setHasTextContent: (val) => (hasTextContent = val),\n                    reasoningContent: () => reasoningContent,\n                    appendReasoningContent: (content) =>\n                      (reasoningContent += content),\n                    isReasoningComplete: () => isReasoningComplete,\n                    setReasoningComplete: (val) => (isReasoningComplete = val),\n                  });\n                } catch (error) {\n                  console.error(\"Error processing line:\", line, error);\n                  // If parsing fails, pass through the original line\n                  controller.enqueue(encoder.encode(line + \"\\n\"));\n                }\n              }\n            }\n          } catch (error) {\n            console.error(\"Stream error:\", error);\n            controller.error(error);\n          } finally {\n            try {\n              reader.releaseLock();\n            } catch (e) {\n              console.error(\"Error releasing reader lock:\", e);\n            }\n            controller.close();\n          }\n        },\n      });\n\n      return new Response(stream, {\n        status: response.status,\n        statusText: response.statusText,\n        headers: {\n          \"Content-Type\": \"text/event-stream\",\n          \"Cache-Control\": \"no-cache\",\n          Connection: \"keep-alive\",\n        },\n      });\n    }\n\n    return response;\n  }\n}"
  },
  {
    "path": "packages/core/src/transformer/vertex-claude.transformer.ts",
    "content": "import { LLMProvider, UnifiedChatRequest } from \"../types/llm\";\nimport { Transformer } from \"../types/transformer\";\nimport {\n  buildRequestBody,\n  transformRequestOut,\n  transformResponseOut,\n} from \"../utils/vertex-claude.util\";\n\nasync function getAccessToken(): Promise<string> {\n  try {\n    const { GoogleAuth } = await import('google-auth-library');\n\n    const auth = new GoogleAuth({\n      scopes: ['https://www.googleapis.com/auth/cloud-platform']\n    });\n\n    const client = await auth.getClient();\n    const accessToken = await client.getAccessToken();\n    return accessToken.token || '';\n  } catch (error) {\n    console.error('Error getting access token:', error);\n    throw new Error('Failed to get access token for Vertex AI. Please ensure you have set up authentication using one of these methods:\\n' +\n      '1. Set GOOGLE_APPLICATION_CREDENTIALS to point to service account key file\\n' +\n      '2. Run \"gcloud auth application-default login\"\\n' +\n      '3. Use Google Cloud environment with default service account');\n  }\n}\n\n\n\nexport class VertexClaudeTransformer implements Transformer {\n  name = \"vertex-claude\";\n\n  async transformRequestIn(\n    request: UnifiedChatRequest,\n    provider: LLMProvider\n  ): Promise<Record<string, any>> {\n    let projectId = process.env.GOOGLE_CLOUD_PROJECT;\n    const location = process.env.GOOGLE_CLOUD_LOCATION || 'us-east5';\n\n    if (!projectId && process.env.GOOGLE_APPLICATION_CREDENTIALS) {\n      try {\n        const fs = await import('fs');\n        const keyContent = fs.readFileSync(process.env.GOOGLE_APPLICATION_CREDENTIALS, 'utf8');\n        const credentials = JSON.parse(keyContent);\n        if (credentials && credentials.project_id) {\n          projectId = credentials.project_id;\n        }\n      } catch (error) {\n        console.error('Error extracting project_id from GOOGLE_APPLICATION_CREDENTIALS:', error);\n      }\n    }\n\n    if (!projectId) {\n      throw new Error('Project ID is required for Vertex AI. Set GOOGLE_CLOUD_PROJECT environment variable or ensure project_id is in GOOGLE_APPLICATION_CREDENTIALS file.');\n    }\n\n    const accessToken = await getAccessToken();\n    return {\n      body: buildRequestBody(request),\n      config: {\n        url: new URL(\n          `/v1/projects/${projectId}/locations/${location}/publishers/anthropic/models/${request.model}:${request.stream ? \"streamRawPredict\" : \"rawPredict\"}`,\n          `https://${location}-aiplatform.googleapis.com`\n        ).toString(),\n        headers: {\n          \"Authorization\": `Bearer ${accessToken}`,\n          \"Content-Type\": \"application/json\",\n        },\n      },\n    };\n  }\n\n  async transformRequestOut(request: Record<string, any>): Promise<UnifiedChatRequest> {\n    return transformRequestOut(request);\n  }\n\n  async transformResponseOut(response: Response): Promise<Response> {\n    return transformResponseOut(response, this.name, this.logger);\n  }\n}\n"
  },
  {
    "path": "packages/core/src/transformer/vertex-gemini.transformer.ts",
    "content": "import { LLMProvider, UnifiedChatRequest } from \"../types/llm\";\nimport { Transformer } from \"../types/transformer\";\nimport {\n  buildRequestBody,\n  transformRequestOut,\n  transformResponseOut,\n} from \"../utils/gemini.util\";\n\nasync function getAccessToken(): Promise<string> {\n  try {\n    const { GoogleAuth } = await import('google-auth-library');\n\n    const auth = new GoogleAuth({\n      scopes: ['https://www.googleapis.com/auth/cloud-platform']\n    });\n\n    const client = await auth.getClient();\n    const accessToken = await client.getAccessToken();\n    return accessToken.token || '';\n  } catch (error) {\n    console.error('Error getting access token:', error);\n    throw new Error('Failed to get access token for Vertex AI. Please ensure you have set up authentication using one of these methods:\\n' +\n      '1. Set GOOGLE_APPLICATION_CREDENTIALS to point to service account key file\\n' +\n      '2. Run \"gcloud auth application-default login\"\\n' +\n      '3. Use Google Cloud environment with default service account');\n  }\n}\n\nexport class VertexGeminiTransformer implements Transformer {\n  name = \"vertex-gemini\";\n\n  async transformRequestIn(\n    request: UnifiedChatRequest,\n    provider: LLMProvider\n  ): Promise<Record<string, any>> {\n    let projectId = process.env.GOOGLE_CLOUD_PROJECT;\n    const location = process.env.GOOGLE_CLOUD_LOCATION || 'us-central1';\n\n    if (!projectId && process.env.GOOGLE_APPLICATION_CREDENTIALS) {\n      try {\n        const fs = await import('fs');\n        const keyContent = fs.readFileSync(process.env.GOOGLE_APPLICATION_CREDENTIALS, 'utf8');\n        const credentials = JSON.parse(keyContent);\n        if (credentials && credentials.project_id) {\n          projectId = credentials.project_id;\n        }\n      } catch (error) {\n        console.error('Error extracting project_id from GOOGLE_APPLICATION_CREDENTIALS:', error);\n      }\n    }\n\n    if (!projectId) {\n      throw new Error('Project ID is required for Vertex AI. Set GOOGLE_CLOUD_PROJECT environment variable or ensure project_id is in GOOGLE_APPLICATION_CREDENTIALS file.');\n    }\n\n    const accessToken = await getAccessToken();\n    return {\n      body: buildRequestBody(request),\n      config: {\n        url: new URL(\n          `./v1beta1/projects/${projectId}/locations/${location}/publishers/google/models/${request.model}:${request.stream ? \"streamGenerateContent\" : \"generateContent\"}`,\n            provider.baseUrl.endsWith('/') ? provider.baseUrl : provider.baseUrl + '/' || `https://${location}-aiplatform.googleapis.com`\n        ),\n        headers: {\n          \"Authorization\": `Bearer ${accessToken}`,\n          \"x-goog-api-key\": undefined,\n        },\n      },\n    };\n  }\n\n  async transformRequestOut(request: Record<string, any>): Promise<UnifiedChatRequest> {\n    return transformRequestOut(request);\n  }\n\n  async transformResponseOut(response: Response): Promise<Response> {\n    return transformResponseOut(response, this.name);\n  }\n}\n"
  },
  {
    "path": "packages/core/src/types/llm.ts",
    "content": "import type { ChatCompletionMessageParam as OpenAIMessage } from \"openai/resources/chat/completions\";\nimport type { MessageParam as AnthropicMessage } from \"@anthropic-ai/sdk/resources/messages\";\nimport type {\n  ChatCompletion,\n  ChatCompletionChunk,\n} from \"openai/resources/chat/completions\";\nimport type {\n  Message,\n  MessageStreamEvent,\n} from \"@anthropic-ai/sdk/resources/messages\";\nimport type { ChatCompletionTool } from \"openai/resources/chat/completions\";\nimport type { Tool as AnthropicTool } from \"@anthropic-ai/sdk/resources/messages\";\nimport { Transformer } from \"./transformer\";\nimport type { ProviderTokenizerConfig } from \"./tokenizer\";\n\nexport interface UrlCitation {\n  url: string;\n  title: string;\n  content: string;\n  start_index: number;\n  end_index: number;\n}\nexport interface Annotation {\n  type: \"url_citation\";\n  url_citation?: UrlCitation;\n}\n\n// 内容类型定义\nexport interface TextContent {\n  type: \"text\";\n  text: string;\n  cache_control?: {\n    type?: string;\n  };\n}\n\nexport interface ImageContent {\n  type: \"image_url\";\n  image_url: {\n    url: string;\n  };\n  media_type: string;\n}\n\nexport type MessageContent = TextContent | ImageContent;\n\n// 统一的消息接口\nexport interface UnifiedMessage {\n  role: \"user\" | \"assistant\" | \"system\" | \"tool\";\n  content: string | null | MessageContent[];\n  tool_calls?: Array<{\n    id: string;\n    type: \"function\";\n    function: {\n      name: string;\n      arguments: string;\n    };\n  }>;\n  tool_call_id?: string;\n  cache_control?: {\n    type?: string;\n  };\n  thinking?: {\n    content: string;\n    signature?: string;\n  };\n}\n\n// 统一的工具定义接口\nexport interface UnifiedTool {\n  type: \"function\";\n  function: {\n    name: string;\n    description: string;\n    parameters: {\n      type: \"object\";\n      properties: Record<string, any>;\n      required?: string[];\n      additionalProperties?: boolean;\n      $schema?: string;\n    };\n  };\n}\n\nexport type ThinkLevel = \"none\" | \"low\" | \"medium\" | \"high\";\n\n// 统一的请求接口\nexport interface UnifiedChatRequest {\n  messages: UnifiedMessage[];\n  model: string;\n  max_tokens?: number;\n  temperature?: number;\n  stream?: boolean;\n  tools?: UnifiedTool[];\n  tool_choice?:\n    | \"auto\"\n    | \"none\"\n    | \"required\"\n    | string\n    | { type: \"function\"; function: { name: string } };\n  reasoning?: {\n    // OpenAI-style\n    effort?: ThinkLevel;\n\n    // Anthropic-style\n    max_tokens?: number;\n\n    enabled?: boolean;\n  };\n}\n\n// 统一的响应接口\nexport interface UnifiedChatResponse {\n  id: string;\n  model: string;\n  content: string | null;\n  usage?: {\n    prompt_tokens: number;\n    completion_tokens: number;\n    total_tokens: number;\n  };\n  tool_calls?: Array<{\n    id: string;\n    type: \"function\";\n    function: {\n      name: string;\n      arguments: string;\n    };\n  }>;\n  annotations?: Annotation[];\n}\n\n// 流式响应相关类型\nexport interface StreamChunk {\n  id: string;\n  object: string;\n  created: number;\n  model: string;\n  choices?: Array<{\n    index: number;\n    delta: {\n      role?: string;\n      content?: string;\n      thinking?: {\n        content?: string;\n        signature?: string;\n      };\n      tool_calls?: Array<{\n        id?: string;\n        type?: \"function\";\n        function?: {\n          name?: string;\n          arguments?: string;\n        };\n      }>;\n      annotations?: Annotation[];\n    };\n    finish_reason?: string | null;\n  }>;\n}\n\n// Anthropic 流式事件类型\nexport type AnthropicStreamEvent = MessageStreamEvent;\n\n// OpenAI 流式块类型\nexport type OpenAIStreamChunk = ChatCompletionChunk;\n\n// OpenAI 特定类型\nexport interface OpenAIChatRequest {\n  messages: OpenAIMessage[];\n  model: string;\n  max_tokens?: number;\n  temperature?: number;\n  stream?: boolean;\n  tools?: ChatCompletionTool[];\n  tool_choice?:\n    | \"auto\"\n    | \"none\"\n    | { type: \"function\"; function: { name: string } };\n}\n\n// Anthropic 特定类型\nexport interface AnthropicChatRequest {\n  messages: AnthropicMessage[];\n  model: string;\n  max_tokens: number;\n  temperature?: number;\n  stream?: boolean;\n  system?: string;\n  tools?: AnthropicTool[];\n  tool_choice?: { type: \"auto\" } | { type: \"tool\"; name: string };\n}\n\n// 转换选项\nexport interface ConversionOptions {\n  targetProvider: \"openai\" | \"anthropic\";\n  sourceProvider: \"openai\" | \"anthropic\";\n}\n\nexport interface LLMProvider {\n  name: string;\n  baseUrl: string;\n  apiKey: string;\n  models: string[];\n  transformer?: {\n    [key: string]: {\n      use?: Transformer[];\n    };\n  } & {\n    use?: Transformer[];\n  };\n}\n\nexport type RegisterProviderRequest = LLMProvider;\n\nexport interface ModelRoute {\n  provider: string;\n  model: string;\n  fullModel: string;\n}\n\nexport interface RequestRouteInfo {\n  provider: LLMProvider;\n  originalModel: string;\n  targetModel: string;\n}\n\nexport interface ConfigProvider {\n  name: string;\n  api_base_url: string;\n  api_key: string;\n  models: string[];\n  transformer: {\n    use?: string[] | Array<any>[];\n  } & {\n    [key: string]: {\n      use?: string[] | Array<any>[];\n    };\n  };\n  tokenizer?: ProviderTokenizerConfig;\n}\n"
  },
  {
    "path": "packages/core/src/types/tokenizer.d.ts",
    "content": "/**\n * Tokenizer configuration types\n */\n\n/**\n * Tokenizer type enum\n */\nexport type TokenizerType = 'tiktoken' | 'huggingface' | 'api';\n\n/**\n * API request format type\n */\nexport type ApiRequestFormat = 'standard' | 'openai' | 'anthropic' | 'custom';\n\n/**\n * Configuration for a specific tokenizer\n */\nexport interface TokenizerConfig {\n  /** Type of tokenizer to use */\n  type: TokenizerType;\n\n  /** For tiktoken: encoding name (e.g., 'cl100k_base', 'p50k_base') */\n  encoding?: string;\n\n  /** For huggingface: model ID (e.g., 'Qwen/Qwen2.5-Coder-32B-Instruct') */\n  model?: string;\n\n  /** For API-based tokenizers: complete API URL (e.g., 'https://api.example.com/v1/tokenize') */\n  url?: string;\n\n  /** For API-based tokenizers: API key */\n  apiKey?: string;\n\n  /** For API-based tokenizers: request format (default: 'standard') */\n  requestFormat?: ApiRequestFormat;\n\n  /** For API-based tokenizers: response field path to extract token count (default: 'token_count') */\n  responseField?: string;\n\n  /** For API-based tokenizers: custom request headers */\n  headers?: Record<string, string>;\n\n  /** Fallback tokenizer type if this one fails */\n  fallback?: TokenizerType;\n}\n\n/**\n * Options for TokenizerService\n */\nexport interface TokenizerOptions {\n  /** Enable token count caching */\n  cacheEnabled?: boolean;\n\n  /** Maximum cache size */\n  cacheSize?: number;\n\n  /** Timeout for API calls (in milliseconds) */\n  timeout?: number;\n}\n\n/**\n * Token count request structure (matches existing calculateTokenCount interface)\n */\nexport interface TokenizeRequest {\n  messages: Array<{\n    role: string;\n    content: string | Array<{\n      type: string;\n      text?: string;\n      input?: any;\n      content?: string | any;\n    }>;\n  }>;\n  system?: string | Array<{\n    type: string;\n    text?: string | string[];\n  }>;\n  tools?: Array<{\n    name: string;\n    description?: string;\n    input_schema: object;\n  }>;\n}\n\n/**\n * Result from token counting operation\n */\nexport interface TokenizerResult {\n  /** Total token count */\n  tokenCount: number;\n\n  /** Name/type of tokenizer used */\n  tokenizerUsed: string;\n\n  /** Whether the result was from cache */\n  cached: boolean;\n}\n\n/**\n * Abstract interface for all tokenizers\n */\nexport interface ITokenizer {\n  /** Tokenizer type identifier */\n  readonly type: string;\n\n  /** Human-readable tokenizer name */\n  readonly name: string;\n\n  /** Initialize the tokenizer (async for loading models, etc.) */\n  initialize(): Promise<void>;\n\n  /** Count tokens for a given request */\n  countTokens(request: TokenizeRequest): Promise<number>;\n\n  /** Encode text to tokens (for simple text tokenization) */\n  encodeText?(text: string): number[];\n\n  /** Check if tokenizer is initialized */\n  isInitialized(): boolean;\n\n  /** Clean up resources */\n  dispose(): void;\n}\n\n/**\n * Provider-specific tokenizer configuration\n */\nexport interface ProviderTokenizerConfig {\n  /** Default tokenizer for all models in this provider */\n  default?: TokenizerConfig;\n\n  /** Model-specific tokenizer configurations */\n  models?: Record<string, TokenizerConfig>;\n}\n"
  },
  {
    "path": "packages/core/src/types/transformer.ts",
    "content": "import { LLMProvider, UnifiedChatRequest } from \"./llm\";\n\nexport interface TransformerOptions {\n  [key: string]: any;\n}\n\ninterface TransformerWithStaticName {\n  new (options?: TransformerOptions): Transformer;\n  TransformerName?: string;\n}\n\n\ninterface TransformerWithInstanceName {\n  new (): Transformer;\n  name?: never;\n}\n\nexport type TransformerConstructor = TransformerWithStaticName;\n\nexport interface TransformerContext {\n  [key: string]: any;\n}\n\nexport type Transformer = {\n  transformRequestIn?: (\n    request: UnifiedChatRequest,\n    provider: LLMProvider,\n    context: TransformerContext,\n  ) => Promise<Record<string, any>>;\n  transformResponseIn?: (response: Response, context?: TransformerContext) => Promise<Response>;\n\n  // 将请求格式转换为通用的格式\n  transformRequestOut?: (request: any, context: TransformerContext) => Promise<UnifiedChatRequest>;\n  // 将相应格式转换为通用的格式\n  transformResponseOut?: (response: Response, context: TransformerContext) => Promise<Response>;\n\n  endPoint?: string;\n  name?: string;\n  auth?: (request: any, provider: LLMProvider, context: TransformerContext) => Promise<any>;\n  \n  // Logger for transformer\n  logger?: any;\n};\n"
  },
  {
    "path": "packages/core/src/utils/cache.ts",
    "content": "// LRU cache for session usage\n\nexport interface Usage {\n  input_tokens: number;\n  output_tokens: number;\n}\n\nclass LRUCache<K, V> {\n  private capacity: number;\n  private cache: Map<K, V>;\n\n  constructor(capacity: number) {\n    this.capacity = capacity;\n    this.cache = new Map<K, V>();\n  }\n\n  get(key: K): V | undefined {\n    if (!this.cache.has(key)) {\n      return undefined;\n    }\n    const value = this.cache.get(key) as V;\n    // Move to end to mark as recently used\n    this.cache.delete(key);\n    this.cache.set(key, value);\n    return value;\n  }\n\n  put(key: K, value: V): void {\n    if (this.cache.has(key)) {\n      // If key exists, delete it to update its position\n      this.cache.delete(key);\n    } else if (this.cache.size >= this.capacity) {\n      // If cache is full, delete the least recently used item\n      const leastRecentlyUsedKey = this.cache.keys().next().value;\n      if (leastRecentlyUsedKey !== undefined) {\n        this.cache.delete(leastRecentlyUsedKey);\n      }\n    }\n    this.cache.set(key, value);\n  }\n\n  values(): V[] {\n    return Array.from(this.cache.values());\n  }\n}\n\nexport const sessionUsageCache = new LRUCache<string, Usage>(100);\n"
  },
  {
    "path": "packages/core/src/utils/converter.ts",
    "content": "import type { ChatCompletionMessageParam as OpenAIMessage } from \"openai/resources/chat/completions\";\nimport type { MessageParam as AnthropicMessage } from \"@anthropic-ai/sdk/resources/messages\";\nimport type { ChatCompletionTool } from \"openai/resources/chat/completions\";\nimport type { Tool as AnthropicTool } from \"@anthropic-ai/sdk/resources/messages\";\nimport {\n  UnifiedMessage,\n  UnifiedChatRequest,\n  UnifiedTool,\n  OpenAIChatRequest,\n  AnthropicChatRequest,\n  ConversionOptions,\n} from \"../types/llm\";\n\n// Simple logger function\nfunction log(...args: any[]) {\n  // Can be extended to use a proper logger\n  console.log(...args);\n}\n\nexport function convertToolsToOpenAI(\n  tools: UnifiedTool[]\n): ChatCompletionTool[] {\n  return tools.map((tool) => ({\n    type: \"function\" as const,\n    function: {\n      name: tool.function.name,\n      description: tool.function.description,\n      parameters: tool.function.parameters,\n    },\n  }));\n}\n\nexport function convertToolsToAnthropic(tools: UnifiedTool[]): AnthropicTool[] {\n  return tools.map((tool) => ({\n    name: tool.function.name,\n    description: tool.function.description,\n    input_schema: tool.function.parameters,\n  }));\n}\n\nexport function convertToolsFromOpenAI(\n  tools: ChatCompletionTool[]\n): UnifiedTool[] {\n  return tools.map((tool) => ({\n    type: \"function\" as const,\n    function: {\n      name: tool.function.name,\n      description: tool.function.description || \"\",\n      parameters: tool.function.parameters as any,\n    },\n  }));\n}\n\nexport function convertToolsFromAnthropic(\n  tools: AnthropicTool[]\n): UnifiedTool[] {\n  return tools.map((tool) => ({\n    type: \"function\" as const,\n    function: {\n      name: tool.name,\n      description: tool.description || \"\",\n      parameters: tool.input_schema as any,\n    },\n  }));\n}\n\nexport function convertToOpenAI(\n  request: UnifiedChatRequest\n): OpenAIChatRequest {\n  const messages: OpenAIMessage[] = [];\n  const toolResponsesQueue: Map<string, any> = new Map(); // For storing tool responses\n\n  request.messages.forEach((msg) => {\n    if (msg.role === \"tool\" && msg.tool_call_id) {\n      if (!toolResponsesQueue.has(msg.tool_call_id)) {\n        toolResponsesQueue.set(msg.tool_call_id, []);\n      }\n      toolResponsesQueue.get(msg.tool_call_id).push({\n        role: \"tool\",\n        content: msg.content,\n        tool_call_id: msg.tool_call_id,\n      });\n    }\n  });\n\n  for (let i = 0; i < request.messages.length; i++) {\n    const msg = request.messages[i];\n\n    if (msg.role === \"tool\") {\n      continue;\n    }\n\n    const message: any = {\n      role: msg.role,\n      content: msg.content,\n    };\n\n    if (msg.tool_calls && msg.tool_calls.length > 0) {\n      message.tool_calls = msg.tool_calls;\n      if (message.content === null) {\n        message.content = null;\n      }\n    }\n\n    messages.push(message);\n\n    if (\n      msg.role === \"assistant\" &&\n      msg.tool_calls &&\n      msg.tool_calls.length > 0\n    ) {\n      for (const toolCall of msg.tool_calls) {\n        if (toolResponsesQueue.has(toolCall.id)) {\n          const responses = toolResponsesQueue.get(toolCall.id);\n\n          responses.forEach((response) => {\n            messages.push(response);\n          });\n\n          toolResponsesQueue.delete(toolCall.id);\n        } else {\n          messages.push({\n            role: \"tool\",\n            content: JSON.stringify({\n              success: true,\n              message: \"Tool call executed successfully\",\n              tool_call_id: toolCall.id,\n            }),\n            tool_call_id: toolCall.id,\n          } as any);\n        }\n      }\n    }\n  }\n\n  if (toolResponsesQueue.size > 0) {\n    for (const [id, responses] of toolResponsesQueue.entries()) {\n      responses.forEach((response) => {\n        messages.push(response);\n      });\n    }\n  }\n\n  const result: any = {\n    messages,\n    model: request.model,\n    max_tokens: request.max_tokens,\n    temperature: request.temperature,\n    stream: request.stream,\n  };\n\n  if (request.tools && request.tools.length > 0) {\n    result.tools = convertToolsToOpenAI(request.tools);\n    if (request.tool_choice) {\n      if (request.tool_choice === \"auto\" || request.tool_choice === \"none\") {\n        result.tool_choice = request.tool_choice;\n      } else {\n        result.tool_choice = {\n          type: \"function\",\n          function: { name: request.tool_choice },\n        };\n      }\n    }\n  }\n\n  return result;\n}\n\n\n\nfunction isToolCallContent(content: string): boolean {\n  try {\n    const parsed = JSON.parse(content);\n    return (\n      Array.isArray(parsed) &&\n      parsed.some((item) => item.type === \"tool_use\" && item.id && item.name)\n    );\n  } catch {\n    return false;\n  }\n}\n\nexport function convertFromOpenAI(\n  request: OpenAIChatRequest\n): UnifiedChatRequest {\n  const messages: UnifiedMessage[] = request.messages.map((msg) => {\n    if (\n      msg.role === \"assistant\" &&\n      typeof msg.content === \"string\" &&\n      isToolCallContent(msg.content)\n    ) {\n      try {\n        const toolCalls = JSON.parse(msg.content);\n        const convertedToolCalls = toolCalls.map((call: any) => ({\n          id: call.id,\n          type: \"function\" as const,\n          function: {\n            name: call.name,\n            arguments: JSON.stringify(call.input || {}),\n          },\n        }));\n\n        return {\n          role: msg.role as \"user\" | \"assistant\" | \"system\",\n          content: null,\n          tool_calls: convertedToolCalls,\n        };\n      } catch (error) {\n        return {\n          role: msg.role as \"user\" | \"assistant\" | \"system\",\n          content: msg.content,\n        };\n      }\n    }\n\n    if (msg.role === \"tool\") {\n      return {\n        role: msg.role as \"tool\",\n        content:\n          typeof msg.content === \"string\"\n            ? msg.content\n            : JSON.stringify(msg.content),\n        tool_call_id: (msg as any).tool_call_id,\n      };\n    }\n\n    return {\n      role: msg.role as \"user\" | \"assistant\" | \"system\",\n      content:\n        typeof msg.content === \"string\"\n          ? msg.content\n          : JSON.stringify(msg.content),\n      ...((msg as any).tool_calls && { tool_calls: (msg as any).tool_calls }),\n    };\n  });\n\n  const result: UnifiedChatRequest = {\n    messages,\n    model: request.model,\n    max_tokens: request.max_tokens,\n    temperature: request.temperature,\n    stream: request.stream,\n  };\n\n  if (request.tools && request.tools.length > 0) {\n    result.tools = convertToolsFromOpenAI(request.tools);\n\n    if (request.tool_choice) {\n      if (typeof request.tool_choice === \"string\") {\n        result.tool_choice = request.tool_choice;\n      } else if (request.tool_choice.type === \"function\") {\n        result.tool_choice = request.tool_choice.function.name;\n      }\n    }\n  }\n\n  return result;\n}\n\nexport function convertFromAnthropic(\n  request: AnthropicChatRequest\n): UnifiedChatRequest {\n  const messages: UnifiedMessage[] = [];\n\n  if (request.system) {\n    messages.push({\n      role: \"system\",\n      content: request.system,\n    });\n  }\n  const pendingToolCalls: any[] = [];\n  const pendingTextContent: string[] = [];\n  let lastRole: string | null = null;\n\n  for (let i = 0; i < request.messages.length; i++) {\n    const msg = request.messages[i];\n\n    if (typeof msg.content === \"string\") {\n      if (\n        lastRole === \"assistant\" &&\n        pendingToolCalls.length > 0 &&\n        msg.role !== \"assistant\"\n      ) {\n        const assistantMessage: UnifiedMessage = {\n          role: \"assistant\",\n          content: pendingTextContent.join(\"\") || null,\n          tool_calls:\n            pendingToolCalls.length > 0 ? pendingToolCalls : undefined,\n        };\n        if (assistantMessage.tool_calls && pendingTextContent.length === 0) {\n          assistantMessage.content = null;\n        }\n        messages.push(assistantMessage);\n        pendingToolCalls.length = 0;\n        pendingTextContent.length = 0;\n      }\n\n      messages.push({\n        role: msg.role,\n        content: msg.content,\n      });\n    } else if (Array.isArray(msg.content)) {\n      const textBlocks: string[] = [];\n      const toolCalls: any[] = [];\n      const toolResults: any[] = [];\n\n      msg.content.forEach((block) => {\n        if (block.type === \"text\") {\n          textBlocks.push(block.text);\n        } else if (block.type === \"tool_use\") {\n          toolCalls.push({\n            id: block.id,\n            type: \"function\" as const,\n            function: {\n              name: block.name,\n              arguments: JSON.stringify(block.input || {}),\n            },\n          });\n        } else if (block.type === \"tool_result\") {\n          toolResults.push(block);\n        }\n      });\n\n      if (toolResults.length > 0) {\n        if (lastRole === \"assistant\" && pendingToolCalls.length > 0) {\n          const assistantMessage: UnifiedMessage = {\n            role: \"assistant\",\n            content: pendingTextContent.join(\"\") || null,\n            tool_calls: pendingToolCalls,\n          };\n          if (pendingTextContent.length === 0) {\n            assistantMessage.content = null;\n          }\n          messages.push(assistantMessage);\n          pendingToolCalls.length = 0;\n          pendingTextContent.length = 0;\n        }\n\n        toolResults.forEach((toolResult) => {\n          messages.push({\n            role: \"tool\",\n            content:\n              typeof toolResult.content === \"string\"\n                ? toolResult.content\n                : JSON.stringify(toolResult.content),\n            tool_call_id: toolResult.tool_use_id,\n          });\n        });\n      } else if (msg.role === \"assistant\") {\n        if (lastRole === \"assistant\") {\n          pendingToolCalls.push(...toolCalls);\n          pendingTextContent.push(...textBlocks);\n        } else {\n          if (pendingToolCalls.length > 0) {\n            const prevAssistantMessage: UnifiedMessage = {\n              role: \"assistant\",\n              content: pendingTextContent.join(\"\") || null,\n              tool_calls: pendingToolCalls,\n            };\n            if (pendingTextContent.length === 0) {\n              prevAssistantMessage.content = null;\n            }\n            messages.push(prevAssistantMessage);\n          }\n\n          pendingToolCalls.length = 0;\n          pendingTextContent.length = 0;\n          pendingToolCalls.push(...toolCalls);\n          pendingTextContent.push(...textBlocks);\n        }\n      } else {\n        if (lastRole === \"assistant\" && pendingToolCalls.length > 0) {\n          const assistantMessage: UnifiedMessage = {\n            role: \"assistant\",\n            content: pendingTextContent.join(\"\") || null,\n            tool_calls: pendingToolCalls,\n          };\n          if (pendingTextContent.length === 0) {\n            assistantMessage.content = null;\n          }\n          messages.push(assistantMessage);\n          pendingToolCalls.length = 0;\n          pendingTextContent.length = 0;\n        }\n\n        const message: UnifiedMessage = {\n          role: msg.role,\n          content: textBlocks.join(\"\") || null,\n        };\n\n        if (toolCalls.length > 0) {\n          message.tool_calls = toolCalls;\n          if (textBlocks.length === 0) {\n            message.content = null;\n          }\n        }\n\n        messages.push(message);\n      }\n    } else {\n      if (lastRole === \"assistant\" && pendingToolCalls.length > 0) {\n        const assistantMessage: UnifiedMessage = {\n          role: \"assistant\",\n          content: pendingTextContent.join(\"\") || null,\n          tool_calls: pendingToolCalls,\n        };\n        if (pendingTextContent.length === 0) {\n          assistantMessage.content = null;\n        }\n        messages.push(assistantMessage);\n        pendingToolCalls.length = 0;\n        pendingTextContent.length = 0;\n      }\n\n      messages.push({\n        role: msg.role,\n        content: JSON.stringify(msg.content),\n      });\n    }\n\n    lastRole = msg.role;\n  }\n\n  if (lastRole === \"assistant\" && pendingToolCalls.length > 0) {\n    const assistantMessage: UnifiedMessage = {\n      role: \"assistant\",\n      content: pendingTextContent.join(\"\") || null,\n      tool_calls: pendingToolCalls,\n    };\n    if (pendingTextContent.length === 0) {\n      assistantMessage.content = null;\n    }\n    messages.push(assistantMessage);\n  }\n\n  const result: UnifiedChatRequest = {\n    messages,\n    model: request.model,\n    max_tokens: request.max_tokens,\n    temperature: request.temperature,\n    stream: request.stream,\n  };\n\n  if (request.tools && request.tools.length > 0) {\n    result.tools = convertToolsFromAnthropic(request.tools);\n\n    if (request.tool_choice) {\n      if (request.tool_choice.type === \"auto\") {\n        result.tool_choice = \"auto\";\n      } else if (request.tool_choice.type === \"tool\") {\n        result.tool_choice = request.tool_choice.name;\n      }\n    }\n  }\n\n  return result;\n}\n\nexport function convertRequest(\n  request: OpenAIChatRequest | AnthropicChatRequest | UnifiedChatRequest,\n  options: ConversionOptions\n): OpenAIChatRequest | AnthropicChatRequest {\n  let unifiedRequest: UnifiedChatRequest;\n  if (options.sourceProvider === \"openai\") {\n    unifiedRequest = convertFromOpenAI(request as OpenAIChatRequest);\n  } else if (options.sourceProvider === \"anthropic\") {\n    unifiedRequest = convertFromAnthropic(request as AnthropicChatRequest);\n  } else {\n    unifiedRequest = request as UnifiedChatRequest;\n  }\n\n  if (options.targetProvider === \"openai\") {\n    return convertToOpenAI(unifiedRequest);\n  } else {\n    // For now, return unified request since Anthropic format is similar\n    return unifiedRequest as any;\n  }\n}\n"
  },
  {
    "path": "packages/core/src/utils/gemini.util.ts",
    "content": "import { UnifiedChatRequest, UnifiedMessage } from \"../types/llm\";\nimport { Content, ContentListUnion, Part, ToolListUnion } from \"@google/genai\";\n\nexport function cleanupParameters(obj: any, keyName?: string): void {\n  if (!obj || typeof obj !== \"object\") {\n    return;\n  }\n\n  if (Array.isArray(obj)) {\n    obj.forEach((item) => {\n      cleanupParameters(item);\n    });\n    return;\n  }\n\n  const validFields = new Set([\n    \"type\",\n    \"format\",\n    \"title\",\n    \"description\",\n    \"nullable\",\n    \"enum\",\n    \"maxItems\",\n    \"minItems\",\n    \"properties\",\n    \"required\",\n    \"minProperties\",\n    \"maxProperties\",\n    \"minLength\",\n    \"maxLength\",\n    \"pattern\",\n    \"example\",\n    \"anyOf\",\n    \"propertyOrdering\",\n    \"default\",\n    \"items\",\n    \"minimum\",\n    \"maximum\",\n  ]);\n\n  if (keyName !== \"properties\") {\n    Object.keys(obj).forEach((key) => {\n      if (!validFields.has(key)) {\n        delete obj[key];\n      }\n    });\n  }\n\n  if (obj.enum && obj.type !== \"string\") {\n    delete obj.enum;\n  }\n\n  if (\n    obj.type === \"string\" &&\n    obj.format &&\n    ![\"enum\", \"date-time\"].includes(obj.format)\n  ) {\n    delete obj.format;\n  }\n\n  Object.keys(obj).forEach((key) => {\n    cleanupParameters(obj[key], key);\n  });\n}\n\n// Type enum equivalent in JavaScript\nconst Type = {\n  TYPE_UNSPECIFIED: \"TYPE_UNSPECIFIED\",\n  STRING: \"STRING\",\n  NUMBER: \"NUMBER\",\n  INTEGER: \"INTEGER\",\n  BOOLEAN: \"BOOLEAN\",\n  ARRAY: \"ARRAY\",\n  OBJECT: \"OBJECT\",\n  NULL: \"NULL\",\n};\n\n/**\n * Transform the type field from an array of types to an array of anyOf fields.\n * @param {string[]} typeList - List of types\n * @param {Object} resultingSchema - The schema object to modify\n */\nfunction flattenTypeArrayToAnyOf(\n  typeList: Array<string>,\n  resultingSchema: any\n): void {\n  if (typeList.includes(\"null\")) {\n    resultingSchema[\"nullable\"] = true;\n  }\n  const listWithoutNull = typeList.filter((type) => type !== \"null\");\n\n  if (listWithoutNull.length === 1) {\n    const upperCaseType = listWithoutNull[0].toUpperCase();\n    resultingSchema[\"type\"] = Object.values(Type).includes(upperCaseType)\n      ? upperCaseType\n      : Type.TYPE_UNSPECIFIED;\n  } else {\n    resultingSchema[\"anyOf\"] = [];\n    for (const i of listWithoutNull) {\n      const upperCaseType = i.toUpperCase();\n      resultingSchema[\"anyOf\"].push({\n        type: Object.values(Type).includes(upperCaseType)\n          ? upperCaseType\n          : Type.TYPE_UNSPECIFIED,\n      });\n    }\n  }\n}\n\n/**\n * Process a JSON schema to make it compatible with the GenAI API\n * @param {Object} _jsonSchema - The JSON schema to process\n * @returns {Object} - The processed schema\n */\nfunction processJsonSchema(_jsonSchema: any): any {\n  const genAISchema = {};\n  const schemaFieldNames = [\"items\"];\n  const listSchemaFieldNames = [\"anyOf\"];\n  const dictSchemaFieldNames = [\"properties\"];\n\n  if (_jsonSchema[\"type\"] && _jsonSchema[\"anyOf\"]) {\n    throw new Error(\"type and anyOf cannot be both populated.\");\n  }\n\n  /*\n  This is to handle the nullable array or object. The _jsonSchema will\n  be in the format of {anyOf: [{type: 'null'}, {type: 'object'}]}. The\n  logic is to check if anyOf has 2 elements and one of the element is null,\n  if so, the anyOf field is unnecessary, so we need to get rid of the anyOf\n  field and make the schema nullable. Then use the other element as the new\n  _jsonSchema for processing. This is because the backend doesn't have a null\n  type.\n  */\n  const incomingAnyOf = _jsonSchema[\"anyOf\"];\n  if (\n    incomingAnyOf != null &&\n    Array.isArray(incomingAnyOf) &&\n    incomingAnyOf.length == 2\n  ) {\n    if (incomingAnyOf[0] && incomingAnyOf[0][\"type\"] === \"null\") {\n      genAISchema[\"nullable\"] = true;\n      _jsonSchema = incomingAnyOf[1];\n    } else if (incomingAnyOf[1] && incomingAnyOf[1][\"type\"] === \"null\") {\n      genAISchema[\"nullable\"] = true;\n      _jsonSchema = incomingAnyOf[0];\n    }\n  }\n\n  if (_jsonSchema[\"type\"] && Array.isArray(_jsonSchema[\"type\"])) {\n    flattenTypeArrayToAnyOf(_jsonSchema[\"type\"], genAISchema);\n  }\n\n  for (const [fieldName, fieldValue] of Object.entries(_jsonSchema)) {\n    // Skip if the fieldValue is undefined or null.\n    if (fieldValue == null) {\n      continue;\n    }\n\n    if (fieldName == \"type\") {\n      if (fieldValue === \"null\") {\n        throw new Error(\n          \"type: null can not be the only possible type for the field.\"\n        );\n      }\n      if (Array.isArray(fieldValue)) {\n        // we have already handled the type field with array of types in the\n        // beginning of this function.\n        continue;\n      }\n      const upperCaseValue = fieldValue.toUpperCase();\n      genAISchema[\"type\"] = Object.values(Type).includes(upperCaseValue)\n        ? upperCaseValue\n        : Type.TYPE_UNSPECIFIED;\n    } else if (schemaFieldNames.includes(fieldName)) {\n      genAISchema[fieldName] = processJsonSchema(fieldValue);\n    } else if (listSchemaFieldNames.includes(fieldName)) {\n      const listSchemaFieldValue = [];\n      for (const item of fieldValue) {\n        if (item[\"type\"] == \"null\") {\n          genAISchema[\"nullable\"] = true;\n          continue;\n        }\n        listSchemaFieldValue.push(processJsonSchema(item));\n      }\n      genAISchema[fieldName] = listSchemaFieldValue;\n    } else if (dictSchemaFieldNames.includes(fieldName)) {\n      const dictSchemaFieldValue = {};\n      for (const [key, value] of Object.entries(fieldValue)) {\n        dictSchemaFieldValue[key] = processJsonSchema(value);\n      }\n      genAISchema[fieldName] = dictSchemaFieldValue;\n    } else {\n      // additionalProperties is not included in JSONSchema, skipping it.\n      if (fieldName === \"additionalProperties\") {\n        continue;\n      }\n      genAISchema[fieldName] = fieldValue;\n    }\n  }\n  return genAISchema;\n}\n\n/**\n * Transform a tool object\n * @param {Object} tool - The tool object to transform\n * @returns {Object} - The transformed tool object\n */\nexport function tTool(tool: any): any {\n  if (tool.functionDeclarations) {\n    for (const functionDeclaration of tool.functionDeclarations) {\n      if (functionDeclaration.parameters) {\n        if (!Object.keys(functionDeclaration.parameters).includes(\"$schema\")) {\n          functionDeclaration.parameters = processJsonSchema(\n            functionDeclaration.parameters\n          );\n        } else {\n          if (!functionDeclaration.parametersJsonSchema) {\n            functionDeclaration.parametersJsonSchema =\n              functionDeclaration.parameters;\n            delete functionDeclaration.parameters;\n          }\n        }\n      }\n      if (functionDeclaration.response) {\n        if (!Object.keys(functionDeclaration.response).includes(\"$schema\")) {\n          functionDeclaration.response = processJsonSchema(\n            functionDeclaration.response\n          );\n        } else {\n          if (!functionDeclaration.responseJsonSchema) {\n            functionDeclaration.responseJsonSchema =\n              functionDeclaration.response;\n            delete functionDeclaration.response;\n          }\n        }\n      }\n    }\n  }\n  return tool;\n}\n\nexport function buildRequestBody(\n  request: UnifiedChatRequest\n): Record<string, any> {\n  const tools = [];\n  const functionDeclarations = request.tools\n    ?.filter((tool) => tool.function.name !== \"web_search\")\n    ?.map((tool) => {\n      return {\n        name: tool.function.name,\n        description: tool.function.description,\n        parametersJsonSchema: tool.function.parameters,\n      };\n    });\n  if (functionDeclarations?.length) {\n    tools.push(\n      tTool({\n        functionDeclarations,\n      })\n    );\n  }\n  const webSearch = request.tools?.find(\n    (tool) => tool.function.name === \"web_search\"\n  );\n  if (webSearch) {\n    tools.push({\n      googleSearch: {},\n    });\n  }\n\n  const contents: any[] = [];\n  const toolResponses = request.messages.filter((item) => item.role === \"tool\");\n  request.messages\n    .filter((item) => item.role !== \"tool\")\n    .forEach((message: UnifiedMessage) => {\n      let role: \"user\" | \"model\";\n      if (message.role === \"assistant\") {\n        role = \"model\";\n      } else if ([\"user\", \"system\"].includes(message.role)) {\n        role = \"user\";\n      } else {\n        role = \"user\"; // Default to user if role is not recognized\n      }\n      const parts = [];\n      if (typeof message.content === \"string\") {\n        const part: any = {\n          text: message.content,\n        };\n        if (message?.thinking?.signature) {\n          part.thoughtSignature = message.thinking.signature;\n        }\n        parts.push(part);\n      } else if (Array.isArray(message.content)) {\n        parts.push(\n          ...message.content.map((content) => {\n            if (content.type === \"text\") {\n              return {\n                text: content.text || \"\",\n              };\n            }\n            if (content.type === \"image_url\") {\n              if (content.image_url.url.startsWith(\"http\")) {\n                return {\n                  file_data: {\n                    mime_type: content.media_type,\n                    file_uri: content.image_url.url,\n                  },\n                };\n              } else {\n                return {\n                  inlineData: {\n                    mime_type: content.media_type,\n                    data:\n                      content.image_url.url?.split(\",\")?.pop() ||\n                      content.image_url.url,\n                  },\n                };\n              }\n            }\n          })\n        );\n      } else if (message.content && typeof message.content === \"object\") {\n        // Object like { text: \"...\" }\n        if (message.content.text) {\n          parts.push({ text: message.content.text });\n        } else {\n          parts.push({ text: JSON.stringify(message.content) });\n        }\n      }\n\n      if (Array.isArray(message.tool_calls)) {\n        parts.push(\n          ...message.tool_calls.map((toolCall, index) => {\n            return {\n              functionCall: {\n                id:\n                  toolCall.id ||\n                  `tool_${Math.random().toString(36).substring(2, 15)}`,\n                name: toolCall.function.name,\n                args: JSON.parse(toolCall.function.arguments || \"{}\"),\n              },\n              thoughtSignature:\n                index === 0 && message.thinking?.signature\n                  ? message.thinking?.signature\n                  : undefined,\n            };\n          })\n        );\n      }\n\n      if (parts.length === 0) {\n        parts.push({ text: \"\" });\n      }\n\n      contents.push({\n        role,\n        parts,\n      });\n\n      if (role === \"model\" && message.tool_calls) {\n        const functionResponses = message.tool_calls.map((tool) => {\n          const response = toolResponses.find(\n            (item) => item.tool_call_id === tool.id\n          );\n          return {\n            functionResponse: {\n              name: tool?.function?.name,\n              response: { result: response?.content },\n            },\n          };\n        });\n        contents.push({\n          role: \"user\",\n          parts: functionResponses,\n        });\n      }\n    });\n\n  const generationConfig: any = {};\n\n  if (\n    request.reasoning &&\n    request.reasoning.effort &&\n    request.reasoning.effort !== \"none\"\n  ) {\n    generationConfig.thinkingConfig = {\n      includeThoughts: true,\n    };\n    if (request.model.includes(\"gemini-3\")) {\n      generationConfig.thinkingConfig.thinkingLevel = request.reasoning.effort;\n    } else {\n      const thinkingBudgets = request.model.includes(\"pro\")\n        ? [128, 32768]\n        : [0, 24576];\n      let thinkingBudget;\n      const max_tokens = request.reasoning.max_tokens;\n      if (typeof max_tokens !== \"undefined\") {\n        if (\n          max_tokens >= thinkingBudgets[0] &&\n          max_tokens <= thinkingBudgets[1]\n        ) {\n          thinkingBudget = max_tokens;\n        } else if (max_tokens < thinkingBudgets[0]) {\n          thinkingBudget = thinkingBudgets[0];\n        } else if (max_tokens > thinkingBudgets[1]) {\n          thinkingBudget = thinkingBudgets[1];\n        }\n        generationConfig.thinkingConfig.thinkingBudget = thinkingBudget;\n      }\n    }\n  }\n\n  const body = {\n    contents,\n    tools: tools.length ? tools : undefined,\n    generationConfig,\n  };\n\n  if (request.tool_choice) {\n    const toolConfig = {\n      functionCallingConfig: {},\n    };\n    if (request.tool_choice === \"auto\") {\n      toolConfig.functionCallingConfig.mode = \"auto\";\n    } else if (request.tool_choice === \"none\") {\n      toolConfig.functionCallingConfig.mode = \"none\";\n    } else if (request.tool_choice === \"required\") {\n      toolConfig.functionCallingConfig.mode = \"any\";\n    } else if (request.tool_choice?.function?.name) {\n      toolConfig.functionCallingConfig.mode = \"any\";\n      toolConfig.functionCallingConfig.allowedFunctionNames = [\n        request.tool_choice?.function?.name,\n      ];\n    }\n    body.toolConfig = toolConfig;\n  }\n\n  return body;\n}\n\nexport function transformRequestOut(\n  request: Record<string, any>\n): UnifiedChatRequest {\n  const contents: ContentListUnion = request.contents;\n  const tools: ToolListUnion = request.tools;\n  const model: string = request.model;\n  const max_tokens: number | undefined = request.max_tokens;\n  const temperature: number | undefined = request.temperature;\n  const stream: boolean | undefined = request.stream;\n  const tool_choice: \"auto\" | \"none\" | string | undefined = request.tool_choice;\n\n  const unifiedChatRequest: UnifiedChatRequest = {\n    messages: [],\n    model,\n    max_tokens,\n    temperature,\n    stream,\n    tool_choice,\n  };\n\n  if (Array.isArray(contents)) {\n    contents.forEach((content) => {\n      if (typeof content === \"string\") {\n        unifiedChatRequest.messages.push({\n          role: \"user\",\n          content,\n        });\n      } else if (typeof (content as Part).text === \"string\") {\n        unifiedChatRequest.messages.push({\n          role: \"user\",\n          content: (content as Part).text || null,\n        });\n      } else if ((content as Content).role === \"user\") {\n        unifiedChatRequest.messages.push({\n          role: \"user\",\n          content:\n            (content as Content)?.parts?.map((part: Part) => ({\n              type: \"text\",\n              text: part.text || \"\",\n            })) || [],\n        });\n      } else if ((content as Content).role === \"model\") {\n        unifiedChatRequest.messages.push({\n          role: \"assistant\",\n          content:\n            (content as Content)?.parts?.map((part: Part) => ({\n              type: \"text\",\n              text: part.text || \"\",\n            })) || [],\n        });\n      }\n    });\n  }\n\n  if (Array.isArray(tools)) {\n    unifiedChatRequest.tools = [];\n    tools.forEach((tool) => {\n      if (Array.isArray(tool.functionDeclarations)) {\n        tool.functionDeclarations.forEach((tool) => {\n          unifiedChatRequest.tools!.push({\n            type: \"function\",\n            function: {\n              name: tool.name,\n              description: tool.description,\n              parameters: tool.parameters,\n            },\n          });\n        });\n      }\n    });\n  }\n\n  return unifiedChatRequest;\n}\n\nexport async function transformResponseOut(\n  response: Response,\n  providerName: string,\n  logger?: any\n): Promise<Response> {\n  if (response.headers.get(\"Content-Type\")?.includes(\"application/json\")) {\n    const jsonResponse: any = await response.json();\n    logger?.debug({ response: jsonResponse }, `${providerName} response:`);\n\n    // Extract thinking content from parts with thought: true\n    let thinkingContent = \"\";\n    let thinkingSignature = \"\";\n\n    const parts = jsonResponse.candidates[0]?.content?.parts || [];\n    const nonThinkingParts: Part[] = [];\n\n    for (const part of parts) {\n      if (part.text && part.thought === true) {\n        thinkingContent += part.text;\n      } else {\n        nonThinkingParts.push(part);\n      }\n    }\n\n    // Get thoughtSignature from functionCall args or usageMetadata\n    thinkingSignature = parts.find(\n      (part: any) => part.thoughtSignature\n    )?.thoughtSignature;\n\n    const tool_calls =\n      nonThinkingParts\n        ?.filter((part: Part) => part.functionCall)\n        ?.map((part: Part) => ({\n          id:\n            part.functionCall?.id ||\n            `tool_${Math.random().toString(36).substring(2, 15)}`,\n          type: \"function\",\n          function: {\n            name: part.functionCall?.name,\n            arguments: JSON.stringify(part.functionCall?.args || {}),\n          },\n        })) || [];\n\n    const textContent =\n      nonThinkingParts\n        ?.filter((part: Part) => part.text)\n        ?.map((part: Part) => part.text)\n        ?.join(\"\\n\") || \"\";\n\n    const res = {\n      id: jsonResponse.responseId,\n      choices: [\n        {\n          finish_reason:\n            (\n              jsonResponse.candidates[0].finishReason as string\n            )?.toLowerCase() || null,\n          index: 0,\n          message: {\n            content: textContent,\n            role: \"assistant\",\n            tool_calls: tool_calls.length > 0 ? tool_calls : undefined,\n            // Add thinking as separate field if available\n            ...(thinkingSignature && {\n              thinking: {\n                content: thinkingContent || \"(no content)\",\n                signature: thinkingSignature,\n              },\n            }),\n          },\n        },\n      ],\n      created: parseInt(new Date().getTime() / 1000 + \"\", 10),\n      model: jsonResponse.modelVersion,\n      object: \"chat.completion\",\n      usage: {\n        completion_tokens:\n          jsonResponse.usageMetadata?.candidatesTokenCount || 0,\n        prompt_tokens: jsonResponse.usageMetadata?.promptTokenCount || 0,\n        prompt_tokens_details: {\n          cached_tokens:\n            jsonResponse.usageMetadata?.cachedContentTokenCount || 0,\n        },\n        total_tokens: jsonResponse.usageMetadata?.totalTokenCount || 0,\n        output_tokens_details: {\n          reasoning_tokens: jsonResponse.usageMetadata?.thoughtsTokenCount || 0,\n        },\n      },\n    };\n    return new Response(JSON.stringify(res), {\n      status: response.status,\n      statusText: response.statusText,\n      headers: response.headers,\n    });\n  } else if (response.headers.get(\"Content-Type\")?.includes(\"stream\")) {\n    if (!response.body) {\n      return response;\n    }\n\n    const decoder = new TextDecoder();\n    const encoder = new TextEncoder();\n    let signatureSent = false;\n    let contentSent = false;\n    let hasThinkingContent = false;\n    let pendingContent = \"\";\n    let contentIndex = 0;\n    let toolCallIndex = -1;\n\n    const stream = new ReadableStream({\n      async start(controller) {\n        const processLine = async (\n          line: string,\n          controller: ReadableStreamDefaultController\n        ) => {\n          if (line.startsWith(\"data: \")) {\n            const chunkStr = line.slice(6).trim();\n            if (chunkStr) {\n              logger?.debug({ chunkStr }, `${providerName} chunk:`);\n              try {\n                const chunk = JSON.parse(chunkStr);\n\n                // Check if chunk has valid structure\n                if (!chunk.candidates || !chunk.candidates[0]) {\n                  logger?.debug({ chunkStr }, `Invalid chunk structure`);\n                  return;\n                }\n\n                const candidate = chunk.candidates[0];\n                const parts = candidate.content?.parts || [];\n\n                parts\n                  .filter((part: any) => part.text && part.thought === true)\n                  .forEach((part: any) => {\n                    if (!hasThinkingContent) {\n                      hasThinkingContent = true;\n                    }\n                    const thinkingChunk = {\n                      choices: [\n                        {\n                          delta: {\n                            role: \"assistant\",\n                            content: null,\n                            thinking: {\n                              content: part.text,\n                            },\n                          },\n                          finish_reason: null,\n                          index: contentIndex,\n                          logprobs: null,\n                        },\n                      ],\n                      created: parseInt(new Date().getTime() / 1000 + \"\", 10),\n                      id: chunk.responseId || \"\",\n                      model: chunk.modelVersion || \"\",\n                      object: \"chat.completion.chunk\",\n                      system_fingerprint: \"fp_a49d71b8a1\",\n                    };\n                    controller.enqueue(\n                      encoder.encode(\n                        `data: ${JSON.stringify(thinkingChunk)}\\n\\n`\n                      )\n                    );\n                  });\n\n                let signature = parts.find(\n                  (part: Part) => part.thoughtSignature\n                )?.thoughtSignature;\n                if (signature && !signatureSent) {\n                  if (!hasThinkingContent) {\n                    const thinkingChunk = {\n                      choices: [\n                        {\n                          delta: {\n                            role: \"assistant\",\n                            content: null,\n                            thinking: {\n                              content: \"(no content)\",\n                            },\n                          },\n                          finish_reason: null,\n                          index: contentIndex,\n                          logprobs: null,\n                        },\n                      ],\n                      created: parseInt(new Date().getTime() / 1000 + \"\", 10),\n                      id: chunk.responseId || \"\",\n                      model: chunk.modelVersion || \"\",\n                      object: \"chat.completion.chunk\",\n                      system_fingerprint: \"fp_a49d71b8a1\",\n                    };\n                    controller.enqueue(\n                      encoder.encode(\n                        `data: ${JSON.stringify(thinkingChunk)}\\n\\n`\n                      )\n                    );\n                  }\n                  const signatureChunk = {\n                    choices: [\n                      {\n                        delta: {\n                          role: \"assistant\",\n                          content: null,\n                          thinking: {\n                            signature,\n                          },\n                        },\n                        finish_reason: null,\n                        index: contentIndex,\n                        logprobs: null,\n                      },\n                    ],\n                    created: parseInt(new Date().getTime() / 1000 + \"\", 10),\n                    id: chunk.responseId || \"\",\n                    model: chunk.modelVersion || \"\",\n                    object: \"chat.completion.chunk\",\n                    system_fingerprint: \"fp_a49d71b8a1\",\n                  };\n                  controller.enqueue(\n                    encoder.encode(\n                      `data: ${JSON.stringify(signatureChunk)}\\n\\n`\n                    )\n                  );\n                  signatureSent = true;\n                  contentIndex++;\n                  if (pendingContent) {\n                    const res = {\n                      choices: [\n                        {\n                          delta: {\n                            role: \"assistant\",\n                            content: pendingContent,\n                          },\n                          finish_reason: null,\n                          index: contentIndex,\n                          logprobs: null,\n                        },\n                      ],\n                      created: parseInt(new Date().getTime() / 1000 + \"\", 10),\n                      id: chunk.responseId || \"\",\n                      model: chunk.modelVersion || \"\",\n                      object: \"chat.completion.chunk\",\n                      system_fingerprint: \"fp_a49d71b8a1\",\n                    };\n\n                    controller.enqueue(\n                      encoder.encode(`data: ${JSON.stringify(res)}\\n\\n`)\n                    );\n\n                    pendingContent = \"\";\n                    if (!contentSent) {\n                      contentSent = true;\n                    }\n                  }\n                }\n\n                const tool_calls = parts\n                  .filter((part: Part) => part.functionCall)\n                  .map((part: Part) => ({\n                    id:\n                      part.functionCall?.id ||\n                      `ccr_tool_${Math.random().toString(36).substring(2, 15)}`,\n                    type: \"function\",\n                    function: {\n                      name: part.functionCall?.name,\n                      arguments: JSON.stringify(part.functionCall?.args || {}),\n                    },\n                  }));\n\n                const textContent = parts\n                  .filter((part: Part) => part.text && part.thought !== true)\n                  .map((part: Part) => part.text)\n                  .join(\"\\n\");\n\n                if (!textContent && signatureSent && !contentSent) {\n                  const emptyContentChunk = {\n                    choices: [\n                      {\n                        delta: {\n                          role: \"assistant\",\n                          content: \"(no content)\",\n                        },\n                        index: contentIndex,\n                        finish_reason: null,\n                        logprobs: null,\n                      },\n                    ],\n                    created: parseInt(new Date().getTime() / 1000 + \"\", 10),\n                    id: chunk.responseId || \"\",\n                    model: chunk.modelVersion || \"\",\n                    object: \"chat.completion.chunk\",\n                    system_fingerprint: \"fp_a49d71b8a1\",\n                  };\n                  controller.enqueue(\n                    encoder.encode(\n                      `data: ${JSON.stringify(emptyContentChunk)}\\n\\n`\n                    )\n                  );\n\n                  if (!contentSent) {\n                    contentSent = true;\n                  }\n                }\n\n                if (hasThinkingContent && textContent && !signatureSent) {\n                  if (chunk.modelVersion.includes(\"3\")) {\n                    pendingContent += textContent;\n                    return;\n                  } else {\n                    const signatureChunk = {\n                      choices: [\n                        {\n                          delta: {\n                            role: \"assistant\",\n                            content: null,\n                            thinking: {\n                              signature: `ccr_${+new Date()}`,\n                            },\n                          },\n                          finish_reason: null,\n                          index: contentIndex,\n                          logprobs: null,\n                        },\n                      ],\n                      created: parseInt(new Date().getTime() / 1000 + \"\", 10),\n                      id: chunk.responseId || \"\",\n                      model: chunk.modelVersion || \"\",\n                      object: \"chat.completion.chunk\",\n                      system_fingerprint: \"fp_a49d71b8a1\",\n                    };\n                    controller.enqueue(\n                      encoder.encode(\n                        `data: ${JSON.stringify(signatureChunk)}\\n\\n`\n                      )\n                    );\n                    signatureSent = true;\n                  }\n                }\n\n                if (textContent) {\n                  if (!pendingContent) contentIndex++;\n                  const res = {\n                    choices: [\n                      {\n                        delta: {\n                          role: \"assistant\",\n                          content: textContent,\n                        },\n                        finish_reason:\n                          candidate.finishReason?.toLowerCase() || null,\n                        index: contentIndex,\n                        logprobs: null,\n                      },\n                    ],\n                    created: parseInt(new Date().getTime() / 1000 + \"\", 10),\n                    id: chunk.responseId || \"\",\n                    model: chunk.modelVersion || \"\",\n                    object: \"chat.completion.chunk\",\n                    system_fingerprint: \"fp_a49d71b8a1\",\n                    usage: {\n                      completion_tokens:\n                        chunk.usageMetadata?.candidatesTokenCount || 0,\n                      prompt_tokens: chunk.usageMetadata?.promptTokenCount || 0,\n                      prompt_tokens_details: {\n                        cached_tokens:\n                          chunk.usageMetadata?.cachedContentTokenCount || 0,\n                      },\n                      total_tokens: chunk.usageMetadata?.totalTokenCount || 0,\n                      output_tokens_details: {\n                        reasoning_tokens:\n                          chunk.usageMetadata?.thoughtsTokenCount || 0,\n                      },\n                    },\n                  };\n\n                  if (candidate?.groundingMetadata?.groundingChunks?.length) {\n                    (res.choices[0].delta as any).annotations =\n                      candidate.groundingMetadata.groundingChunks.map(\n                        (groundingChunk: any, index: number) => {\n                          const support =\n                            candidate?.groundingMetadata?.groundingSupports?.filter(\n                              (item: any) =>\n                                item.groundingChunkIndices?.includes(index)\n                            );\n                          return {\n                            type: \"url_citation\",\n                            url_citation: {\n                              url: groundingChunk?.web?.uri || \"\",\n                              title: groundingChunk?.web?.title || \"\",\n                              content: support?.[0]?.segment?.text || \"\",\n                              start_index:\n                                support?.[0]?.segment?.startIndex || 0,\n                              end_index: support?.[0]?.segment?.endIndex || 0,\n                            },\n                          };\n                        }\n                      );\n                  }\n                  controller.enqueue(\n                    encoder.encode(`data: ${JSON.stringify(res)}\\n\\n`)\n                  );\n\n                  if (!contentSent && textContent) {\n                    contentSent = true;\n                  }\n                }\n\n                if (tool_calls.length > 0) {\n                  tool_calls.forEach((tool) => {\n                    contentIndex++;\n                    toolCallIndex++;\n                    const res = {\n                      choices: [\n                        {\n                          delta: {\n                            role: \"assistant\",\n                            tool_calls: [\n                              {\n                                ...tool,\n                                index: toolCallIndex,\n                              },\n                            ],\n                          },\n                          finish_reason:\n                            candidate.finishReason?.toLowerCase() || null,\n                          index: contentIndex,\n                          logprobs: null,\n                        },\n                      ],\n                      created: parseInt(new Date().getTime() / 1000 + \"\", 10),\n                      id: chunk.responseId || \"\",\n                      model: chunk.modelVersion || \"\",\n                      object: \"chat.completion.chunk\",\n                      system_fingerprint: \"fp_a49d71b8a1\",\n                      usage: {\n                        completion_tokens:\n                          chunk.usageMetadata?.candidatesTokenCount || 0,\n                        prompt_tokens:\n                          chunk.usageMetadata?.promptTokenCount || 0,\n                        prompt_tokens_details: {\n                          cached_tokens:\n                            chunk.usageMetadata?.cachedContentTokenCount || 0,\n                        },\n                        total_tokens: chunk.usageMetadata?.totalTokenCount || 0,\n                        output_tokens_details: {\n                          reasoning_tokens:\n                            chunk.usageMetadata?.thoughtsTokenCount || 0,\n                        },\n                      },\n                    };\n\n                    if (candidate?.groundingMetadata?.groundingChunks?.length) {\n                      (res.choices[0].delta as any).annotations =\n                        candidate.groundingMetadata.groundingChunks.map(\n                          (groundingChunk: any, index: number) => {\n                            const support =\n                              candidate?.groundingMetadata?.groundingSupports?.filter(\n                                (item: any) =>\n                                  item.groundingChunkIndices?.includes(index)\n                              );\n                            return {\n                              type: \"url_citation\",\n                              url_citation: {\n                                url: groundingChunk?.web?.uri || \"\",\n                                title: groundingChunk?.web?.title || \"\",\n                                content: support?.[0]?.segment?.text || \"\",\n                                start_index:\n                                  support?.[0]?.segment?.startIndex || 0,\n                                end_index: support?.[0]?.segment?.endIndex || 0,\n                              },\n                            };\n                          }\n                        );\n                    }\n                    controller.enqueue(\n                      encoder.encode(`data: ${JSON.stringify(res)}\\n\\n`)\n                    );\n                  });\n\n                  if (!contentSent && textContent) {\n                    contentSent = true;\n                  }\n                }\n              } catch (error: any) {\n                logger?.error(\n                  `Error parsing ${providerName} stream chunk`,\n                  chunkStr,\n                  error.message\n                );\n              }\n            }\n          }\n        };\n\n        const reader = response.body!.getReader();\n        let buffer = \"\";\n        try {\n          while (true) {\n            const { done, value } = await reader.read();\n            if (done) {\n              if (buffer) {\n                await processLine(buffer, controller);\n              }\n              break;\n            }\n\n            buffer += decoder.decode(value, { stream: true });\n            const lines = buffer.split(\"\\n\");\n\n            buffer = lines.pop() || \"\";\n\n            for (const line of lines) {\n              await processLine(line, controller);\n            }\n          }\n        } catch (error) {\n          controller.error(error);\n        } finally {\n          controller.close();\n        }\n      },\n    });\n\n    return new Response(stream, {\n      status: response.status,\n      statusText: response.statusText,\n      headers: response.headers,\n    });\n  }\n}\n"
  },
  {
    "path": "packages/core/src/utils/image.ts",
    "content": "export const formatBase64 = (data: string, media_type: string) => {\n  if (data.includes(\"base64\")) {\n    data = data.split(\"base64\").pop() as string;\n    if (data.startsWith(\",\")) {\n      data = data.slice(1);\n    }\n  }\n  return `data:${media_type};base64,${data}`;\n};\n"
  },
  {
    "path": "packages/core/src/utils/request.ts",
    "content": "import { ProxyAgent } from \"undici\";\nimport { UnifiedChatRequest } from \"../types/llm\";\n\nexport function sendUnifiedRequest(\n  url: URL | string,\n  request: UnifiedChatRequest,\n  config: any,\n  context: any,\n  logger?: any\n): Promise<Response> {\n  const headers = new Headers({\n    \"Content-Type\": \"application/json\",\n  });\n  if (config.headers) {\n    Object.entries(config.headers).forEach(([key, value]) => {\n      if (value) {\n        headers.set(key, value as string);\n      }\n    });\n  }\n  let combinedSignal: AbortSignal;\n  const timeoutSignal = AbortSignal.timeout(config.TIMEOUT ?? 60 * 1000 * 60);\n\n  if (config.signal) {\n    const controller = new AbortController();\n    const abortHandler = () => controller.abort();\n    config.signal.addEventListener(\"abort\", abortHandler);\n    timeoutSignal.addEventListener(\"abort\", abortHandler);\n    combinedSignal = controller.signal;\n  } else {\n    combinedSignal = timeoutSignal;\n  }\n\n  const fetchOptions: RequestInit = {\n    method: \"POST\",\n    headers: headers,\n    body: JSON.stringify(request),\n    signal: combinedSignal,\n  };\n\n  if (config.httpsProxy) {\n    (fetchOptions as any).dispatcher = new ProxyAgent(\n      new URL(config.httpsProxy).toString()\n    );\n  }\n  logger?.debug(\n    {\n      reqId: context.req.id,\n      request: fetchOptions,\n      headers: Object.fromEntries(headers.entries()),\n      requestUrl: typeof url === \"string\" ? url : url.toString(),\n      useProxy: config.httpsProxy,\n    },\n    \"final request\"\n  );\n  return fetch(typeof url === \"string\" ? url : url.toString(), fetchOptions);\n}\n"
  },
  {
    "path": "packages/core/src/utils/router.ts",
    "content": "import { get_encoding } from \"tiktoken\";\nimport { sessionUsageCache, Usage } from \"./cache\";\nimport { readFile } from \"fs/promises\";\nimport { opendir, stat } from \"fs/promises\";\nimport { join } from \"path\";\nimport { CLAUDE_PROJECTS_DIR, HOME_DIR } from \"@CCR/shared\";\nimport { LRUCache } from \"lru-cache\";\nimport { ConfigService } from \"../services/config\";\nimport { TokenizerService } from \"../services/tokenizer\";\n\n// Types from @anthropic-ai/sdk\ninterface Tool {\n  name: string;\n  description?: string;\n  input_schema: object;\n}\n\ninterface ContentBlockParam {\n  type: string;\n  [key: string]: any;\n}\n\ninterface MessageParam {\n  role: string;\n  content: string | ContentBlockParam[];\n}\n\ninterface MessageCreateParamsBase {\n  messages?: MessageParam[];\n  system?: string | any[];\n  tools?: Tool[];\n  [key: string]: any;\n}\n\nconst enc = get_encoding(\"cl100k_base\");\n\nexport const calculateTokenCount = (\n  messages: MessageParam[],\n  system: any,\n  tools: Tool[]\n) => {\n  let tokenCount = 0;\n  if (Array.isArray(messages)) {\n    messages.forEach((message) => {\n      if (typeof message.content === \"string\") {\n        tokenCount += enc.encode(message.content).length;\n      } else if (Array.isArray(message.content)) {\n        message.content.forEach((contentPart: any) => {\n          if (contentPart.type === \"text\") {\n            tokenCount += enc.encode(contentPart.text).length;\n          } else if (contentPart.type === \"tool_use\") {\n            tokenCount += enc.encode(JSON.stringify(contentPart.input)).length;\n          } else if (contentPart.type === \"tool_result\") {\n            tokenCount += enc.encode(\n              typeof contentPart.content === \"string\"\n                ? contentPart.content\n                : JSON.stringify(contentPart.content)\n            ).length;\n          }\n        });\n      }\n    });\n  }\n  if (typeof system === \"string\") {\n    tokenCount += enc.encode(system).length;\n  } else if (Array.isArray(system)) {\n    system.forEach((item: any) => {\n      if (item.type !== \"text\") return;\n      if (typeof item.text === \"string\") {\n        tokenCount += enc.encode(item.text).length;\n      } else if (Array.isArray(item.text)) {\n        item.text.forEach((textPart: any) => {\n          tokenCount += enc.encode(textPart || \"\").length;\n        });\n      }\n    });\n  }\n  if (tools) {\n    tools.forEach((tool: Tool) => {\n      if (tool.description) {\n        tokenCount += enc.encode(tool.name + tool.description).length;\n      }\n      if (tool.input_schema) {\n        tokenCount += enc.encode(JSON.stringify(tool.input_schema)).length;\n      }\n    });\n  }\n  return tokenCount;\n};\n\nconst getProjectSpecificRouter = async (\n  req: any,\n  configService: ConfigService\n) => {\n  // Check if there is project-specific configuration\n  if (req.sessionId) {\n    const project = await searchProjectBySession(req.sessionId);\n    if (project) {\n      const projectConfigPath = join(HOME_DIR, project, \"config.json\");\n      const sessionConfigPath = join(\n        HOME_DIR,\n        project,\n        `${req.sessionId}.json`\n      );\n\n      // First try to read sessionConfig file\n      try {\n        const sessionConfig = JSON.parse(await readFile(sessionConfigPath, \"utf8\"));\n        if (sessionConfig && sessionConfig.Router) {\n          return sessionConfig.Router;\n        }\n      } catch {}\n      try {\n        const projectConfig = JSON.parse(await readFile(projectConfigPath, \"utf8\"));\n        if (projectConfig && projectConfig.Router) {\n          return projectConfig.Router;\n        }\n      } catch {}\n    }\n  }\n  return undefined; // Return undefined to use original configuration\n};\n\nconst getUseModel = async (\n  req: any,\n  tokenCount: number,\n  configService: ConfigService,\n  lastUsage?: Usage | undefined\n): Promise<{ model: string; scenarioType: RouterScenarioType }> => {\n  const projectSpecificRouter = await getProjectSpecificRouter(req, configService);\n  const providers = configService.get<any[]>(\"providers\") || [];\n  const Router = projectSpecificRouter || configService.get(\"Router\");\n\n  if (req.body.model.includes(\",\")) {\n    const [provider, model] = req.body.model.split(\",\");\n    const finalProvider = providers.find(\n      (p: any) => p.name.toLowerCase() === provider\n    );\n    const finalModel = finalProvider?.models?.find(\n      (m: any) => m.toLowerCase() === model\n    );\n    if (finalProvider && finalModel) {\n      return { model: `${finalProvider.name},${finalModel}`, scenarioType: 'default' };\n    }\n    return { model: req.body.model, scenarioType: 'default' };\n  }\n\n  // if tokenCount is greater than the configured threshold, use the long context model\n  const longContextThreshold = Router?.longContextThreshold || 60000;\n  const lastUsageThreshold =\n    lastUsage &&\n    lastUsage.input_tokens > longContextThreshold &&\n    tokenCount > 20000;\n  const tokenCountThreshold = tokenCount > longContextThreshold;\n  if ((lastUsageThreshold || tokenCountThreshold) && Router?.longContext) {\n    req.log.info(\n      `Using long context model due to token count: ${tokenCount}, threshold: ${longContextThreshold}`\n    );\n    return { model: Router.longContext, scenarioType: 'longContext' };\n  }\n  if (\n    req.body?.system?.length > 1 &&\n    req.body?.system[1]?.text?.startsWith(\"<CCR-SUBAGENT-MODEL>\")\n  ) {\n    const model = req.body?.system[1].text.match(\n      /<CCR-SUBAGENT-MODEL>(.*?)<\\/CCR-SUBAGENT-MODEL>/s\n    );\n    if (model) {\n      req.body.system[1].text = req.body.system[1].text.replace(\n        `<CCR-SUBAGENT-MODEL>${model[1]}</CCR-SUBAGENT-MODEL>`,\n        \"\"\n      );\n      return { model: model[1], scenarioType: 'default' };\n    }\n  }\n  // Use the background model for any Claude Haiku variant\n  const globalRouter = configService.get(\"Router\");\n  if (\n    req.body.model?.includes(\"claude\") &&\n    req.body.model?.includes(\"haiku\") &&\n    globalRouter?.background\n  ) {\n    req.log.info(`Using background model for ${req.body.model}`);\n    return { model: globalRouter.background, scenarioType: 'background' };\n  }\n  // The priority of websearch must be higher than thinking.\n  if (\n    Array.isArray(req.body.tools) &&\n    req.body.tools.some((tool: any) => tool.type?.startsWith(\"web_search\")) &&\n    Router?.webSearch\n  ) {\n    return { model: Router.webSearch, scenarioType: 'webSearch' };\n  }\n  // if exits thinking, use the think model\n  if (req.body.thinking && Router?.think) {\n    req.log.info(`Using think model for ${req.body.thinking}`);\n    return { model: Router.think, scenarioType: 'think' };\n  }\n  return { model: Router?.default, scenarioType: 'default' };\n};\n\nexport interface RouterContext {\n  configService: ConfigService;\n  tokenizerService?: TokenizerService;\n  event?: any;\n}\n\nexport type RouterScenarioType = 'default' | 'background' | 'think' | 'longContext' | 'webSearch';\n\nexport interface RouterFallbackConfig {\n  default?: string[];\n  background?: string[];\n  think?: string[];\n  longContext?: string[];\n  webSearch?: string[];\n}\n\nexport const router = async (req: any, _res: any, context: RouterContext) => {\n  const { configService, event } = context;\n  // Parse sessionId from metadata.user_id\n  if (req.body.metadata?.user_id) {\n    const parts = req.body.metadata.user_id.split(\"_session_\");\n    if (parts.length > 1) {\n      req.sessionId = parts[1];\n    }\n  }\n  const lastMessageUsage = sessionUsageCache.get(req.sessionId);\n  const { messages, system = [], tools }: MessageCreateParamsBase = req.body;\n  const rewritePrompt = configService.get(\"REWRITE_SYSTEM_PROMPT\");\n  if (\n    rewritePrompt &&\n    system.length > 1 &&\n    system[1]?.text?.includes(\"<env>\")\n  ) {\n    const prompt = await readFile(rewritePrompt, \"utf-8\");\n    system[1].text = `${prompt}<env>${system[1].text.split(\"<env>\").pop()}`;\n  }\n\n  try {\n    // Try to get tokenizer config for the current model\n    const [providerName, modelName] = req.body.model.split(\",\");\n    const tokenizerConfig = context.tokenizerService?.getTokenizerConfigForModel(\n      providerName,\n      modelName\n    );\n\n    // Use TokenizerService if available, otherwise fall back to legacy method\n    let tokenCount: number;\n\n    if (context.tokenizerService) {\n      const result = await context.tokenizerService.countTokens(\n        {\n          messages: messages as MessageParam[],\n          system,\n          tools: tools as Tool[],\n        },\n        tokenizerConfig\n      );\n      tokenCount = result.tokenCount;\n    } else {\n      // Legacy fallback\n      tokenCount = calculateTokenCount(\n        messages as MessageParam[],\n        system,\n        tools as Tool[]\n      );\n    }\n\n    let model;\n    const customRouterPath = configService.get(\"CUSTOM_ROUTER_PATH\");\n    if (customRouterPath) {\n      try {\n        const customRouter = require(customRouterPath);\n        req.tokenCount = tokenCount; // Pass token count to custom router\n        model = await customRouter(req, configService.getAll(), {\n          event,\n        });\n      } catch (e: any) {\n        req.log.error(`failed to load custom router: ${e.message}`);\n      }\n    }\n    if (!model) {\n      const result = await getUseModel(req, tokenCount, configService, lastMessageUsage);\n      model = result.model;\n      req.scenarioType = result.scenarioType;\n    } else {\n      // Custom router doesn't provide scenario type, default to 'default'\n      req.scenarioType = 'default';\n    }\n    req.body.model = model;\n  } catch (error: any) {\n    req.log.error(`Error in router middleware: ${error.message}`);\n    const Router = configService.get(\"Router\");\n    req.body.model = Router?.default;\n    req.scenarioType = 'default';\n  }\n  return;\n};\n\n// Memory cache for sessionId to project name mapping\n// null value indicates previously searched but not found\n// Uses LRU cache with max 1000 entries\nconst sessionProjectCache = new LRUCache<string, string>({\n  max: 1000,\n});\n\nexport const searchProjectBySession = async (\n  sessionId: string\n): Promise<string | null> => {\n  // Check cache first\n  if (sessionProjectCache.has(sessionId)) {\n    const result = sessionProjectCache.get(sessionId);\n    if (!result || result === '') {\n      return null;\n    }\n    return result;\n  }\n\n  try {\n    const dir = await opendir(CLAUDE_PROJECTS_DIR);\n    const folderNames: string[] = [];\n\n    // Collect all folder names\n    for await (const dirent of dir) {\n      if (dirent.isDirectory()) {\n        folderNames.push(dirent.name);\n      }\n    }\n\n    // Concurrently check each project folder for sessionId.jsonl file\n    const checkPromises = folderNames.map(async (folderName) => {\n      const sessionFilePath = join(\n        CLAUDE_PROJECTS_DIR,\n        folderName,\n        `${sessionId}.jsonl`\n      );\n      try {\n        const fileStat = await stat(sessionFilePath);\n        return fileStat.isFile() ? folderName : null;\n      } catch {\n        // File does not exist, continue checking next\n        return null;\n      }\n    });\n\n    const results = await Promise.all(checkPromises);\n\n    // Return the first existing project directory name\n    for (const result of results) {\n      if (result) {\n        // Cache the found result\n        sessionProjectCache.set(sessionId, result);\n        return result;\n      }\n    }\n\n    // Cache not found result (null value means previously searched but not found)\n    sessionProjectCache.set(sessionId, '');\n    return null; // No matching project found\n  } catch (error) {\n    console.error(\"Error searching for project by session:\", error);\n    // Cache null result on error to avoid repeated errors\n    sessionProjectCache.set(sessionId, '');\n    return null;\n  }\n};\n"
  },
  {
    "path": "packages/core/src/utils/sse/SSEParser.transform.ts",
    "content": "export class SSEParserTransform extends TransformStream<string, any> {\n    private buffer = '';\n    private currentEvent: Record<string, any> = {};\n\n    constructor() {\n        super({\n            transform: (chunk: string, controller) => {\n                this.buffer += chunk;\n                const lines = this.buffer.split('\\n');\n\n                // Keep last line (may be incomplete)\n                this.buffer = lines.pop() || '';\n\n                for (const line of lines) {\n                    const event = this.processLine(line);\n                    if (event) {\n                        controller.enqueue(event);\n                    }\n                }\n            },\n            flush: (controller) => {\n                // Process remaining content in buffer\n                if (this.buffer.trim()) {\n                    const events: any[] = [];\n                    this.processLine(this.buffer.trim(), events);\n                    events.forEach(event => controller.enqueue(event));\n                }\n\n                // Push last event (if any)\n                if (Object.keys(this.currentEvent).length > 0) {\n                    controller.enqueue(this.currentEvent);\n                }\n            }\n        });\n    }\n\n    private processLine(line: string, events?: any[]): any | null {\n        if (!line.trim()) {\n            if (Object.keys(this.currentEvent).length > 0) {\n                const event = { ...this.currentEvent };\n                this.currentEvent = {};\n                if (events) {\n                    events.push(event);\n                    return null;\n                }\n                return event;\n            }\n            return null;\n        }\n\n        if (line.startsWith('event:')) {\n            this.currentEvent.event = line.slice(6).trim();\n        } else if (line.startsWith('data:')) {\n            const data = line.slice(5).trim();\n            if (data === '[DONE]') {\n                this.currentEvent.data = { type: 'done' };\n            } else {\n                try {\n                    this.currentEvent.data = JSON.parse(data);\n                } catch (e) {\n                    this.currentEvent.data = { raw: data, error: 'JSON parse failed' };\n                }\n            }\n        } else if (line.startsWith('id:')) {\n            this.currentEvent.id = line.slice(3).trim();\n        } else if (line.startsWith('retry:')) {\n            this.currentEvent.retry = parseInt(line.slice(6).trim());\n        }\n        return null;\n    }\n}\n"
  },
  {
    "path": "packages/core/src/utils/sse/SSESerializer.transform.ts",
    "content": "export class SSESerializerTransform extends TransformStream<any, string> {\n    constructor() {\n        super({\n            transform: (event, controller) => {\n                let output = '';\n\n                if (event.event) {\n                    output += `event: ${event.event}\\n`;\n                }\n                if (event.id) {\n                    output += `id: ${event.id}\\n`;\n                }\n                if (event.retry) {\n                    output += `retry: ${event.retry}\\n`;\n                }\n                if (event.data) {\n                    if (event.data.type === 'done') {\n                        output += 'data: [DONE]\\n';\n                    } else {\n                        output += `data: ${JSON.stringify(event.data)}\\n`;\n                    }\n                }\n\n                output += '\\n';\n                controller.enqueue(output);\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "packages/core/src/utils/sse/index.ts",
    "content": "export { SSEParserTransform } from './SSEParser.transform';\nexport { SSESerializerTransform } from './SSESerializer.transform';\nexport { rewriteStream } from './rewriteStream';\n"
  },
  {
    "path": "packages/core/src/utils/sse/rewriteStream.ts",
    "content": "/**\n * Rewrite stream utility\n * Reads source readablestream and returns a new readablestream,\n * processor processes source data and pushes returned new value to new stream,\n * no push if no return value\n * @param stream\n * @param processor\n */\nexport const rewriteStream = (stream: ReadableStream, processor: (data: any, controller: ReadableStreamController<any>) => Promise<any>): ReadableStream => {\n  const reader = stream.getReader()\n\n  return new ReadableStream({\n    async start(controller) {\n      try {\n        while (true) {\n          const { done, value } = await reader.read()\n          if (done) {\n            controller.close()\n            break\n          }\n\n          const processed = await processor(value, controller)\n          if (processed !== undefined) {\n            controller.enqueue(processed)\n          }\n        }\n      } catch (error) {\n        controller.error(error)\n      } finally {\n        reader.releaseLock()\n      }\n    }\n  })\n}\n"
  },
  {
    "path": "packages/core/src/utils/thinking.ts",
    "content": "import { ThinkLevel } from \"@/types/llm\";\n\nexport const getThinkLevel = (thinking_budget: number): ThinkLevel => {\n  if (thinking_budget <= 0) return \"none\";\n  if (thinking_budget <= 1024) return \"low\";\n  if (thinking_budget <= 8192) return \"medium\";\n  return \"high\";\n};\n"
  },
  {
    "path": "packages/core/src/utils/toolArgumentsParser.ts",
    "content": "import JSON5 from \"json5\";\nimport { jsonrepair } from \"jsonrepair\";\n\n/**\n * Parse tool call arguments function\n * First try standard JSON parsing, then JSON5 parsing, finally use jsonrepair for safe repair\n *\n * @param argsString - Parameter string to parse\n * @returns Parsed parameter object or safe empty object\n */\nexport function parseToolArguments(argsString: string, logger?: any): string {\n  // Handle empty or null input\n  if (!argsString || argsString.trim() === \"\" || argsString === \"{}\") {\n    return \"{}\";\n  }\n\n  try {\n    // First attempt: Standard JSON parsing\n    JSON.parse(argsString);\n    logger?.debug(`工具调用参数标准JSON解析成功 / Tool arguments standard JSON parsing successful`);\n    return argsString;\n  } catch (jsonError: any) {\n    try {\n      // Second attempt: JSON5 parsing for relaxed syntax\n      const args = JSON5.parse(argsString);\n      logger?.debug(`Tool arguments JSON5 parsing successful`);\n      return JSON.stringify(args);\n    } catch (json5Error: any) {\n      try {\n        // Third attempt: Safe JSON repair without code execution\n        const repairedJson = jsonrepair(argsString);\n        logger?.debug(`Tool arguments safely repaired`);\n        return repairedJson;\n      } catch (repairError: any) {\n        // All parsing attempts failed - log errors and return safe fallback\n        logger?.error(\n          `JSON parsing failed: ${jsonError.message}. ` +\n          `JSON5 parsing failed: ${json5Error.message}. ` +\n          `JSON repair failed: ${repairError.message}. ` +\n          `Input data: ${JSON.stringify(argsString)}`\n        );\n        \n        // Return safe empty object as fallback instead of potentially malformed input\n        logger?.debug(`Returning safe empty object as fallback`);\n        return \"{}\";\n      }\n    }\n  }\n}"
  },
  {
    "path": "packages/core/src/utils/vertex-claude.util.ts",
    "content": "import { UnifiedChatRequest, UnifiedMessage, UnifiedTool } from \"../types/llm\";\n\n// Vertex Claude message interface\ninterface ClaudeMessage {\n  role: \"user\" | \"assistant\";\n  content: Array<{\n    type: \"text\" | \"image\";\n    text?: string;\n    source?: {\n      type: \"base64\";\n      media_type: string;\n      data: string;\n    };\n  }>;\n}\n\n// Vertex Claude tool interface\ninterface ClaudeTool {\n  name: string;\n  description: string;\n  input_schema: {\n    type: string;\n    properties: Record<string, any>;\n    required?: string[];\n    additionalProperties?: boolean;\n    $schema?: string;\n  };\n}\n\n// Vertex Claude request interface\ninterface VertexClaudeRequest {\n  anthropic_version: \"vertex-2023-10-16\";\n  messages: ClaudeMessage[];\n  max_tokens: number;\n  stream?: boolean;\n  temperature?: number;\n  top_p?: number;\n  top_k?: number;\n  tools?: ClaudeTool[];\n  tool_choice?: \"auto\" | \"none\" | { type: \"tool\"; name: string };\n}\n\n// Vertex Claude response interface\ninterface VertexClaudeResponse {\n  content: Array<{\n    type: \"text\";\n    text: string;\n  }>;\n  id: string;\n  model: string;\n  role: \"assistant\";\n  stop_reason: string;\n  stop_sequence: null;\n  type: \"message\";\n  usage: {\n    input_tokens: number;\n    output_tokens: number;\n  };\n  tool_use?: Array<{\n    id: string;\n    name: string;\n    input: Record<string, any>;\n  }>;\n}\n\nexport function buildRequestBody(\n  request: UnifiedChatRequest\n): VertexClaudeRequest {\n  const messages: ClaudeMessage[] = [];\n\n  for (let i = 0; i < request.messages.length; i++) {\n    const message = request.messages[i];\n    const isLastMessage = i === request.messages.length - 1;\n    const isAssistantMessage = message.role === \"assistant\";\n\n    const content: ClaudeMessage[\"content\"] = [];\n\n    if (typeof message.content === \"string\") {\n      // Keep all string content, even empty strings, as it may contain important information\n      content.push({\n        type: \"text\",\n        text: message.content,\n      });\n    } else if (Array.isArray(message.content)) {\n      message.content.forEach((item) => {\n        if (item.type === \"text\") {\n          // Keep all text content, even empty strings\n          content.push({\n            type: \"text\",\n            text: item.text || \"\",\n          });\n        } else if (item.type === \"image_url\") {\n          // Handle image content\n          content.push({\n            type: \"image\",\n            source: {\n              type: \"base64\",\n              media_type: item.media_type || \"image/jpeg\",\n              data: item.image_url.url,\n            },\n          });\n        }\n      });\n    }\n\n    // Only skip completely empty non-last messages (no content and no tool calls)\n    if (\n      !isLastMessage &&\n      content.length === 0 &&\n      !message.tool_calls &&\n      !message.content\n    ) {\n      continue;\n    }\n\n    // For last assistant message, add empty content if no content but has tool calls\n    if (\n      isLastMessage &&\n      isAssistantMessage &&\n      content.length === 0 &&\n      message.tool_calls\n    ) {\n      content.push({\n        type: \"text\",\n        text: \"\",\n      });\n    }\n\n    messages.push({\n      role: message.role === \"assistant\" ? \"assistant\" : \"user\",\n      content,\n    });\n  }\n\n  const requestBody: VertexClaudeRequest = {\n    anthropic_version: \"vertex-2023-10-16\",\n    messages,\n    max_tokens: request.max_tokens || 1000,\n    stream: request.stream || false,\n    ...(request.temperature && { temperature: request.temperature }),\n  };\n\n  // Handle tool definitions\n  if (request.tools && request.tools.length > 0) {\n    requestBody.tools = request.tools.map((tool: UnifiedTool) => ({\n      name: tool.function.name,\n      description: tool.function.description,\n      input_schema: tool.function.parameters,\n    }));\n  }\n\n  // Handle tool choice\n  if (request.tool_choice) {\n    if (request.tool_choice === \"auto\" || request.tool_choice === \"none\") {\n      requestBody.tool_choice = request.tool_choice;\n    } else if (typeof request.tool_choice === \"string\") {\n      // If tool_choice is a string, assume it's the tool name\n      requestBody.tool_choice = {\n        type: \"tool\",\n        name: request.tool_choice,\n      };\n    }\n  }\n\n  return requestBody;\n}\n\nexport function transformRequestOut(\n  request: Record<string, any>\n): UnifiedChatRequest {\n  const vertexRequest = request as VertexClaudeRequest;\n\n  const messages: UnifiedMessage[] = vertexRequest.messages.map((msg) => {\n    const content = msg.content.map((item) => {\n      if (item.type === \"text\") {\n        return {\n          type: \"text\" as const,\n          text: item.text || \"\",\n        };\n      } else if (item.type === \"image\" && item.source) {\n        return {\n          type: \"image_url\" as const,\n          image_url: {\n            url: item.source.data,\n          },\n          media_type: item.source.media_type,\n        };\n      }\n      return {\n        type: \"text\" as const,\n        text: \"\",\n      };\n    });\n\n    return {\n      role: msg.role,\n      content,\n    };\n  });\n\n  const result: UnifiedChatRequest = {\n    messages,\n    model: request.model || \"claude-sonnet-4@20250514\",\n    max_tokens: vertexRequest.max_tokens,\n    temperature: vertexRequest.temperature,\n    stream: vertexRequest.stream,\n  };\n\n  // Handle tool definitions\n  if (vertexRequest.tools && vertexRequest.tools.length > 0) {\n    result.tools = vertexRequest.tools.map((tool) => ({\n      type: \"function\" as const,\n      function: {\n        name: tool.name,\n        description: tool.description,\n        parameters: {\n          type: \"object\" as const,\n          properties: tool.input_schema.properties,\n          required: tool.input_schema.required,\n          additionalProperties: tool.input_schema.additionalProperties,\n          $schema: tool.input_schema.$schema,\n        },\n      },\n    }));\n  }\n\n  // Handle tool choice\n  if (vertexRequest.tool_choice) {\n    if (typeof vertexRequest.tool_choice === \"string\") {\n      result.tool_choice = vertexRequest.tool_choice;\n    } else if (vertexRequest.tool_choice.type === \"tool\") {\n      result.tool_choice = vertexRequest.tool_choice.name;\n    }\n  }\n\n  return result;\n}\n\nexport async function transformResponseOut(\n  response: Response,\n  providerName: string,\n  logger?: any\n): Promise<Response> {\n  if (response.headers.get(\"Content-Type\")?.includes(\"application/json\")) {\n    const jsonResponse = (await response.json()) as VertexClaudeResponse;\n\n    // Handle tool calls\n    let tool_calls = undefined;\n    if (jsonResponse.tool_use && jsonResponse.tool_use.length > 0) {\n      tool_calls = jsonResponse.tool_use.map((tool) => ({\n        id: tool.id,\n        type: \"function\" as const,\n        function: {\n          name: tool.name,\n          arguments: JSON.stringify(tool.input),\n        },\n      }));\n    }\n\n    // Convert to OpenAI format response\n    const res = {\n      id: jsonResponse.id,\n      choices: [\n        {\n          finish_reason: jsonResponse.stop_reason || null,\n          index: 0,\n          message: {\n            content: jsonResponse.content[0]?.text || \"\",\n            role: \"assistant\",\n            ...(tool_calls && { tool_calls }),\n          },\n        },\n      ],\n      created: parseInt(new Date().getTime() / 1000 + \"\", 10),\n      model: jsonResponse.model,\n      object: \"chat.completion\",\n      usage: {\n        completion_tokens: jsonResponse.usage.output_tokens,\n        prompt_tokens: jsonResponse.usage.input_tokens,\n        total_tokens:\n          jsonResponse.usage.input_tokens + jsonResponse.usage.output_tokens,\n      },\n    };\n\n    return new Response(JSON.stringify(res), {\n      status: response.status,\n      statusText: response.statusText,\n      headers: response.headers,\n    });\n  } else if (response.headers.get(\"Content-Type\")?.includes(\"stream\")) {\n    // Handle streaming response\n    if (!response.body) {\n      return response;\n    }\n\n    const decoder = new TextDecoder();\n    const encoder = new TextEncoder();\n\n    const processLine = (\n      line: string,\n      controller: ReadableStreamDefaultController\n    ) => {\n      if (line.startsWith(\"data: \")) {\n        const chunkStr = line.slice(6).trim();\n        if (chunkStr) {\n          logger?.debug({ chunkStr }, `${providerName} chunk:`);\n          try {\n            const chunk = JSON.parse(chunkStr);\n\n            // Handle Anthropic native format streaming response\n            if (\n              chunk.type === \"content_block_delta\" &&\n              chunk.delta?.type === \"text_delta\"\n            ) {\n              // This is Anthropic native format, need to convert to OpenAI format\n              const res = {\n                choices: [\n                  {\n                    delta: {\n                      role: \"assistant\",\n                      content: chunk.delta.text || \"\",\n                    },\n                    finish_reason: null,\n                    index: 0,\n                    logprobs: null,\n                  },\n                ],\n                created: parseInt(new Date().getTime() / 1000 + \"\", 10),\n                id: chunk.id || \"\",\n                model: chunk.model || \"\",\n                object: \"chat.completion.chunk\",\n                system_fingerprint: \"fp_a49d71b8a1\",\n                usage: {\n                  completion_tokens: chunk.usage?.output_tokens || 0,\n                  prompt_tokens: chunk.usage?.input_tokens || 0,\n                  total_tokens:\n                    (chunk.usage?.input_tokens || 0) +\n                    (chunk.usage?.output_tokens || 0),\n                },\n              };\n              controller.enqueue(\n                encoder.encode(`data: ${JSON.stringify(res)}\\n\\n`)\n              );\n            } else if (\n              chunk.type === \"content_block_delta\" &&\n              chunk.delta?.type === \"input_json_delta\"\n            ) {\n              // Handle tool call argument delta\n              const res = {\n                choices: [\n                  {\n                    delta: {\n                      tool_calls: [\n                        {\n                          index: chunk.index || 0,\n                          function: {\n                            arguments: chunk.delta.partial_json || \"\",\n                          },\n                        },\n                      ],\n                    },\n                    finish_reason: null,\n                    index: 0,\n                    logprobs: null,\n                  },\n                ],\n                created: parseInt(new Date().getTime() / 1000 + \"\", 10),\n                id: chunk.id || \"\",\n                model: chunk.model || \"\",\n                object: \"chat.completion.chunk\",\n                system_fingerprint: \"fp_a49d71b8a1\",\n                usage: {\n                  completion_tokens: chunk.usage?.output_tokens || 0,\n                  prompt_tokens: chunk.usage?.input_tokens || 0,\n                  total_tokens:\n                    (chunk.usage?.input_tokens || 0) +\n                    (chunk.usage?.output_tokens || 0),\n                },\n              };\n              controller.enqueue(\n                encoder.encode(`data: ${JSON.stringify(res)}\\n\\n`)\n              );\n            } else if (\n              chunk.type === \"content_block_start\" &&\n              chunk.content_block?.type === \"tool_use\"\n            ) {\n              // Handle tool call start\n              const res = {\n                choices: [\n                  {\n                    delta: {\n                      tool_calls: [\n                        {\n                          index: chunk.index || 0,\n                          id: chunk.content_block.id,\n                          type: \"function\",\n                          function: {\n                            name: chunk.content_block.name,\n                            arguments: \"\",\n                          },\n                        },\n                      ],\n                    },\n                    finish_reason: null,\n                    index: 0,\n                    logprobs: null,\n                  },\n                ],\n                created: parseInt(new Date().getTime() / 1000 + \"\", 10),\n                id: chunk.id || \"\",\n                model: chunk.model || \"\",\n                object: \"chat.completion.chunk\",\n                system_fingerprint: \"fp_a49d71b8a1\",\n                usage: {\n                  completion_tokens: chunk.usage?.output_tokens || 0,\n                  prompt_tokens: chunk.usage?.input_tokens || 0,\n                  total_tokens:\n                    (chunk.usage?.input_tokens || 0) +\n                    (chunk.usage?.output_tokens || 0),\n                },\n              };\n              controller.enqueue(\n                encoder.encode(`data: ${JSON.stringify(res)}\\n\\n`)\n              );\n            } else if (chunk.type === \"message_delta\") {\n              // Handle message end\n              const res = {\n                choices: [\n                  {\n                    delta: {},\n                    finish_reason:\n                      chunk.delta?.stop_reason === \"tool_use\"\n                        ? \"tool_calls\"\n                        : chunk.delta?.stop_reason === \"max_tokens\"\n                        ? \"length\"\n                        : chunk.delta?.stop_reason === \"stop_sequence\"\n                        ? \"content_filter\"\n                        : \"stop\",\n                    index: 0,\n                    logprobs: null,\n                  },\n                ],\n                created: parseInt(new Date().getTime() / 1000 + \"\", 10),\n                id: chunk.id || \"\",\n                model: chunk.model || \"\",\n                object: \"chat.completion.chunk\",\n                system_fingerprint: \"fp_a49d71b8a1\",\n                usage: {\n                  completion_tokens: chunk.usage?.output_tokens || 0,\n                  prompt_tokens: chunk.usage?.input_tokens || 0,\n                  total_tokens:\n                    (chunk.usage?.input_tokens || 0) +\n                    (chunk.usage?.output_tokens || 0),\n                },\n              };\n              controller.enqueue(\n                encoder.encode(`data: ${JSON.stringify(res)}\\n\\n`)\n              );\n            } else if (chunk.type === \"message_stop\") {\n              // Send end marker\n              controller.enqueue(encoder.encode(`data: [DONE]\\n\\n`));\n            } else {\n              // Handle other format responses (keep original logic as fallback)\n              const res = {\n                choices: [\n                  {\n                    delta: {\n                      role: \"assistant\",\n                      content: chunk.content?.[0]?.text || \"\",\n                    },\n                    finish_reason: chunk.stop_reason?.toLowerCase() || null,\n                    index: 0,\n                    logprobs: null,\n                  },\n                ],\n                created: parseInt(new Date().getTime() / 1000 + \"\", 10),\n                id: chunk.id || \"\",\n                model: chunk.model || \"\",\n                object: \"chat.completion.chunk\",\n                system_fingerprint: \"fp_a49d71b8a1\",\n                usage: {\n                  completion_tokens: chunk.usage?.output_tokens || 0,\n                  prompt_tokens: chunk.usage?.input_tokens || 0,\n                  total_tokens:\n                    (chunk.usage?.input_tokens || 0) +\n                    (chunk.usage?.output_tokens || 0),\n                },\n              };\n              controller.enqueue(\n                encoder.encode(`data: ${JSON.stringify(res)}\\n\\n`)\n              );\n            }\n          } catch (error: any) {\n            logger?.error(\n              `Error parsing ${providerName} stream chunk`,\n              chunkStr,\n              error.message\n            );\n          }\n        }\n      }\n    };\n\n    const stream = new ReadableStream({\n      async start(controller) {\n        const reader = response.body!.getReader();\n        let buffer = \"\";\n        try {\n          while (true) {\n            const { done, value } = await reader.read();\n            if (done) {\n              if (buffer) {\n                processLine(buffer, controller);\n              }\n              break;\n            }\n\n            buffer += decoder.decode(value, { stream: true });\n            const lines = buffer.split(\"\\n\");\n\n            buffer = lines.pop() || \"\";\n\n            for (const line of lines) {\n              processLine(line, controller);\n            }\n          }\n        } catch (error) {\n          controller.error(error);\n        } finally {\n          controller.close();\n        }\n      },\n    });\n\n    return new Response(stream, {\n      status: response.status,\n      statusText: response.statusText,\n      headers: response.headers,\n    });\n  }\n  return response;\n}\n"
  },
  {
    "path": "packages/core/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"module\": \"CommonJS\",\n    \"lib\": [\"ES2022\", \"DOM\"],\n    \"outDir\": \"./dist\",\n    \"rootDir\": \"./src\",\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"resolveJsonModule\": true,\n    \"moduleResolution\": \"node\",\n    \"declaration\": true,\n    \"sourceMap\": true,\n    \"baseUrl\": \"./\",\n    \"paths\": {\n      \"@/*\": [\"src/*\"]\n    }\n  },\n  \"include\": [\n    \"src/**/*\"\n  ],\n  \"exclude\": [\n    \"node_modules\",\n    \"dist\"\n  ]\n}\n"
  },
  {
    "path": "packages/server/.dockerignore",
    "content": "node_modules\ndist\n.git\n.gitignore\n*.md\n.vscode\n.idea\n*.log\n.env\n.DS_Store\ncoverage\n.nyc_output\n"
  },
  {
    "path": "packages/server/Dockerfile",
    "content": "# ===========================\n# 构建阶段\n# ===========================\nFROM node:20-alpine AS builder\n\nWORKDIR /app\n\n# 安装 pnpm\nRUN npm install -g pnpm@latest && \\\n    rm -rf /root/.npm\n\n# 复制工作区配置文件\nCOPY pnpm-workspace.yaml pnpm-lock.yaml package.json tsconfig.base.json ./\nCOPY scripts ./scripts\nCOPY packages/shared/package.json ./packages/shared/\nCOPY packages/core/package.json ./packages/core/\nCOPY packages/server/package.json ./packages/server/\n\n# 安装所有依赖（包括开发依赖）并清理\nRUN pnpm install --frozen-lockfile && \\\n    pnpm store prune\n\n# 复制源代码并构建\nCOPY packages/shared ./packages/shared\nCOPY packages/core ./packages/core\nCOPY packages/server ./packages/server\n\n# 构建所有包\nWORKDIR /app/packages/core\nRUN pnpm build\n\nWORKDIR /app/packages/shared\nRUN pnpm build\n\nWORKDIR /app/packages/server\nRUN pnpm build && \\\n    rm -rf node_modules/.cache\n\n# ===========================\n# 生产阶段\n# ===========================\nFROM node:20-alpine AS production\n\n# 安装 PM2、curl 和 pm2-logrotate\nRUN apk add --no-cache curl && \\\n    npm install -g pm2 pm2-logrotate --no-scripts && \\\n    pm2 install pm2-logrotate && \\\n    pm2 set pm2-logrotate:max_size 100M && \\\n    pm2 set pm2-logrotate:retain 5 && \\\n    pm2 set pm2-logrotate:compress true && \\\n    pm2 set pm2-logrotate:rotateInterval '0 0 * * *'\n\nWORKDIR /app\n\n# 复制 core 包（workspace 依赖）\nCOPY --from=builder /app/packages/core ./packages/core\n\n# 复制生产依赖\nCOPY --from=builder /app/packages/server/node_modules ./packages/server/node_modules\n\n# 从构建阶段复制 server bundle\nCOPY --from=builder /app/packages/server/dist ./packages/server/dist\n# 复制本地预先构建的 UI 产物到同一目录\nCOPY packages/ui/dist/. ./packages/server/dist/\n\n# 复制 PM2 配置文件\nCOPY packages/server/ecosystem.config.cjs /app/\n\n# 创建日志目录\nRUN mkdir -p /root/.claude-code-router/logs\n\n# 暴露端口\nEXPOSE 3456\n\n# 健康检查\nHEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \\\n    CMD curl -f http://127.0.0.1:3456/health || exit 1\n\n# 直接启动应用\nCMD [\"pm2-runtime\", \"start\", \"/app/ecosystem.config.cjs\"]\n"
  },
  {
    "path": "packages/server/ecosystem.config.cjs",
    "content": "module.exports = {\n  apps: [\n    {\n      name: 'claude-code-router-server',\n      script: '/app/packages/server/dist/index.js',\n      cwd: '/app/packages/server',\n      instances: 1,\n      autorestart: true,\n      watch: false,\n      max_memory_restart: '1G',\n      env: {\n        NODE_ENV: 'production',\n      },\n      // 日志配置\n      error_file: '/root/.claude-code-router/logs/error.log',\n      out_file: '/root/.claude-code-router/logs/out.log',\n      log_date_format: 'YYYY-MM-DD HH:mm:ss Z',\n      merge_logs: true,\n      // 启用日志时间戳\n      time: true,\n    },\n  ],\n};\n"
  },
  {
    "path": "packages/server/package.json",
    "content": "{\n  \"name\": \"@CCR/server\",\n  \"version\": \"2.0.0\",\n  \"description\": \"Server for Claude Code Router\",\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"scripts\": {\n    \"build\": \"node ../../scripts/build-server.js\",\n    \"dev\": \"ts-node src/index.ts\"\n  },\n  \"keywords\": [\n    \"claude\",\n    \"code\",\n    \"router\",\n    \"server\"\n  ],\n  \"author\": \"musistudio\",\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"@fastify/multipart\": \"^9.0.0\",\n    \"@fastify/static\": \"^8.2.0\",\n    \"@musistudio/llms\": \"workspace:*\",\n    \"adm-zip\": \"^0.5.16\",\n    \"dotenv\": \"^16.4.7\",\n    \"json5\": \"^2.2.3\",\n    \"lru-cache\": \"^11.2.2\",\n    \"rotating-file-stream\": \"^3.2.7\",\n    \"shell-quote\": \"^1.8.3\",\n    \"tiktoken\": \"^1.0.21\",\n    \"uuid\": \"^11.1.0\"\n  },\n  \"devDependencies\": {\n    \"@CCR/shared\": \"workspace:*\",\n    \"@types/adm-zip\": \"^0.5.7\",\n    \"@types/node\": \"^24.0.15\",\n    \"esbuild\": \"^0.25.1\",\n    \"fastify\": \"^5.4.0\",\n    \"ts-node\": \"^10.9.2\",\n    \"typescript\": \"^5.8.2\"\n  }\n}\n"
  },
  {
    "path": "packages/server/src/agents/image.agent.ts",
    "content": "import { IAgent, ITool } from \"./type\";\nimport { createHash } from \"crypto\";\nimport * as LRU from \"lru-cache\";\n\ninterface ImageCacheEntry {\n  source: any;\n  timestamp: number;\n}\n\nclass ImageCache {\n  private cache: any;\n\n  constructor(maxSize = 100) {\n    const CacheClass: any = (LRU as any).LRUCache || (LRU as any);\n    this.cache = new CacheClass({\n      max: maxSize,\n      ttl: 5 * 60 * 1000, // 5 minutes\n    });\n  }\n\n  storeImage(id: string, source: any): void {\n    if (this.hasImage(id)) return;\n    this.cache.set(id, {\n      source,\n      timestamp: Date.now(),\n    });\n  }\n\n  getImage(id: string): any {\n    const entry = this.cache.get(id);\n    return entry ? entry.source : null;\n  }\n\n  hasImage(hash: string): boolean {\n    return this.cache.has(hash);\n  }\n\n  clear(): void {\n    this.cache.clear();\n  }\n\n  size(): number {\n    return this.cache.size;\n  }\n}\n\nconst imageCache = new ImageCache();\n\nexport class ImageAgent implements IAgent {\n  name = \"image\";\n  tools: Map<string, ITool>;\n\n  constructor() {\n    this.tools = new Map<string, ITool>();\n    this.appendTools();\n  }\n\n  shouldHandle(req: any, config: any): boolean {\n    if (!config.Router.image || req.body.model === config.Router.image)\n      return false;\n    const lastMessage = req.body.messages[req.body.messages.length - 1];\n    if (\n      !config.forceUseImageAgent &&\n      lastMessage.role === \"user\" &&\n      Array.isArray(lastMessage.content) &&\n      lastMessage.content.find(\n        (item: any) =>\n          item.type === \"image\" ||\n          (Array.isArray(item?.content) &&\n            item.content.some((sub: any) => sub.type === \"image\"))\n      )\n    ) {\n      req.body.model = config.Router.image;\n      const images: any[] = [];\n      lastMessage.content\n        .filter((item: any) => item.type === \"tool_result\")\n        .forEach((item: any) => {\n          if (Array.isArray(item.content)) {\n            item.content.forEach((element: any) => {\n              if (element.type === \"image\") {\n                images.push(element);\n              }\n            });\n            item.content = \"read image successfully\";\n          }\n        });\n      lastMessage.content.push(...images);\n      return false;\n    }\n    return req.body.messages.some(\n      (msg: any) =>\n        msg.role === \"user\" &&\n        Array.isArray(msg.content) &&\n        msg.content.some(\n          (item: any) =>\n            item.type === \"image\" ||\n            (Array.isArray(item?.content) &&\n              item.content.some((sub: any) => sub.type === \"image\"))\n        )\n    );\n  }\n\n  appendTools() {\n    this.tools.set(\"analyzeImage\", {\n      name: \"analyzeImage\",\n      description:\n        \"Analyse image or images by ID and extract information such as OCR text, objects, layout, colors, or safety signals.\",\n      input_schema: {\n        type: \"object\",\n        properties: {\n          imageId: {\n            type: \"array\",\n            description: \"an array of IDs to analyse\",\n            items: {\n              type: \"string\",\n            },\n          },\n          task: {\n            type: \"string\",\n            description:\n              \"Details of task to perform on the image.The more detailed, the better\",\n          },\n          regions: {\n            type: \"array\",\n            description: \"Optional regions of interest within the image\",\n            items: {\n              type: \"object\",\n              properties: {\n                name: {\n                  type: \"string\",\n                  description: \"Optional label for the region\",\n                },\n                x: { type: \"number\", description: \"X coordinate\" },\n                y: { type: \"number\", description: \"Y coordinate\" },\n                w: { type: \"number\", description: \"Width of the region\" },\n                h: { type: \"number\", description: \"Height of the region\" },\n                units: {\n                  type: \"string\",\n                  enum: [\"px\", \"pct\"],\n                  description: \"Units for coordinates and size\",\n                },\n              },\n              required: [\"x\", \"y\", \"w\", \"h\", \"units\"],\n            },\n          },\n        },\n        required: [\"imageId\", \"task\"],\n      },\n      handler: async (args, context) => {\n        const imageMessages = [];\n        let imageId;\n\n        // Create image messages from cached images\n        if (args.imageId) {\n          if (Array.isArray(args.imageId)) {\n            args.imageId.forEach((imgId: string) => {\n              const image = imageCache.getImage(\n                `${context.req.id}_Image#${imgId}`\n              );\n              if (image) {\n                imageMessages.push({\n                  type: \"image\",\n                  source: image,\n                });\n              }\n            });\n          } else {\n            const image = imageCache.getImage(\n              `${context.req.id}_Image#${args.imageId}`\n            );\n            if (image) {\n              imageMessages.push({\n                type: \"image\",\n                source: image,\n              });\n            }\n          }\n          imageId = args.imageId;\n          delete args.imageId;\n        }\n\n        const userMessage =\n          context.req.body.messages[context.req.body.messages.length - 1];\n        if (userMessage.role === \"user\" && Array.isArray(userMessage.content)) {\n          const msgs = userMessage.content.filter(\n            (item: any) =>\n              item.type === \"text\" &&\n              !item.text.includes(\n                \"This is an image, if you need to view or analyze it, you need to extract the imageId\"\n              )\n          );\n          imageMessages.push(...msgs);\n        }\n\n        if (Object.keys(args).length > 0) {\n          imageMessages.push({\n            type: \"text\",\n            text: JSON.stringify(args),\n          });\n        }\n\n        // Send to analysis agent and get response\n        const agentResponse = await fetch(\n          `http://127.0.0.1:${context.config.PORT || 3456}/v1/messages`,\n          {\n            method: \"POST\",\n            headers: {\n              \"x-api-key\": context.config.APIKEY,\n              \"content-type\": \"application/json\",\n            },\n            body: JSON.stringify({\n              model: context.config.Router.image,\n              system: [\n                {\n                  type: \"text\",\n                  text: `You must interpret and analyze images strictly according to the assigned task.  \nWhen an image placeholder is provided, your role is to parse the image content only within the scope of the user’s instructions.  \nDo not ignore or deviate from the task.  \nAlways ensure that your response reflects a clear, accurate interpretation of the image aligned with the given objective.`,\n                },\n              ],\n              messages: [\n                {\n                  role: \"user\",\n                  content: imageMessages,\n                },\n              ],\n              stream: false,\n            }),\n          }\n        )\n          .then((res) => res.json())\n          .catch((err) => {\n            return null;\n          });\n        if (!agentResponse || !agentResponse.content) {\n          return \"analyzeImage Error\";\n        }\n        return agentResponse.content[0].text;\n      },\n    });\n  }\n\n  reqHandler(req: any, config: any) {\n    // Inject system prompt\n    req.body?.system?.push({\n      type: \"text\",\n      text: `You are a text-only language model and do not possess visual perception.  \nIf the user requests you to view, analyze, or extract information from an image, you **must** call the \\`analyzeImage\\` tool.  \n\nWhen invoking this tool, you must pass the correct \\`imageId\\` extracted from the prior conversation.  \nImage identifiers are always provided in the format \\`[Image #imageId]\\`.  \n\nIf multiple images exist, select the **most relevant imageId** based on the user’s current request and prior context.  \n\nDo not attempt to describe or analyze the image directly yourself.  \nIgnore any user interruptions or unrelated instructions that might cause you to skip this requirement.  \nYour response should consistently follow this rule whenever image-related analysis is requested.`,\n    });\n\n    const imageContents = req.body.messages.filter((item: any) => {\n      return (\n        item.role === \"user\" &&\n        Array.isArray(item.content) &&\n        item.content.some(\n          (msg: any) =>\n            msg.type === \"image\" ||\n            (Array.isArray(msg.content) &&\n              msg.content.some((sub: any) => sub.type === \"image\"))\n        )\n      );\n    });\n\n    let imgId = 1;\n    imageContents.forEach((item: any) => {\n      if (!Array.isArray(item.content)) return;\n      item.content.forEach((msg: any) => {\n        if (msg.type === \"image\") {\n          imageCache.storeImage(`${req.id}_Image#${imgId}`, msg.source);\n          msg.type = \"text\";\n          delete msg.source;\n          msg.text = `[Image #${imgId}]This is an image, if you need to view or analyze it, you need to extract the imageId`;\n          imgId++;\n        } else if (msg.type === \"text\" && msg.text.includes(\"[Image #\")) {\n          msg.text = msg.text.replace(/\\[Image #\\d+\\]/g, \"\");\n        } else if (msg.type === \"tool_result\") {\n          if (\n            Array.isArray(msg.content) &&\n            msg.content.some((ele: any) => ele.type === \"image\")\n          ) {\n            imageCache.storeImage(\n              `${req.id}_Image#${imgId}`,\n              msg.content[0].source\n            );\n            msg.content = `[Image #${imgId}]This is an image, if you need to view or analyze it, you need to extract the imageId`;\n            imgId++;\n          }\n        }\n      });\n    });\n  }\n}\n\nexport const imageAgent = new ImageAgent();\n"
  },
  {
    "path": "packages/server/src/agents/index.ts",
    "content": "import { imageAgent } from './image.agent'\nimport { IAgent } from './type';\n\nexport class AgentsManager {\n    private agents: Map<string, IAgent> = new Map();\n\n    /**\n     * Register an agent\n     * @param agent The agent instance to register\n     * @param isDefault Whether to set as default agent\n     */\n    registerAgent(agent: IAgent): void {\n        this.agents.set(agent.name, agent);\n    }\n    /**\n     * Find agent by name\n     * @param name Agent name\n     * @returns Found agent instance, undefined if not found\n     */\n    getAgent(name: string): IAgent | undefined {\n        return this.agents.get(name);\n    }\n\n    /**\n     * Get all registered agents\n     * @returns Array of all agent instances\n     */\n    getAllAgents(): IAgent[] {\n        return Array.from(this.agents.values());\n    }\n\n\n    /**\n     * Get all agent tools\n     * @returns Array of tools\n     */\n    getAllTools(): any[] {\n        const allTools: any[] = [];\n        for (const agent of this.agents.values()) {\n            allTools.push(...agent.tools.values());\n        }\n        return allTools;\n    }\n}\n\nconst agentsManager = new AgentsManager()\nagentsManager.registerAgent(imageAgent)\nexport default agentsManager\n"
  },
  {
    "path": "packages/server/src/agents/type.ts",
    "content": "export interface ITool {\n  name: string;\n  description: string;\n  input_schema: any;\n\n  handler: (args: any, context: any) => Promise<string>;\n}\n\nexport interface IAgent {\n  name: string;\n\n  tools: Map<string, ITool>;\n\n  shouldHandle: (req: any, config: any) => boolean;\n\n  reqHandler: (req: any, config: any) => void;\n\n  resHandler?: (payload: any, config: any) => void;\n}\n"
  },
  {
    "path": "packages/server/src/index.ts",
    "content": "import { existsSync } from \"fs\";\nimport { writeFile } from \"fs/promises\";\nimport { homedir } from \"os\";\nimport { join } from \"path\";\nimport { initConfig, initDir } from \"./utils\";\nimport { createServer } from \"./server\";\nimport { apiKeyAuth } from \"./middleware/auth\";\nimport { CONFIG_FILE, HOME_DIR, listPresets } from \"@CCR/shared\";\nimport { createStream } from 'rotating-file-stream';\nimport { sessionUsageCache } from \"@musistudio/llms\";\nimport { SSEParserTransform } from \"./utils/SSEParser.transform\";\nimport { SSESerializerTransform } from \"./utils/SSESerializer.transform\";\nimport { rewriteStream } from \"./utils/rewriteStream\";\nimport JSON5 from \"json5\";\nimport { IAgent, ITool } from \"./agents/type\";\nimport agentsManager from \"./agents\";\nimport { EventEmitter } from \"node:events\";\nimport { pluginManager, tokenSpeedPlugin } from \"@musistudio/llms\";\n\nconst event = new EventEmitter()\n\nasync function initializeClaudeConfig() {\n  const homeDir = homedir();\n  const configPath = join(homeDir, \".claude.json\");\n  if (!existsSync(configPath)) {\n    const userID = Array.from(\n      { length: 64 },\n      () => Math.random().toString(16)[2]\n    ).join(\"\");\n    const configContent = {\n      numStartups: 184,\n      autoUpdaterStatus: \"enabled\",\n      userID,\n      hasCompletedOnboarding: true,\n      lastOnboardingVersion: \"1.0.17\",\n      projects: {},\n    };\n    await writeFile(configPath, JSON.stringify(configContent, null, 2));\n  }\n}\n\ninterface RunOptions {\n  port?: number;\n  logger?: any;\n}\n\n/**\n * Plugin configuration from config file\n */\ninterface PluginConfig {\n  name: string;\n  enabled?: boolean;\n  options?: Record<string, any>;\n}\n\n/**\n * Register plugins from configuration\n * @param serverInstance Server instance\n * @param config Application configuration\n */\nasync function registerPluginsFromConfig(serverInstance: any, config: any): Promise<void> {\n  // Get plugins configuration from config file\n  const pluginsConfig: PluginConfig[] = config.plugins || config.Plugins || [];\n\n  for (const pluginConfig of pluginsConfig) {\n      const { name, enabled = false, options = {} } = pluginConfig;\n\n      switch (name) {\n        case 'token-speed':\n          pluginManager.registerPlugin(tokenSpeedPlugin, {\n            enabled,\n            outputHandlers: [\n              {\n                type: 'temp-file',\n                enabled: true\n              }\n            ],\n            ...options\n          });\n          break;\n\n        default:\n          console.warn(`Unknown plugin: ${name}`);\n          break;\n      }\n    }\n  // Enable all registered plugins\n  await pluginManager.enablePlugins(serverInstance);\n}\n\nasync function getServer(options: RunOptions = {}) {\n  await initializeClaudeConfig();\n  await initDir();\n  const config = await initConfig();\n\n  // Check if Providers is configured\n  const providers = config.Providers || config.providers || [];\n  const hasProviders = providers && providers.length > 0;\n\n  let HOST = config.HOST || \"127.0.0.1\";\n\n  if (hasProviders) {\n    HOST = config.HOST;\n    if (!config.APIKEY) {\n      HOST = \"127.0.0.1\";\n    }\n  } else {\n    // When no providers are configured, listen on 0.0.0.0 without authentication\n    HOST = \"0.0.0.0\";\n    console.log(\"ℹ️  No providers configured. Listening on 0.0.0.0 without authentication.\");\n  }\n\n  const port = config.PORT || 3456;\n\n  // Use port from environment variable if set (for background process)\n  const servicePort = process.env.SERVICE_PORT\n    ? parseInt(process.env.SERVICE_PORT)\n    : port;\n\n  // Configure logger based on config settings or external options\n  const pad = (num: number) => (num > 9 ? \"\" : \"0\") + num;\n  const generator = (time: number | Date | undefined, index: number | undefined) => {\n    let date: Date;\n    if (!time) {\n      date = new Date();\n    } else if (typeof time === 'number') {\n      date = new Date(time);\n    } else {\n      date = time;\n    }\n\n    const month = date.getFullYear() + \"\" + pad(date.getMonth() + 1);\n    const day = pad(date.getDate());\n    const hour = pad(date.getHours());\n    const minute = pad(date.getMinutes());\n\n    return `./logs/ccr-${month}${day}${hour}${minute}${pad(date.getSeconds())}${index ? `_${index}` : ''}.log`;\n  };\n\n  let loggerConfig: any;\n\n  // Use external logger configuration if provided\n  if (options.logger !== undefined) {\n    loggerConfig = options.logger;\n  } else {\n    // Enable logger if not provided and config.LOG !== false\n    if (config.LOG !== false) {\n      // Set config.LOG to true (if not already set)\n      if (config.LOG === undefined) {\n        config.LOG = true;\n      }\n      loggerConfig = {\n        level: config.LOG_LEVEL || \"debug\",\n        stream: createStream(generator, {\n          path: HOME_DIR,\n          maxFiles: 3,\n          interval: \"1d\",\n          compress: false,\n          maxSize: \"50M\"\n        }),\n      };\n    } else {\n      loggerConfig = false;\n    }\n  }\n\n  const presets = await listPresets();\n\n  const serverInstance = await createServer({\n    jsonPath: CONFIG_FILE,\n    initialConfig: {\n      // ...config,\n      providers: config.Providers || config.providers,\n      HOST: HOST,\n      PORT: servicePort,\n      LOG_FILE: join(\n        homedir(),\n        \".claude-code-router\",\n        \"claude-code-router.log\"\n      ),\n    },\n    logger: loggerConfig,\n  });\n\n  await Promise.allSettled(\n      presets.map(async preset => await serverInstance.registerNamespace(`/preset/${preset.name}`, preset.config))\n  )\n\n  // Register and configure plugins from config\n  await registerPluginsFromConfig(serverInstance, config);\n\n  // Add async preHandler hook for authentication\n  serverInstance.addHook(\"preHandler\", async (req: any, reply: any) => {\n    return new Promise<void>((resolve, reject) => {\n      const done = (err?: Error) => {\n        if (err) reject(err);\n        else resolve();\n      };\n      // Call the async auth function\n      apiKeyAuth(config)(req, reply, done).catch(reject);\n    });\n  });\n  serverInstance.addHook(\"preHandler\", async (req: any, reply: any) => {\n    const url = new URL(`http://127.0.0.1${req.url}`);\n    req.pathname = url.pathname;\n    if (req.pathname.endsWith(\"/v1/messages\") && req.pathname !== \"/v1/messages\") {\n      req.preset = req.pathname.replace(\"/v1/messages\", \"\").replace(\"/\", \"\");\n    }\n  })\n\n  serverInstance.addHook(\"preHandler\", async (req: any, reply: any) => {\n    if (req.pathname.endsWith(\"/v1/messages\")) {\n      const useAgents = []\n\n      for (const agent of agentsManager.getAllAgents()) {\n        if (agent.shouldHandle(req, config)) {\n          // Set agent identifier\n          useAgents.push(agent.name)\n\n          // change request body\n          agent.reqHandler(req, config);\n\n          // append agent tools\n          if (agent.tools.size) {\n            if (!req.body?.tools?.length) {\n              req.body.tools = []\n            }\n            req.body.tools.unshift(...Array.from(agent.tools.values()).map(item => {\n              return {\n                name: item.name,\n                description: item.description,\n                input_schema: item.input_schema\n              }\n            }))\n          }\n        }\n      }\n\n      if (useAgents.length) {\n        req.agents = useAgents;\n      }\n    }\n  });\n  serverInstance.addHook(\"onError\", async (request: any, reply: any, error: any) => {\n    event.emit('onError', request, reply, error);\n  })\n  serverInstance.addHook(\"onSend\", (req: any, reply: any, payload: any, done: any) => {\n    if (req.sessionId && req.pathname.endsWith(\"/v1/messages\")) {\n      if (payload instanceof ReadableStream) {\n        if (req.agents) {\n          const abortController = new AbortController();\n          const eventStream = payload.pipeThrough(new SSEParserTransform())\n          let currentAgent: undefined | IAgent;\n          let currentToolIndex = -1\n          let currentToolName = ''\n          let currentToolArgs = ''\n          let currentToolId = ''\n          const toolMessages: any[] = []\n          const assistantMessages: any[] = []\n          // Store Anthropic format message body, distinguishing text and tool types\n          return done(null, rewriteStream(eventStream, async (data, controller) => {\n            try {\n              // Detect tool call start\n              if (data.event === 'content_block_start' && data?.data?.content_block?.name) {\n                const agent = req.agents.find((name: string) => agentsManager.getAgent(name)?.tools.get(data.data.content_block.name))\n                if (agent) {\n                  currentAgent = agentsManager.getAgent(agent)\n                  currentToolIndex = data.data.index\n                  currentToolName = data.data.content_block.name\n                  currentToolId = data.data.content_block.id\n                  return undefined;\n                }\n              }\n\n              // Collect tool arguments\n              if (currentToolIndex > -1 && data.data.index === currentToolIndex && data.data?.delta?.type === 'input_json_delta') {\n                currentToolArgs += data.data?.delta?.partial_json;\n                return undefined;\n              }\n\n              // Tool call completed, handle agent invocation\n              if (currentToolIndex > -1 && data.data.index === currentToolIndex && data.data.type === 'content_block_stop') {\n                try {\n                  const args = JSON5.parse(currentToolArgs);\n                  assistantMessages.push({\n                    type: \"tool_use\",\n                    id: currentToolId,\n                    name: currentToolName,\n                    input: args\n                  })\n                  const toolResult = await currentAgent?.tools.get(currentToolName)?.handler(args, {\n                    req,\n                    config\n                  });\n                  toolMessages.push({\n                    \"tool_use_id\": currentToolId,\n                    \"type\": \"tool_result\",\n                    \"content\": toolResult\n                  })\n                  currentAgent = undefined\n                  currentToolIndex = -1\n                  currentToolName = ''\n                  currentToolArgs = ''\n                  currentToolId = ''\n                } catch (e) {\n                  console.log(e);\n                }\n                return undefined;\n              }\n\n              if (data.event === 'message_delta' && toolMessages.length) {\n                req.body.messages.push({\n                  role: 'assistant',\n                  content: assistantMessages\n                })\n                req.body.messages.push({\n                  role: 'user',\n                  content: toolMessages\n                })\n                const response = await fetch(`http://127.0.0.1:${config.PORT || 3456}/v1/messages`, {\n                  method: \"POST\",\n                  headers: {\n                    'x-api-key': config.APIKEY,\n                    'content-type': 'application/json',\n                  },\n                  body: JSON.stringify(req.body),\n                })\n                if (!response.ok) {\n                  return undefined;\n                }\n                const stream = response.body!.pipeThrough(new SSEParserTransform() as any)\n                const reader = stream.getReader()\n                while (true) {\n                  try {\n                    const {value, done} = await reader.read();\n                    if (done) {\n                      break;\n                    }\n                    const eventData = value as any;\n                    if (['message_start', 'message_stop'].includes(eventData.event)) {\n                      continue\n                    }\n\n                    // Check if stream is still writable\n                    if (!controller.desiredSize) {\n                      break;\n                    }\n\n                    controller.enqueue(eventData)\n                  }catch (readError: any) {\n                    if (readError.name === 'AbortError' || readError.code === 'ERR_STREAM_PREMATURE_CLOSE') {\n                      abortController.abort(); // Abort all related operations\n                      break;\n                    }\n                    throw readError;\n                  }\n\n                }\n                return undefined\n              }\n              return data\n            }catch (error: any) {\n              console.error('Unexpected error in stream processing:', error);\n\n              // Handle premature stream closure error\n              if (error.code === 'ERR_STREAM_PREMATURE_CLOSE') {\n                abortController.abort();\n                return undefined;\n              }\n\n              // Re-throw other errors\n              throw error;\n            }\n          }).pipeThrough(new SSESerializerTransform()))\n        }\n\n        const [originalStream, clonedStream] = payload.tee();\n        const read = async (stream: ReadableStream) => {\n          const reader = stream.getReader();\n          try {\n            while (true) {\n              const { done, value } = await reader.read();\n              if (done) break;\n              // Process the value if needed\n              const dataStr = new TextDecoder().decode(value);\n              if (!dataStr.startsWith(\"event: message_delta\")) {\n                continue;\n              }\n              const str = dataStr.slice(27);\n              try {\n                const message = JSON.parse(str);\n                sessionUsageCache.put(req.sessionId, message.usage);\n              } catch {}\n            }\n          } catch (readError: any) {\n            if (readError.name === 'AbortError' || readError.code === 'ERR_STREAM_PREMATURE_CLOSE') {\n              console.error('Background read stream closed prematurely');\n            } else {\n              console.error('Error in background stream reading:', readError);\n            }\n          } finally {\n            reader.releaseLock();\n          }\n        }\n        read(clonedStream);\n        return done(null, originalStream)\n      }\n      sessionUsageCache.put(req.sessionId, payload.usage);\n      if (typeof payload ==='object') {\n        if (payload.error) {\n          return done(payload.error, null)\n        } else {\n          return done(payload, null)\n        }\n      }\n    }\n    if (typeof payload ==='object' && payload.error) {\n      return done(payload.error, null)\n    }\n    done(null, payload)\n  });\n  serverInstance.addHook(\"onSend\", async (req: any, reply: any, payload: any) => {\n    event.emit('onSend', req, reply, payload);\n    return payload;\n  });\n\n  // Add global error handlers to prevent the service from crashing\n  process.on(\"uncaughtException\", (err) => {\n    serverInstance.app.log.error(\"Uncaught exception:\", err);\n  });\n\n  process.on(\"unhandledRejection\", (reason, promise) => {\n    serverInstance.app.log.error(\"Unhandled rejection at:\", promise, \"reason:\", reason);\n  });\n\n  return serverInstance;\n}\n\nasync function run() {\n  const server = await getServer();\n  server.app.post(\"/api/restart\", async () => {\n    setTimeout(async () => {\n      process.exit(0);\n    }, 100);\n\n    return { success: true, message: \"Service restart initiated\" }\n  });\n  await server.start();\n}\n\nexport { getServer };\nexport type { RunOptions };\nexport type { IAgent, ITool } from \"./agents/type\";\nexport { initDir, initConfig, readConfigFile, writeConfigFile, backupConfigFile } from \"./utils\";\nexport { pluginManager, tokenSpeedPlugin } from \"@musistudio/llms\";\n\n// Start service if this file is run directly\nif (require.main === module) {\n  run().catch((error) => {\n    console.error('Failed to start server:', error);\n    process.exit(1);\n  });\n}\n"
  },
  {
    "path": "packages/server/src/middleware/auth.ts",
    "content": "import { FastifyRequest, FastifyReply } from \"fastify\";\n\nexport const apiKeyAuth =\n  (config: any) =>\n  async (req: FastifyRequest, reply: FastifyReply, done: () => void) => {\n    // Public endpoints that don't require authentication\n    const publicPaths = [\"/\", \"/health\"];\n    if (publicPaths.includes(req.url) || req.url.startsWith(\"/ui\")) {\n      return done();\n    }\n\n    // Check if Providers is empty or not configured\n    const providers = config.Providers || config.providers || [];\n    if (!providers || providers.length === 0) {\n      // No providers configured, skip authentication\n      return done();\n    }\n\n    const apiKey = config.APIKEY;\n    if (!apiKey) {\n      // If no API key is set, enable CORS for local\n      const allowedOrigins = [\n        `http://127.0.0.1:${config.PORT || 3456}`,\n        `http://localhost:${config.PORT || 3456}`,\n      ];\n      if (req.headers.origin && !allowedOrigins.includes(req.headers.origin)) {\n        reply.status(403).send(\"CORS not allowed for this origin\");\n        return;\n      } else {\n        reply.header('Access-Control-Allow-Origin', `http://127.0.0.1:${config.PORT || 3456}`);\n        reply.header('Access-Control-Allow-Origin', `http://localhost:${config.PORT || 3456}`);\n      }\n      return done();\n    }\n\n    const authHeaderValue =\n      req.headers.authorization || req.headers[\"x-api-key\"];\n    const authKey: string = Array.isArray(authHeaderValue)\n      ? authHeaderValue[0]\n      : authHeaderValue || \"\";\n    if (!authKey) {\n      reply.status(401).send(\"APIKEY is missing\");\n      return;\n    }\n    let token = \"\";\n    if (authKey.startsWith(\"Bearer\")) {\n      token = authKey.split(\" \")[1];\n    } else {\n      token = authKey;\n    }\n\n    if (token !== apiKey) {\n      reply.status(401).send(\"Invalid API key\");\n      return;\n    }\n\n    done();\n  };\n"
  },
  {
    "path": "packages/server/src/server.ts",
    "content": "import Server, { calculateTokenCount, TokenizerService } from \"@musistudio/llms\";\nimport { readConfigFile, writeConfigFile, backupConfigFile } from \"./utils\";\nimport { join } from \"path\";\nimport fastifyStatic from \"@fastify/static\";\nimport { readdirSync, statSync, readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync, rmSync } from \"fs\";\nimport { homedir } from \"os\";\nimport {\n  getPresetDir,\n  readManifestFromDir,\n  manifestToPresetFile,\n  saveManifest,\n  isPresetInstalled,\n  extractPreset,\n  HOME_DIR,\n  extractMetadata,\n  loadConfigFromManifest,\n  downloadPresetToTemp,\n  getTempDir,\n  findMarketPresetByName,\n  getMarketPresets,\n  type PresetFile,\n  type ManifestFile,\n  type PresetMetadata,\n} from \"@CCR/shared\";\nimport fastifyMultipart from \"@fastify/multipart\";\nimport AdmZip from \"adm-zip\";\n\nexport const createServer = async (config: any): Promise<any> => {\n  const server = new Server(config);\n  const app = server.app;\n\n  app.register(fastifyMultipart, {\n    limits: {\n      fileSize: 50 * 1024 * 1024, // 50MB\n    },\n  });\n\n  app.post(\"/v1/messages/count_tokens\", async (req: any, reply: any) => {\n    const {messages, tools, system, model} = req.body;\n    const tokenizerService = (app as any)._server!.tokenizerService as TokenizerService;\n\n    // If model is specified in \"providerName,modelName\" format, use the configured tokenizer\n    if (model && model.includes(\",\") && tokenizerService) {\n      try {\n        const [provider, modelName] = model.split(\",\");\n        req.log?.info(`Looking up tokenizer for provider: ${provider}, model: ${modelName}`);\n\n        const tokenizerConfig = tokenizerService.getTokenizerConfigForModel(provider, modelName);\n\n        if (!tokenizerConfig) {\n          req.log?.warn(`No tokenizer config found for ${provider},${modelName}, using default tiktoken`);\n        } else {\n          req.log?.info(`Using tokenizer config: ${JSON.stringify(tokenizerConfig)}`);\n        }\n\n        const result = await tokenizerService.countTokens(\n          { messages, system, tools },\n          tokenizerConfig\n        );\n\n        return {\n          \"input_tokens\": result.tokenCount,\n          \"tokenizer\": result.tokenizerUsed,\n        };\n      } catch (error: any) {\n        req.log?.error(`Error using configured tokenizer: ${error.message}`);\n        req.log?.error(error.stack);\n        // Fall back to default calculation\n      }\n    } else {\n      if (!model) {\n        req.log?.info(`No model specified, using default tiktoken`);\n      } else if (!model.includes(\",\")) {\n        req.log?.info(`Model \"${model}\" does not contain comma, using default tiktoken`);\n      } else if (!tokenizerService) {\n        req.log?.warn(`TokenizerService not available, using default tiktoken`);\n      }\n    }\n\n    // Default to tiktoken calculation\n    const tokenCount = calculateTokenCount(messages, system, tools);\n    return { \"input_tokens\": tokenCount }\n  });\n\n  // Add endpoint to read config.json with access control\n  app.get(\"/api/config\", async (req: any, reply: any) => {\n    return await readConfigFile();\n  });\n\n  app.get(\"/api/transformers\", async (req: any, reply: any) => {\n    const transformers =\n      (app as any)._server!.transformerService.getAllTransformers();\n    const transformerList = Array.from(transformers.entries()).map(\n      ([name, transformer]: any) => ({\n        name,\n        endpoint: transformer.endPoint || null,\n      })\n    );\n    return { transformers: transformerList };\n  });\n\n  // Add endpoint to save config.json with access control\n  app.post(\"/api/config\", async (req: any, reply: any) => {\n    const newConfig = req.body;\n\n    // Backup existing config file if it exists\n    const backupPath = await backupConfigFile();\n    if (backupPath) {\n      console.log(`Backed up existing configuration file to ${backupPath}`);\n    }\n\n    await writeConfigFile(newConfig);\n    return { success: true, message: \"Config saved successfully\" };\n  });\n\n  // Register static file serving with caching\n  app.register(fastifyStatic, {\n    root: join(__dirname, \"..\", \"dist\"),\n    prefix: \"/ui/\",\n    maxAge: \"1h\",\n  });\n\n  // Redirect /ui to /ui/ for proper static file serving\n  app.get(\"/ui\", async (_: any, reply: any) => {\n    return reply.redirect(\"/ui/\");\n  });\n\n  // Get log file list endpoint\n  app.get(\"/api/logs/files\", async (req: any, reply: any) => {\n    try {\n      const logDir = join(homedir(), \".claude-code-router\", \"logs\");\n      const logFiles: Array<{ name: string; path: string; size: number; lastModified: string }> = [];\n\n      if (existsSync(logDir)) {\n        const files = readdirSync(logDir);\n\n        for (const file of files) {\n          if (file.endsWith('.log')) {\n            const filePath = join(logDir, file);\n            const stats = statSync(filePath);\n\n            logFiles.push({\n              name: file,\n              path: filePath,\n              size: stats.size,\n              lastModified: stats.mtime.toISOString()\n            });\n          }\n        }\n\n        // Sort by modification time in descending order\n        logFiles.sort((a, b) => new Date(b.lastModified).getTime() - new Date(a.lastModified).getTime());\n      }\n\n      return logFiles;\n    } catch (error) {\n      console.error(\"Failed to get log files:\", error);\n      reply.status(500).send({ error: \"Failed to get log files\" });\n    }\n  });\n\n  // Get log content endpoint\n  app.get(\"/api/logs\", async (req: any, reply: any) => {\n    try {\n      const filePath = (req.query as any).file as string;\n      let logFilePath: string;\n\n      if (filePath) {\n        // If file path is specified, use the specified path\n        logFilePath = filePath;\n      } else {\n        // If file path is not specified, use default log file path\n        logFilePath = join(homedir(), \".claude-code-router\", \"logs\", \"app.log\");\n      }\n\n      if (!existsSync(logFilePath)) {\n        return [];\n      }\n\n      const logContent = readFileSync(logFilePath, 'utf8');\n      const logLines = logContent.split('\\n').filter(line => line.trim())\n\n      return logLines;\n    } catch (error) {\n      console.error(\"Failed to get logs:\", error);\n      reply.status(500).send({ error: \"Failed to get logs\" });\n    }\n  });\n\n  // Clear log content endpoint\n  app.delete(\"/api/logs\", async (req: any, reply: any) => {\n    try {\n      const filePath = (req.query as any).file as string;\n      let logFilePath: string;\n\n      if (filePath) {\n        // If file path is specified, use the specified path\n        logFilePath = filePath;\n      } else {\n        // If file path is not specified, use default log file path\n        logFilePath = join(homedir(), \".claude-code-router\", \"logs\", \"app.log\");\n      }\n\n      if (existsSync(logFilePath)) {\n        writeFileSync(logFilePath, '', 'utf8');\n      }\n\n      return { success: true, message: \"Logs cleared successfully\" };\n    } catch (error) {\n      console.error(\"Failed to clear logs:\", error);\n      reply.status(500).send({ error: \"Failed to clear logs\" });\n    }\n  });\n\n  // Get presets list\n  app.get(\"/api/presets\", async (req: any, reply: any) => {\n    try {\n      const presetsDir = join(HOME_DIR, \"presets\");\n\n      if (!existsSync(presetsDir)) {\n        return { presets: [] };\n      }\n\n      const entries = readdirSync(presetsDir, { withFileTypes: true });\n      const presetDirs = entries.filter(e => e.isDirectory() && !e.name.startsWith('.')).map(e => e.name);\n\n      const presets: Array<PresetMetadata & { installed: boolean; id: string }> = [];\n\n      for (const dirName of presetDirs) {\n        const presetDir = join(presetsDir, dirName);\n        try {\n          const manifestPath = join(presetDir, \"manifest.json\");\n          const content = readFileSync(manifestPath, 'utf-8');\n          const manifest = JSON.parse(content);\n\n          // Extract metadata fields\n          const { Providers, Router, PORT, HOST, API_TIMEOUT_MS, PROXY_URL, LOG, LOG_LEVEL, StatusLine, NON_INTERACTIVE_MODE, ...metadata } = manifest;\n\n          presets.push({\n            id: dirName,  // Use directory name as unique identifier\n            name: metadata.name || dirName,\n            version: metadata.version || '1.0.0',\n            description: metadata.description,\n            author: metadata.author,\n            homepage: metadata.homepage,\n            repository: metadata.repository,\n            license: metadata.license,\n            keywords: metadata.keywords,\n            ccrVersion: metadata.ccrVersion,\n            source: metadata.source,\n            sourceType: metadata.sourceType,\n            checksum: metadata.checksum,\n            installed: true,\n          });\n        } catch (error) {\n          console.error(`Failed to read preset ${dirName}:`, error);\n        }\n      }\n\n      return { presets };\n    } catch (error) {\n      console.error(\"Failed to get presets:\", error);\n      reply.status(500).send({ error: \"Failed to get presets\" });\n    }\n  });\n\n  // Get preset details\n  app.get(\"/api/presets/:name\", async (req: any, reply: any) => {\n    try {\n      const { name } = req.params;\n      const presetDir = getPresetDir(name);\n\n      if (!existsSync(presetDir)) {\n        reply.status(404).send({ error: \"Preset not found\" });\n        return;\n      }\n\n      const manifest = await readManifestFromDir(presetDir);\n      const presetFile = manifestToPresetFile(manifest);\n\n      // Return preset info, config uses the applied userValues configuration\n      return {\n        ...presetFile,\n        config: loadConfigFromManifest(manifest, presetDir),\n        userValues: manifest.userValues || {},\n      };\n    } catch (error: any) {\n      console.error(\"Failed to get preset:\", error);\n      reply.status(500).send({ error: error.message || \"Failed to get preset\" });\n    }\n  });\n\n  // Apply preset (configure sensitive information)\n  app.post(\"/api/presets/:name/apply\", async (req: any, reply: any) => {\n    try {\n      const { name } = req.params;\n      const { secrets } = req.body;\n\n      const presetDir = getPresetDir(name);\n\n      if (!existsSync(presetDir)) {\n        reply.status(404).send({ error: \"Preset not found\" });\n        return;\n      }\n\n      // Read existing manifest\n      const manifest = await readManifestFromDir(presetDir);\n\n      // Save user input to userValues (keep original config unchanged)\n      const updatedManifest: ManifestFile = { ...manifest };\n\n      // Save or update userValues\n      if (secrets && Object.keys(secrets).length > 0) {\n        updatedManifest.userValues = {\n          ...updatedManifest.userValues,\n          ...secrets,\n        };\n      }\n\n      // Save updated manifest\n      await saveManifest(name, updatedManifest);\n\n      return { success: true, message: \"Preset applied successfully\" };\n    } catch (error: any) {\n      console.error(\"Failed to apply preset:\", error);\n      reply.status(500).send({ error: error.message || \"Failed to apply preset\" });\n    }\n  });\n\n  // Delete preset\n  app.delete(\"/api/presets/:name\", async (req: any, reply: any) => {\n    try {\n      const { name } = req.params;\n      const presetDir = getPresetDir(name);\n\n      if (!existsSync(presetDir)) {\n        reply.status(404).send({ error: \"Preset not found\" });\n        return;\n      }\n\n      // Recursively delete entire directory\n      rmSync(presetDir, { recursive: true, force: true });\n\n      return { success: true, message: \"Preset deleted successfully\" };\n    } catch (error: any) {\n      console.error(\"Failed to delete preset:\", error);\n      reply.status(500).send({ error: error.message || \"Failed to delete preset\" });\n    }\n  });\n\n  // Get preset market list\n  app.get(\"/api/presets/market\", async (req: any, reply: any) => {\n    try {\n      // Use market presets function\n      const marketPresets = await getMarketPresets();\n      return { presets: marketPresets };\n    } catch (error: any) {\n      console.error(\"Failed to get market presets:\", error);\n      reply.status(500).send({ error: error.message || \"Failed to get market presets\" });\n    }\n  });\n\n  // Install preset from GitHub repository by preset name\n  app.post(\"/api/presets/install/github\", async (req: any, reply: any) => {\n    try {\n      const { presetName } = req.body;\n\n      if (!presetName) {\n        reply.status(400).send({ error: \"Preset name is required\" });\n        return;\n      }\n\n      // Check if preset is in the marketplace\n      const marketPreset = await findMarketPresetByName(presetName);\n      if (!marketPreset) {\n        reply.status(400).send({\n          error: \"Preset not found in marketplace\",\n          message: `Preset '${presetName}' is not available in the official marketplace. Please check the available presets.`\n        });\n        return;\n      }\n\n      // Get repository from market preset\n      if (!marketPreset.repo) {\n        reply.status(400).send({\n          error: \"Invalid preset data\",\n          message: `Preset '${presetName}' does not have repository information`\n        });\n        return;\n      }\n\n      // Parse GitHub repository URL\n      const githubRepoMatch = marketPreset.repo.match(/(?:github\\.com[:/]|^)([^/]+)\\/([^/\\s#]+?)(?:\\.git)?$/);\n      if (!githubRepoMatch) {\n        reply.status(400).send({ error: \"Invalid GitHub repository URL\" });\n        return;\n      }\n\n      const [, owner, repoName] = githubRepoMatch;\n\n      // Use preset name from market\n      const installedPresetName = marketPreset.name || presetName;\n\n      // Check if already installed BEFORE downloading\n      if (await isPresetInstalled(installedPresetName)) {\n        reply.status(409).send({\n          error: \"Preset already installed\",\n          message: `Preset '${installedPresetName}' is already installed. To update or reconfigure, please delete it first using the delete button.`,\n          presetName: installedPresetName\n        });\n        return;\n      }\n\n      // Download GitHub repository ZIP file\n      const downloadUrl = `https://github.com/${owner}/${repoName}/archive/refs/heads/main.zip`;\n      const tempFile = await downloadPresetToTemp(downloadUrl);\n\n      // Load preset to validate structure\n      const preset = await loadPresetFromZip(tempFile);\n\n      // Double-check if already installed (in case of race condition)\n      if (await isPresetInstalled(installedPresetName)) {\n        unlinkSync(tempFile);\n        reply.status(409).send({\n          error: \"Preset already installed\",\n          message: `Preset '${installedPresetName}' was installed while downloading. Please try again.`,\n          presetName: installedPresetName\n        });\n        return;\n      }\n\n      // Extract to target directory\n      const targetDir = getPresetDir(installedPresetName);\n      await extractPreset(tempFile, targetDir);\n\n      // Read manifest and add repo information\n      const manifest = await readManifestFromDir(targetDir);\n\n      // Add repo information to manifest from market data\n      manifest.repository = marketPreset.repo;\n      if (marketPreset.url) {\n        manifest.source = marketPreset.url;\n      }\n\n      // Save updated manifest\n      await saveManifest(installedPresetName, manifest);\n\n      // Clean up temp file\n      unlinkSync(tempFile);\n\n      return {\n        success: true,\n        presetName: installedPresetName,\n        preset: {\n          ...preset.metadata,\n          installed: true,\n        }\n      };\n    } catch (error: any) {\n      console.error(\"Failed to install preset from GitHub:\", error);\n      reply.status(500).send({ error: error.message || \"Failed to install preset from GitHub\" });\n    }\n  });\n\n  // Helper function: Load preset from ZIP\n  async function loadPresetFromZip(zipFile: string): Promise<PresetFile> {\n    const zip = new AdmZip(zipFile);\n\n    // First try to find manifest.json in root directory\n    let entry = zip.getEntry('manifest.json');\n\n    // If not in root, try to find in subdirectories (handle GitHub repo archive structure)\n    if (!entry) {\n      const entries = zip.getEntries();\n      // Find any manifest.json file\n      entry = entries.find(e => e.entryName.includes('manifest.json')) || null;\n    }\n\n    if (!entry) {\n      throw new Error('Invalid preset file: manifest.json not found');\n    }\n\n    const manifest = JSON.parse(entry.getData().toString('utf-8')) as ManifestFile;\n    return manifestToPresetFile(manifest);\n  }\n\n  return server;\n};\n"
  },
  {
    "path": "packages/server/src/types/llms-plugin.d.ts",
    "content": "import { FastifyPluginAsync } from 'fastify';\n\ndeclare module '@musistudio/llms' {\n\n  export interface CCRPluginOptions {\n    enabled?: boolean;\n    [key: string]: any;\n  }\n\n\n  export interface CCRPlugin {\n    name: string;\n    version?: string;\n    description?: string;\n    register: FastifyPluginAsync<CCRPluginOptions>;\n  }\n\n\n  export interface PluginMetadata {\n    name: string;\n    enabled: boolean;\n    options?: any;\n  }\n\n\n  export class PluginManager {\n    private plugins;\n    private pluginInstances;\n    registerPlugin(plugin: CCRPlugin, options?: any): void;\n    enablePlugin(name: string, fastify: import('fastify').FastifyInstance): Promise<void>;\n    enablePlugins(fastify: import('fastify').FastifyInstance): Promise<void>;\n    getPlugins(): PluginMetadata[];\n    getPlugin(name: string): CCRPlugin | undefined;\n    hasPlugin(name: string): boolean;\n    isPluginEnabled(name: string): boolean;\n    setPluginEnabled(name: string, enabled: boolean): void;\n    removePlugin(name: string): void;\n    clear(): void;\n  }\n\n\n  export const pluginManager: PluginManager;\n\n\n  export const tokenSpeedPlugin: CCRPlugin;\n\n\n  export class SSEParserTransform extends TransformStream<string, any> {\n    constructor();\n  }\n\n\n  export class SSESerializerTransform extends TransformStream<any, string> {\n    constructor();\n  }\n\n\n  export function rewriteStream(\n    stream: ReadableStream,\n    processor: (data: any, controller: ReadableStreamController<any>) => Promise<any>\n  ): ReadableStream;\n}\n"
  },
  {
    "path": "packages/server/src/types.d.ts",
    "content": "declare module \"@musistudio/llms\" {\n  import { FastifyInstance } from \"fastify\";\n  import { FastifyBaseLogger } from \"fastify\";\n\n  export interface ServerConfig {\n    jsonPath?: string;\n    initialConfig?: any;\n    logger?: any;\n  }\n\n  /**\n   * Plugin configuration from config file\n   */\n  export interface PluginConfig {\n    name: string;\n    enabled?: boolean;\n    options?: Record<string, any>;\n  }\n\n  export interface Server {\n    app: FastifyInstance;\n    logger: FastifyBaseLogger;\n    start(): Promise<void>;\n  }\n\n  const Server: {\n    new (config: ServerConfig): Server;\n  };\n\n  export default Server;\n\n  // Export cache\n  export interface Usage {\n    input_tokens: number;\n    output_tokens: number;\n  }\n\n  export const sessionUsageCache: any;\n\n  // Export router\n  export interface RouterContext {\n    configService: any;\n    event?: any;\n  }\n\n  export const router: (req: any, res: any, context: RouterContext) => Promise<void>;\n\n  // Export utilities\n  export const calculateTokenCount: (messages: any[], system: any, tools: any[]) => number;\n  export const searchProjectBySession: (sessionId: string) => Promise<string | null>;\n\n  // Export services\n  export class ConfigService {\n    constructor(options?: any);\n    get<T = any>(key: string): T | undefined;\n    get<T = any>(key: string, defaultValue: T): T;\n    getAll(): any;\n    has(key: string): boolean;\n    set(key: string, value: any): void;\n    reload(): void;\n  }\n\n  export class ProviderService {\n    constructor(configService: any, transformerService: any, logger: any);\n  }\n\n  export class TransformerService {\n    constructor(configService: any, logger: any);\n    initialize(): Promise<void>;\n  }\n\n  // Tokenizer types\n  export type TokenizerType = 'tiktoken' | 'huggingface' | 'api';\n  export type ApiRequestFormat = 'standard' | 'openai' | 'anthropic' | 'custom';\n\n  export interface TokenizerConfig {\n    type: TokenizerType;\n    encoding?: string;\n    model?: string;\n    url?: string;\n    apiKey?: string;\n    requestFormat?: ApiRequestFormat;\n    responseField?: string;\n    headers?: Record<string, string>;\n    fallback?: TokenizerType;\n  }\n\n  export interface TokenizeRequest {\n    messages: Array<{\n      role: string;\n      content: string | Array<{\n        type: string;\n        text?: string;\n        input?: any;\n        content?: string | any;\n      }>;\n    }>;\n    system?: string | Array<{\n      type: string;\n      text?: string | string[];\n    }>;\n    tools?: Array<{\n      name: string;\n      description?: string;\n      input_schema: object;\n    }>;\n  }\n\n  export interface TokenizerResult {\n    tokenCount: number;\n    tokenizerUsed: string;\n    cached: boolean;\n  }\n\n  export class TokenizerService {\n    countTokens(request: TokenizeRequest, config?: TokenizerConfig): Promise<TokenizerResult>;\n    getTokenizerConfigForModel(providerName: string, modelName: string): TokenizerConfig | undefined;\n    clearCache(): void;\n    dispose(): void;\n  }\n\n  // Token speed statistics types\n  export interface TokenStats {\n    requestId: string;\n    startTime: number;\n    firstTokenTime?: number;\n    lastTokenTime: number;\n    tokenCount: number;\n    tokensPerSecond: number;\n    timeToFirstToken?: number;\n    contentBlocks: {\n      index: number;\n      tokenCount: number;\n      speed: number;\n    }[];\n  }\n\n  export function getTokenSpeedStats(): {\n    current: TokenStats | null;\n    global: {\n      totalRequests: number;\n      totalTokens: number;\n      totalTime: number;\n      avgTokensPerSecond: number;\n      minTokensPerSecond: number;\n      maxTokensPerSecond: number;\n      avgTimeToFirstToken: number;\n      allSpeeds: number[];\n    };\n    lastUpdate: number;\n  };\n\n  export function getGlobalTokenSpeedStats(): {\n    totalRequests: number;\n    totalTokens: number;\n    totalTime: number;\n    avgTokensPerSecond: number;\n    minTokensPerSecond: number;\n    maxTokensPerSecond: number;\n    avgTimeToFirstToken: number;\n    allSpeeds: number[];\n  };\n}\n"
  },
  {
    "path": "packages/server/src/utils/SSEParser.transform.ts",
    "content": "export class SSEParserTransform extends TransformStream<string, any> {\n    private buffer = '';\n    private currentEvent: Record<string, any> = {};\n\n    constructor() {\n        super({\n            transform: (chunk: string, controller) => {\n                this.buffer += chunk;\n                const lines = this.buffer.split('\\n');\n\n                // Keep last line (may be incomplete)\n                this.buffer = lines.pop() || '';\n\n                for (const line of lines) {\n                    const event = this.processLine(line);\n                    if (event) {\n                        controller.enqueue(event);\n                    }\n                }\n            },\n            flush: (controller) => {\n                // Process remaining content in buffer\n                if (this.buffer.trim()) {\n                    const events: any[] = [];\n                    this.processLine(this.buffer.trim(), events);\n                    events.forEach(event => controller.enqueue(event));\n                }\n\n                // Push last event (if any)\n                if (Object.keys(this.currentEvent).length > 0) {\n                    controller.enqueue(this.currentEvent);\n                }\n            }\n        });\n    }\n\n    private processLine(line: string, events?: any[]): any | null {\n        if (!line.trim()) {\n            if (Object.keys(this.currentEvent).length > 0) {\n                const event = { ...this.currentEvent };\n                this.currentEvent = {};\n                if (events) {\n                    events.push(event);\n                    return null;\n                }\n                return event;\n            }\n            return null;\n        }\n\n        if (line.startsWith('event:')) {\n            this.currentEvent.event = line.slice(6).trim();\n        } else if (line.startsWith('data:')) {\n            const data = line.slice(5).trim();\n            if (data === '[DONE]') {\n                this.currentEvent.data = { type: 'done' };\n            } else {\n                try {\n                    this.currentEvent.data = JSON.parse(data);\n                } catch (e) {\n                    this.currentEvent.data = { raw: data, error: 'JSON parse failed' };\n                }\n            }\n        } else if (line.startsWith('id:')) {\n            this.currentEvent.id = line.slice(3).trim();\n        } else if (line.startsWith('retry:')) {\n            this.currentEvent.retry = parseInt(line.slice(6).trim());\n        }\n        return null;\n    }\n}\n"
  },
  {
    "path": "packages/server/src/utils/SSESerializer.transform.ts",
    "content": "export class SSESerializerTransform extends TransformStream<any, string> {\n    constructor() {\n        super({\n            transform: (event, controller) => {\n                let output = '';\n\n                if (event.event) {\n                    output += `event: ${event.event}\\n`;\n                }\n                if (event.id) {\n                    output += `id: ${event.id}\\n`;\n                }\n                if (event.retry) {\n                    output += `retry: ${event.retry}\\n`;\n                }\n                if (event.data) {\n                    if (event.data.type === 'done') {\n                        output += 'data: [DONE]\\n';\n                    } else {\n                        output += `data: ${JSON.stringify(event.data)}\\n`;\n                    }\n                }\n\n                output += '\\n';\n                controller.enqueue(output);\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "packages/server/src/utils/index.ts",
    "content": "import fs from \"node:fs/promises\";\nimport readline from \"node:readline\";\nimport JSON5 from \"json5\";\nimport path from \"node:path\";\nimport {\n  CONFIG_FILE,\n  DEFAULT_CONFIG,\n  HOME_DIR,\n  PLUGINS_DIR,\n} from \"@CCR/shared\";\n\n// Function to interpolate environment variables in config values\nconst interpolateEnvVars = (obj: any): any => {\n  if (typeof obj === \"string\") {\n    // Replace $VAR_NAME or ${VAR_NAME} with environment variable values\n    return obj.replace(/\\$\\{([^}]+)\\}|\\$([A-Z_][A-Z0-9_]*)/g, (match, braced, unbraced) => {\n      const varName = braced || unbraced;\n      return process.env[varName] || match; // Keep original if env var doesn't exist\n    });\n  } else if (Array.isArray(obj)) {\n    return obj.map(interpolateEnvVars);\n  } else if (obj !== null && typeof obj === \"object\") {\n    const result: any = {};\n    for (const [key, value] of Object.entries(obj)) {\n      result[key] = interpolateEnvVars(value);\n    }\n    return result;\n  }\n  return obj;\n};\n\nconst ensureDir = async (dir_path: string) => {\n  try {\n    await fs.access(dir_path);\n  } catch {\n    await fs.mkdir(dir_path, { recursive: true });\n  }\n};\n\nexport const initDir = async () => {\n  await ensureDir(HOME_DIR);\n  await ensureDir(PLUGINS_DIR);\n  await ensureDir(path.join(HOME_DIR, \"logs\"));\n};\n\nconst createReadline = () => {\n  return readline.createInterface({\n    input: process.stdin,\n    output: process.stdout,\n  });\n};\n\nconst question = (query: string): Promise<string> => {\n  return new Promise((resolve) => {\n    const rl = createReadline();\n    rl.question(query, (answer) => {\n      rl.close();\n      resolve(answer);\n    });\n  });\n};\n\nconst confirm = async (query: string): Promise<boolean> => {\n  const answer = await question(query);\n  return answer.toLowerCase() !== \"n\";\n};\n\nexport const readConfigFile = async () => {\n  try {\n    const config = await fs.readFile(CONFIG_FILE, \"utf-8\");\n    try {\n      // Try to parse with JSON5 first (which also supports standard JSON)\n      const parsedConfig = JSON5.parse(config);\n      // Interpolate environment variables in the parsed config\n      return interpolateEnvVars(parsedConfig);\n    } catch (parseError) {\n      console.error(`Failed to parse config file at ${CONFIG_FILE}`);\n      console.error(\"Error details:\", (parseError as Error).message);\n      console.error(\"Please check your config file syntax.\");\n      process.exit(1);\n    }\n  } catch (readError: any) {\n    if (readError.code === \"ENOENT\") {\n      // Config file doesn't exist, prompt user for initial setup\n      try {\n        // Initialize directories\n        await initDir();\n\n        // Backup existing config file if it exists\n        const backupPath = await backupConfigFile();\n        if (backupPath) {\n          console.log(\n              `Backed up existing configuration file to ${backupPath}`\n          );\n        }\n        const config = {\n          PORT: 3456,\n          Providers: [],\n          Router: {},\n        }\n        // Create a minimal default config file\n        await writeConfigFile(config);\n        console.log(\n            \"Created minimal default configuration file at ~/.claude-code-router/config.json\"\n        );\n        console.log(\n            \"Please edit this file with your actual configuration.\"\n        );\n        return config\n      } catch (error: any) {\n        console.error(\n            \"Failed to create default configuration:\",\n            error.message\n        );\n        process.exit(1);\n      }\n    } else {\n      console.error(`Failed to read config file at ${CONFIG_FILE}`);\n      console.error(\"Error details:\", readError.message);\n      process.exit(1);\n    }\n  }\n};\n\nexport const backupConfigFile = async () => {\n  try {\n    if (await fs.access(CONFIG_FILE).then(() => true).catch(() => false)) {\n      const timestamp = new Date().toISOString().replace(/[:.]/g, '-');\n      const backupPath = `${CONFIG_FILE}.${timestamp}.bak`;\n      await fs.copyFile(CONFIG_FILE, backupPath);\n\n      // Clean up old backups, keeping only the 3 most recent\n      try {\n        const configDir = path.dirname(CONFIG_FILE);\n        const configFileName = path.basename(CONFIG_FILE);\n        const files = await fs.readdir(configDir);\n\n        // Find all backup files for this config\n        const backupFiles = files\n          .filter(file => file.startsWith(configFileName) && file.endsWith('.bak'))\n          .sort()\n          .reverse(); // Sort in descending order (newest first)\n\n        // Delete all but the 3 most recent backups\n        if (backupFiles.length > 3) {\n          for (let i = 3; i < backupFiles.length; i++) {\n            const oldBackupPath = path.join(configDir, backupFiles[i]);\n            await fs.unlink(oldBackupPath);\n          }\n        }\n      } catch (cleanupError) {\n        console.warn(\"Failed to clean up old backups:\", cleanupError);\n      }\n\n      return backupPath;\n    }\n  } catch (error) {\n    console.error(\"Failed to backup config file:\", error);\n  }\n  return null;\n};\n\nexport const writeConfigFile = async (config: any) => {\n  await ensureDir(HOME_DIR);\n  const configWithComment = `${JSON.stringify(config, null, 2)}`;\n  await fs.writeFile(CONFIG_FILE, configWithComment);\n};\n\nexport const initConfig = async () => {\n  const config = await readConfigFile();\n  Object.assign(process.env, config);\n  return config;\n};\n"
  },
  {
    "path": "packages/server/src/utils/rewriteStream.ts",
    "content": "/**rewriteStream\n * Read source readablestream and return a new readablestream, processor processes source data and pushes returned new value to new stream, no push if no return value\n * @param stream\n * @param processor\n */\nexport const rewriteStream = (stream: ReadableStream, processor: (data: any, controller: ReadableStreamController<any>) => Promise<any>): ReadableStream => {\n  const reader = stream.getReader()\n\n  return new ReadableStream({\n    async start(controller) {\n      try {\n        while (true) {\n          const { done, value } = await reader.read()\n          if (done) {\n            controller.close()\n            break\n          }\n\n          const processed = await processor(value, controller)\n          if (processed !== undefined) {\n            controller.enqueue(processed)\n          }\n        }\n      } catch (error) {\n        controller.error(error)\n      } finally {\n        reader.releaseLock()\n      }\n    }\n  })\n}\n"
  },
  {
    "path": "packages/server/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.base.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"./dist\",\n    \"rootDir\": \"./src\",\n    \"baseUrl\": \"./src\"\n  },\n  \"include\": [\"src/**/*.ts\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/shared/package.json",
    "content": "{\n  \"name\": \"@CCR/shared\",\n  \"version\": \"2.0.0\",\n  \"description\": \"Shared utilities and constants for Claude Code Router\",\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"scripts\": {\n    \"build\": \"node ../../scripts/build-shared.js\"\n  },\n  \"keywords\": [\n    \"claude\",\n    \"code\",\n    \"router\",\n    \"shared\"\n  ],\n  \"author\": \"musistudio\",\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"adm-zip\": \"^0.5.16\",\n    \"archiver\": \"^7.0.1\",\n    \"json5\": \"^2.2.3\"\n  },\n  \"devDependencies\": {\n    \"@types/adm-zip\": \"^0.5.7\",\n    \"@types/archiver\": \"^7.0.0\",\n    \"@types/node\": \"^24.0.15\",\n    \"esbuild\": \"^0.25.1\",\n    \"typescript\": \"^5.8.2\"\n  }\n}\n"
  },
  {
    "path": "packages/shared/src/constants.ts",
    "content": "import path from \"node:path\";\nimport os from \"node:os\";\n\nexport const HOME_DIR = path.join(os.homedir(), \".claude-code-router\");\n\nexport const CONFIG_FILE = path.join(HOME_DIR, \"config.json\");\n\nexport const PLUGINS_DIR = path.join(HOME_DIR, \"plugins\");\n\nexport const PRESETS_DIR = path.join(HOME_DIR, \"presets\");\n\nexport const PID_FILE = path.join(HOME_DIR, '.claude-code-router.pid');\n\nexport const REFERENCE_COUNT_FILE = path.join(os.tmpdir(), \"claude-code-reference-count.txt\");\n\n// Claude projects directory\nexport const CLAUDE_PROJECTS_DIR = path.join(os.homedir(), \".claude\", \"projects\");\n\n\nexport interface DefaultConfig {\n  LOG: boolean;\n  OPENAI_API_KEY: string;\n  OPENAI_BASE_URL: string;\n  OPENAI_MODEL: string;\n}\n\nexport const DEFAULT_CONFIG: DefaultConfig = {\n  LOG: false,\n  OPENAI_API_KEY: \"\",\n  OPENAI_BASE_URL: \"\",\n  OPENAI_MODEL: \"\",\n};\n"
  },
  {
    "path": "packages/shared/src/index.ts",
    "content": "export * from \"./constants\";\n\n// Export preset-related functionality\nexport * from './preset/types';\nexport * from './preset/sensitiveFields';\nexport * from './preset/merge';\nexport * from './preset/install';\nexport * from './preset/export';\nexport * from './preset/readPreset';\nexport * from './preset/schema';\nexport * from './preset/marketplace';\n\n"
  },
  {
    "path": "packages/shared/src/preset/export.ts",
    "content": "/**\n * Preset export core functionality\n * Note: This module does not contain CLI interaction logic, interaction logic is provided by the caller\n */\n\nimport * as fs from 'fs/promises';\nimport * as path from 'path';\nimport { sanitizeConfig } from './sensitiveFields';\nimport { PresetMetadata, ManifestFile } from './types';\nimport { HOME_DIR } from '../constants';\n\n/**\n * Export options\n */\nexport interface ExportOptions {\n  includeSensitive?: boolean;\n  description?: string;\n  author?: string;\n  tags?: string;\n}\n\n/**\n * Export result\n */\nexport interface ExportResult {\n  presetDir: string;\n  sanitizedConfig: any;\n  metadata: PresetMetadata;\n  sanitizedCount: number;\n}\n\n/**\n * Create manifest object\n * @param presetName Preset name\n * @param config Configuration object\n * @param sanitizedConfig Sanitized configuration\n * @param options Export options\n */\nexport function createManifest(\n  presetName: string,\n  config: any,\n  sanitizedConfig: any,\n  options: ExportOptions\n): ManifestFile {\n  const metadata: PresetMetadata = {\n    name: presetName,\n    version: '1.0.0',\n    description: options.description,\n    author: options.author,\n    keywords: options.tags ? options.tags.split(',').map(t => t.trim()) : undefined,\n  };\n\n  return {\n    ...metadata,\n    ...sanitizedConfig,\n  };\n}\n\n/**\n * Export preset configuration\n * @param presetName Preset name\n * @param config Current configuration\n * @param options Export options\n * @returns Export result\n */\nexport async function exportPreset(\n  presetName: string,\n  config: any,\n  options: ExportOptions = {}\n): Promise<ExportResult> {\n  // 1. Collect metadata\n  const metadata: PresetMetadata = {\n    name: presetName,\n    version: '1.0.0',\n    description: options.description,\n    author: options.author,\n    keywords: options.tags ? options.tags.split(',').map(t => t.trim()) : undefined,\n  };\n\n  // 2. Sanitize configuration\n  const { sanitizedConfig, sanitizedCount } = await sanitizeConfig(config);\n\n  // 3. Generate manifest.json (flattened structure)\n  const manifest: ManifestFile = {\n    ...metadata,\n    ...sanitizedConfig,\n  };\n\n  // 4. Create preset directory\n  const presetDir = path.join(HOME_DIR, 'presets', presetName);\n\n  // Check if preset directory already exists\n  try {\n    await fs.access(presetDir);\n    throw new Error(`Preset directory already exists: ${presetName}`);\n  } catch (error: any) {\n    if (error.code !== 'ENOENT') {\n      throw error;\n    }\n  }\n\n  // Create preset directory\n  await fs.mkdir(presetDir, { recursive: true });\n\n  // 5. Write manifest.json to preset directory\n  const manifestPath = path.join(presetDir, 'manifest.json');\n  await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8');\n\n  return {\n    presetDir,\n    sanitizedConfig,\n    metadata,\n    sanitizedCount,\n  };\n}\n"
  },
  {
    "path": "packages/shared/src/preset/install.ts",
    "content": "/**\n * Core preset installation functionality\n * Note: This module does not contain CLI interaction logic, interaction logic is provided by the caller\n */\n\nimport * as fs from 'fs/promises';\nimport * as path from 'path';\nimport JSON5 from 'json5';\nimport AdmZip from 'adm-zip';\nimport { PresetFile, ManifestFile, PresetInfo, PresetMetadata } from './types';\nimport { HOME_DIR, PRESETS_DIR } from '../constants';\nimport { loadConfigFromManifest } from './schema';\n\n/**\n * Validate if preset name is safe (prevent path traversal attacks)\n * @param presetName Preset name\n */\nfunction validatePresetName(presetName: string): void {\n  if (!presetName || presetName.trim() === '') {\n    throw new Error('Preset name cannot be empty');\n  }\n\n  // Reject names containing path traversal sequences\n  if (presetName.includes('..') || presetName.includes('/') || presetName.includes('\\\\')) {\n    throw new Error('Invalid preset name: path traversal detected');\n  }\n\n  // Reject absolute paths\n  if (path.isAbsolute(presetName)) {\n    throw new Error('Invalid preset name: absolute path not allowed');\n  }\n}\n\n/**\n * Get the full path of the preset directory\n * @param presetName Preset name\n */\nexport function getPresetDir(presetName: string): string {\n  validatePresetName(presetName);\n  return path.join(HOME_DIR, 'presets', presetName);\n}\n\n/**\n * Get temporary directory path\n */\nexport function getTempDir(): string {\n  return path.join(HOME_DIR, 'temp');\n}\n\n/**\n * Validate and normalize file path, ensuring it's within the target directory\n * @param targetDir Target directory\n * @param entryPath ZIP entry path\n * @returns Safe absolute path\n */\nfunction validateAndResolvePath(targetDir: string, entryPath: string): string {\n  const resolvedTargetDir = path.resolve(targetDir);\n  const resolvedPath = path.resolve(targetDir, entryPath);\n\n  // Verify that the resolved path is within the target directory\n  if (!resolvedPath.startsWith(resolvedTargetDir)) {\n    throw new Error(`Path traversal detected: ${entryPath}`);\n  }\n\n  return resolvedPath;\n}\n\n/**\n * Extract preset file to target directory\n * @param sourceZip Source ZIP file path\n * @param targetDir Target directory\n */\nexport async function extractPreset(sourceZip: string, targetDir: string): Promise<void> {\n  // Check if target directory already exists\n  try {\n    await fs.access(targetDir);\n    throw new Error(`Preset directory already exists: ${path.basename(targetDir)}`);\n  } catch (error: any) {\n    if (error.code !== 'ENOENT') {\n      throw error;\n    }\n    // ENOENT means directory does not exist, can continue\n  }\n\n  // Create target directory\n  await fs.mkdir(targetDir, { recursive: true });\n\n  // Extract files\n  const zip = new AdmZip(sourceZip);\n  const entries = zip.getEntries();\n\n  // Detect if there's a single root directory (GitHub ZIP files usually have this characteristic)\n  if (entries.length > 0) {\n    // Get all top-level directories\n    const rootDirs = new Set<string>();\n    for (const entry of entries) {\n      const parts = entry.entryName.split('/');\n      if (parts.length > 1) {\n        rootDirs.add(parts[0]);\n      }\n    }\n\n    // If there's only one root directory, remove it\n    if (rootDirs.size === 1) {\n      const singleRoot = Array.from(rootDirs)[0];\n\n      // Check if manifest.json is in root directory\n      const hasManifestInRoot = entries.some(e =>\n        e.entryName === 'manifest.json' || e.entryName.startsWith(`${singleRoot}/manifest.json`)\n      );\n\n      if (hasManifestInRoot) {\n        // Extract all files from the root directory\n        for (const entry of entries) {\n          if (entry.isDirectory) {\n            continue;\n          }\n\n          // Remove root directory prefix\n          let newPath = entry.entryName;\n          if (newPath.startsWith(`${singleRoot}/`)) {\n            newPath = newPath.substring(singleRoot.length + 1);\n          }\n\n          // Skip root directory itself\n          if (newPath === '' || newPath === singleRoot) {\n            continue;\n          }\n\n          // Validate path safety and extract file\n          const targetPath = validateAndResolvePath(targetDir, newPath);\n          await fs.mkdir(path.dirname(targetPath), { recursive: true });\n          await fs.writeFile(targetPath, entry.getData());\n        }\n\n        return;\n      }\n    }\n  }\n\n  // If there's no single root directory, validate and extract files one by one\n  for (const entry of entries) {\n    if (entry.isDirectory) {\n      continue;\n    }\n\n    // Validate path safety\n    const targetPath = validateAndResolvePath(targetDir, entry.entryName);\n    await fs.mkdir(path.dirname(targetPath), { recursive: true });\n    await fs.writeFile(targetPath, entry.getData());\n  }\n}\n\n/**\n * Read manifest from extracted directory\n * @param presetDir Preset directory path\n */\nexport async function readManifestFromDir(presetDir: string): Promise<ManifestFile> {\n  const manifestPath = path.join(presetDir, 'manifest.json');\n  const content = await fs.readFile(manifestPath, 'utf-8');\n  return JSON5.parse(content) as ManifestFile;\n}\n\n/**\n * List of known metadata fields\n */\nconst METADATA_FIELDS = [\n  'name',\n  'version',\n  'description',\n  'author',\n  'homepage',\n  'repository',\n  'license',\n  'keywords',\n  'ccrVersion',\n  'source',\n  'sourceType',\n  'checksum',\n];\n\n/**\n * Dynamic configuration system field list\n */\nconst DYNAMIC_CONFIG_FIELDS = [\n  'schema',\n  'template',\n  'configMappings',\n];\n\n/**\n * Convert manifest to PresetFile format\n * Correctly separate metadata, config, and dynamic configuration system fields\n */\nexport function manifestToPresetFile(manifest: ManifestFile): PresetFile {\n  const metadata: any = {};\n  const config: any = {};\n  const dynamicConfig: any = {};\n\n  // Categorize all fields\n  for (const [key, value] of Object.entries(manifest)) {\n    if (METADATA_FIELDS.includes(key)) {\n      // metadata fields\n      metadata[key] = value;\n    } else if (DYNAMIC_CONFIG_FIELDS.includes(key)) {\n      // dynamic configuration system fields\n      dynamicConfig[key] = value;\n    } else {\n      // configuration fields\n      config[key] = value;\n    }\n  }\n\n  return {\n    metadata: Object.keys(metadata).length > 0 ? metadata : undefined,\n    config,\n    schema: dynamicConfig.schema,\n    template: dynamicConfig.template,\n    configMappings: dynamicConfig.configMappings,\n  };\n}\n\n/**\n * Download preset file to temporary location\n * @param url Download URL\n * @returns Temporary file path\n */\nexport async function downloadPresetToTemp(url: string): Promise<string> {\n  const response = await fetch(url);\n  if (!response.ok) {\n    throw new Error(`Failed to download preset: ${response.statusText}`);\n  }\n  const buffer = await response.arrayBuffer();\n\n  // Create temporary file\n  const tempDir = getTempDir();\n  await fs.mkdir(tempDir, { recursive: true });\n\n  const tempFile = path.join(tempDir, `preset-${Date.now()}.zip`);\n  await fs.writeFile(tempFile, Buffer.from(buffer));\n\n  return tempFile;\n}\n\n/**\n * Load preset file\n * @param source Preset source (preset name or directory path)\n */\nexport async function loadPreset(source: string): Promise<PresetFile> {\n  // Check if it's absolute or relative path (contains / or \\)\n  if (source.includes('/') || source.includes('\\\\')) {\n    // Directory path - read manifest from directory\n    const manifestPath = path.join(source, 'manifest.json');\n    const content = await fs.readFile(manifestPath, 'utf-8');\n    const manifest = JSON5.parse(content) as ManifestFile;\n    return manifestToPresetFile(manifest);\n  }\n\n  // Otherwise treat as preset name (read from presets directory)\n  const presetDir = getPresetDir(source);\n  const manifest = await readManifestFromDir(presetDir);\n  return manifestToPresetFile(manifest);\n}\n\n/**\n * Validate preset file\n */\nexport async function validatePreset(preset: PresetFile): Promise<{\n  valid: boolean;\n  errors: string[];\n  warnings: string[];\n}> {\n  const errors: string[] = [];\n  const warnings: string[] = [];\n\n  // Validate metadata\n  if (!preset.metadata) {\n    warnings.push('Missing metadata section');\n  } else {\n    if (!preset.metadata.name) {\n      errors.push('Missing preset name in metadata');\n    }\n    if (!preset.metadata.version) {\n      warnings.push('Missing version in metadata');\n    }\n  }\n\n  // Validate configuration section\n  if (!preset.config) {\n    errors.push('Missing config section');\n  }\n\n  // Validate Providers\n  if (preset.config.Providers) {\n    for (const provider of preset.config.Providers) {\n      if (!provider.name) {\n        errors.push('Provider missing name field');\n      }\n      if (!provider.api_base_url) {\n        errors.push(`Provider \"${provider.name}\" missing api_base_url`);\n      }\n      if (!provider.models || provider.models.length === 0) {\n        warnings.push(`Provider \"${provider.name}\" has no models`);\n      }\n    }\n  }\n\n  return {\n    valid: errors.length === 0,\n    errors,\n    warnings,\n  };\n}\n\n/**\n * Extract metadata fields from manifest\n * @param manifest Manifest object\n * @returns Metadata object\n */\nexport function extractMetadata(manifest: ManifestFile): PresetMetadata {\n  const metadata: PresetMetadata = {\n    name: manifest.name,\n    version: manifest.version,\n  };\n\n  // Optional fields\n  if (manifest.description !== undefined) metadata.description = manifest.description;\n  if (manifest.author !== undefined) metadata.author = manifest.author;\n  if (manifest.homepage !== undefined) metadata.homepage = manifest.homepage;\n  if (manifest.repository !== undefined) metadata.repository = manifest.repository;\n  if (manifest.license !== undefined) metadata.license = manifest.license;\n  if (manifest.keywords !== undefined) metadata.keywords = manifest.keywords;\n  if (manifest.ccrVersion !== undefined) metadata.ccrVersion = manifest.ccrVersion;\n  if (manifest.source !== undefined) metadata.source = manifest.source;\n  if (manifest.sourceType !== undefined) metadata.sourceType = manifest.sourceType;\n  if (manifest.checksum !== undefined) metadata.checksum = manifest.checksum;\n\n  return metadata;\n}\n\n/**\n * Save manifest to preset directory\n * @param presetName Preset name\n * @param manifest Manifest object\n */\nexport async function saveManifest(presetName: string, manifest: ManifestFile): Promise<void> {\n  const presetDir = getPresetDir(presetName);\n  const manifestPath = path.join(presetDir, 'manifest.json');\n  await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8');\n}\n\n/**\n * Check if preset is already installed\n * @param presetName Preset name\n */\nexport async function isPresetInstalled(presetName: string): Promise<boolean> {\n  const presetDir = getPresetDir(presetName);\n  try {\n    await fs.access(presetDir);\n    return true;\n  } catch {\n    return false;\n  }\n}\n\n/**\n * List all installed presets\n * @returns Array of PresetInfo\n */\nexport async function listPresets(): Promise<PresetInfo[]> {\n  const presetsDir = PRESETS_DIR;\n  const presets: PresetInfo[] = [];\n\n  try {\n    await fs.access(presetsDir);\n  } catch {\n    return presets;\n  }\n\n  // Read all subdirectories in the directory\n  const entries = await fs.readdir(presetsDir, { withFileTypes: true });\n\n  for (const entry of entries) {\n    if (entry.isDirectory()) {\n      const presetName = entry.name;\n      const presetDir = path.join(presetsDir, presetName);\n      const manifestPath = path.join(presetDir, 'manifest.json');\n\n      try {\n        // Check if manifest.json exists\n        await fs.access(manifestPath);\n\n        // Read manifest.json\n        const content = await fs.readFile(manifestPath, 'utf-8');\n        const manifest = JSON5.parse(content) as ManifestFile;\n\n        // Get directory creation time\n        const stats = await fs.stat(presetDir);\n\n        presets.push({\n          name: manifest.name || presetName,\n          version: manifest.version,\n          description: manifest.description,\n          author: manifest.author,\n          config: loadConfigFromManifest(manifest, presetDir),\n        });\n      } catch {\n        // Ignore invalid preset directories (no manifest.json or read failed)\n        // Can choose to skip or add to list marked as error\n        continue;\n      }\n    }\n  }\n\n  return presets;\n}\n"
  },
  {
    "path": "packages/shared/src/preset/marketplace.ts",
    "content": "/**\n * Preset marketplace management\n * Fetches preset market data directly from remote without caching\n */\n\nimport { PresetIndexEntry } from './types';\n\n// Preset market URL\nconst MARKET_URL = 'https://pub-0dc3e1677e894f07bbea11b17a29e032.r2.dev/presets.json';\n\n/**\n * Fetch preset market data from remote URL\n */\nasync function fetchMarketData(): Promise<PresetIndexEntry[]> {\n  const response = await fetch(MARKET_URL);\n\n  if (!response.ok) {\n    throw new Error(`Failed to fetch preset market: ${response.status} ${response.statusText}`);\n  }\n\n  const data = await response.json() as PresetIndexEntry[];\n  return data;\n}\n\n/**\n * Get preset market data (always fetches from remote)\n * @returns Array of preset market entries\n */\nexport async function getMarketPresets(): Promise<PresetIndexEntry[]> {\n  return await fetchMarketData();\n}\n\n/**\n * Find a preset in the market by preset name (id or name field)\n * @param presetName Preset name to search for\n * @returns Preset entry if found, null otherwise\n */\nexport async function findMarketPresetByName(presetName: string): Promise<PresetIndexEntry | null> {\n  const marketPresets = await getMarketPresets();\n\n  // First try exact match by id\n  let preset = marketPresets.find(p => p.id === presetName);\n\n  // If not found, try exact match by name\n  if (!preset) {\n    preset = marketPresets.find(p => p.name === presetName);\n  }\n\n  // If still not found, try case-insensitive match by name\n  if (!preset) {\n    const lowerName = presetName.toLowerCase();\n    preset = marketPresets.find(p => p.name.toLowerCase() === lowerName);\n  }\n\n  return preset || null;\n}\n"
  },
  {
    "path": "packages/shared/src/preset/merge.ts",
    "content": "/**\n * Configuration merge strategies\n */\n\nimport { MergeStrategy, ProviderConfig, RouterConfig, TransformerConfig } from './types';\n\n/**\n * Merge Provider configuration\n * Overwrite if provider exists, otherwise add\n */\nfunction mergeProviders(\n  existing: ProviderConfig[],\n  incoming: ProviderConfig[]\n): ProviderConfig[] {\n  const result = [...existing];\n  const existingNames = new Map(existing.map(p => [p.name, result.findIndex(x => x.name === p.name)]));\n\n  for (const provider of incoming) {\n    const existingIndex = existingNames.get(provider.name);\n    if (existingIndex !== undefined) {\n      // Provider exists, overwrite directly\n      result[existingIndex] = provider;\n    } else {\n      // New provider, add directly\n      result.push(provider);\n    }\n  }\n\n  return result;\n}\n\n/**\n * Merge Router configuration\n */\nasync function mergeRouter(\n  existing: RouterConfig,\n  incoming: RouterConfig,\n  strategy: MergeStrategy,\n  onRouterConflict?: (key: string, existingValue: any, newValue: any) => Promise<boolean>\n): Promise<RouterConfig> {\n  const result = { ...existing };\n\n  for (const [key, value] of Object.entries(incoming)) {\n    if (value === undefined || value === null) {\n      continue;\n    }\n\n    const existingValue = result[key];\n\n    if (existingValue === undefined || existingValue === null) {\n      // No such routing rule in existing config, add directly\n      result[key] = value;\n    } else {\n      // Conflict exists\n      if (strategy === MergeStrategy.ASK && onRouterConflict) {\n        const shouldOverwrite = await onRouterConflict(key, existingValue, value);\n        if (shouldOverwrite) {\n          result[key] = value;\n        }\n      } else if (strategy === MergeStrategy.OVERWRITE) {\n        result[key] = value;\n      } else if (strategy === MergeStrategy.MERGE) {\n        // For Router, merge strategy equals skip, keep existing value\n        // Or can ask user\n      }\n      // skip strategy: keep existing value, do nothing\n    }\n  }\n\n  return result;\n}\n\n/**\n * Merge Transformer configuration\n */\nasync function mergeTransformers(\n  existing: TransformerConfig[],\n  incoming: TransformerConfig[],\n  strategy: MergeStrategy,\n  onTransformerConflict?: (transformerPath: string) => Promise<'keep' | 'overwrite' | 'skip'>\n): Promise<TransformerConfig[]> {\n  if (!existing || existing.length === 0) {\n    return incoming;\n  }\n\n  if (!incoming || incoming.length === 0) {\n    return existing;\n  }\n\n  // Transformer merge logic: match by path\n  const result = [...existing];\n  const existingPaths = new Set(existing.map(t => t.path));\n\n  for (const transformer of incoming) {\n    if (!transformer.path) {\n      // Transformer without path, add directly\n      result.push(transformer);\n      continue;\n    }\n\n    if (existingPaths.has(transformer.path)) {\n      // Transformer with same path already exists\n      if (strategy === MergeStrategy.ASK && onTransformerConflict) {\n        const action = await onTransformerConflict(transformer.path);\n        if (action === 'overwrite') {\n          const index = result.findIndex(t => t.path === transformer.path);\n          result[index] = transformer;\n        }\n        // keep and skip do nothing\n      } else if (strategy === MergeStrategy.OVERWRITE) {\n        const index = result.findIndex(t => t.path === transformer.path);\n        result[index] = transformer;\n      }\n      // merge and skip strategies: keep existing\n    } else {\n      // New transformer, add directly\n      result.push(transformer);\n    }\n  }\n\n  return result;\n}\n\n/**\n * Merge other top-level configurations\n */\nasync function mergeOtherConfig(\n  existing: any,\n  incoming: any,\n  strategy: MergeStrategy,\n  onConfigConflict?: (key: string) => Promise<boolean>,\n  excludeKeys: string[] = ['Providers', 'Router', 'transformers']\n): Promise<any> {\n  const result = { ...existing };\n\n  for (const [key, value] of Object.entries(incoming)) {\n    if (excludeKeys.includes(key)) {\n      continue;\n    }\n\n    if (value === undefined || value === null) {\n      continue;\n    }\n\n    const existingValue = result[key];\n\n    if (existingValue === undefined || existingValue === null) {\n      // No such field in existing config, add directly\n      result[key] = value;\n    } else {\n      // Conflict exists\n      if (strategy === MergeStrategy.ASK && onConfigConflict) {\n        const shouldOverwrite = await onConfigConflict(key);\n        if (shouldOverwrite) {\n          result[key] = value;\n        }\n      } else if (strategy === MergeStrategy.OVERWRITE) {\n        result[key] = value;\n      }\n      // merge and skip strategies: keep existing值\n    }\n  }\n\n  return result;\n}\n\n/**\n * Merge interaction callback interface\n */\nexport interface MergeCallbacks {\n  onRouterConflict?: (key: string, existingValue: any, newValue: any) => Promise<boolean>;\n  onTransformerConflict?: (transformerPath: string) => Promise<'keep' | 'overwrite' | 'skip'>;\n  onConfigConflict?: (key: string) => Promise<boolean>;\n}\n\n/**\n * Main configuration merge function\n * @param baseConfig Base configuration (existing configuration)\n * @param presetConfig Preset configuration\n * @param strategy Merge strategy\n * @param callbacks Interactive callback functions\n * @returns Merged configuration\n */\nexport async function mergeConfig(\n  baseConfig: any,\n  presetConfig: any,\n  strategy: MergeStrategy = MergeStrategy.ASK,\n  callbacks?: MergeCallbacks\n): Promise<any> {\n  const result = { ...baseConfig };\n\n  // Merge Providers\n  if (presetConfig.Providers) {\n    result.Providers = mergeProviders(\n      result.Providers || [],\n      presetConfig.Providers\n    );\n  }\n\n  // Merge Router\n  if (presetConfig.Router) {\n    result.Router = await mergeRouter(\n      result.Router || {},\n      presetConfig.Router,\n      strategy,\n      callbacks?.onRouterConflict\n    );\n  }\n\n  // Merge transformers\n  if (presetConfig.transformers) {\n    result.transformers = await mergeTransformers(\n      result.transformers || [],\n      presetConfig.transformers,\n      strategy,\n      callbacks?.onTransformerConflict\n    );\n  }\n\n  // Merge other configurations\n  const otherConfig = await mergeOtherConfig(\n    result,\n    presetConfig,\n    strategy,\n    callbacks?.onConfigConflict\n  );\n\n  return otherConfig;\n}\n"
  },
  {
    "path": "packages/shared/src/preset/readPreset.ts",
    "content": "/**\n * Read preset configuration file\n * Used by CLI to quickly read preset configuration\n */\n\nimport * as fs from 'fs/promises';\nimport * as path from 'path';\nimport JSON5 from 'json5';\nimport { getPresetDir } from './install';\n\n/**\n * Read preset configuration file\n * @param name Preset name\n * @returns Preset configuration object, or null if file does not exist\n */\nexport async function readPresetFile(name: string): Promise<any | null> {\n  try {\n    const presetDir = getPresetDir(name);\n    const manifestPath = path.join(presetDir, 'manifest.json');\n    const manifest = JSON5.parse(await fs.readFile(manifestPath, 'utf-8'));\n    // manifest is already a flat structure, return directly\n    return manifest;\n  } catch (error: any) {\n    if (error.code === 'ENOENT') {\n      return null;\n    }\n    console.error(`Failed to read preset file: ${error.message}`);\n    return null;\n  }\n}\n"
  },
  {
    "path": "packages/shared/src/preset/schema.ts",
    "content": "/**\n * Dynamic configuration Schema handler\n * Responsible for parsing and validating configuration schema, handling conditional logic and variable replacement\n */\n\nimport path from 'path';\nimport {\n  RequiredInput,\n  InputType,\n  Condition,\n  DynamicOptions,\n  InputOption,\n  ConfigMapping,\n  TemplateConfig,\n  PresetConfigSection,\n  PresetFile,\n  ManifestFile,\n  UserInputValues,\n} from './types';\n\n/**\n * Parse field path (supports arrays and nesting)\n * Example: Providers[0].name => ['Providers', '0', 'name']\n */\nexport function parseFieldPath(path: string): string[] {\n  const regex = /(\\w+)|\\[(\\d+)\\]/g;\n  const parts: string[] = [];\n  let match;\n\n  while ((match = regex.exec(path)) !== null) {\n    parts.push(match[1] || match[2]);\n  }\n\n  return parts;\n}\n\n/**\n * Get value from object by field path\n */\nexport function getValueByPath(obj: any, path: string): any {\n  const parts = parseFieldPath(path);\n  let current = obj;\n\n  for (const part of parts) {\n    if (current == null) {\n      return undefined;\n    }\n    current = current[part];\n  }\n\n  return current;\n}\n\n/**\n * Set value in object by field path\n */\nexport function setValueByPath(obj: any, path: string, value: any): void {\n  const parts = parseFieldPath(path);\n  const lastKey = parts.pop()!;\n  let current = obj;\n\n  for (const part of parts) {\n    if (!(part in current)) {\n      // Determine if it's an array or object\n      const nextPart = parts[parts.indexOf(part) + 1];\n      if (nextPart && /^\\d+$/.test(nextPart)) {\n        current[part] = [];\n      } else {\n        current[part] = {};\n      }\n    }\n    current = current[part];\n  }\n\n  current[lastKey] = value;\n}\n\n/**\n * Evaluate conditional expression\n */\nexport function evaluateCondition(\n  condition: Condition,\n  values: UserInputValues\n): boolean {\n  const actualValue = values[condition.field];\n\n  // Handle exists operator\n  if (condition.operator === 'exists') {\n    return actualValue !== undefined && actualValue !== null;\n  }\n\n  // Handle in operator\n  if (condition.operator === 'in') {\n    return Array.isArray(condition.value) && condition.value.includes(actualValue);\n  }\n\n  // Handle nin operator\n  if (condition.operator === 'nin') {\n    return Array.isArray(condition.value) && !condition.value.includes(actualValue);\n  }\n\n  // Handle other operators\n  switch (condition.operator) {\n    case 'eq':\n      return actualValue === condition.value;\n    case 'ne':\n      return actualValue !== condition.value;\n    case 'gt':\n      return actualValue > condition.value;\n    case 'lt':\n      return actualValue < condition.value;\n    case 'gte':\n      return actualValue >= condition.value;\n    case 'lte':\n      return actualValue <= condition.value;\n    default:\n      // Default to eq\n      return actualValue === condition.value;\n  }\n}\n\n/**\n * Evaluate multiple conditions (AND logic)\n */\nexport function evaluateConditions(\n  conditions: Condition | Condition[],\n  values: UserInputValues\n): boolean {\n  if (!conditions) {\n    return true;\n  }\n\n  if (!Array.isArray(conditions)) {\n    return evaluateCondition(conditions, values);\n  }\n\n  // If array, use AND logic (all conditions must be satisfied)\n  return conditions.every(condition => evaluateCondition(condition, values));\n}\n\n/**\n * Determine if field should be displayed\n */\nexport function shouldShowField(\n  field: RequiredInput,\n  values: UserInputValues\n): boolean {\n  if (!field.when) {\n    return true;\n  }\n\n  return evaluateConditions(field.when, values);\n}\n\n/**\n * Get dynamic options list\n */\nexport function getDynamicOptions(\n  dynamicOptions: DynamicOptions,\n  presetConfig: PresetConfigSection,\n  values: UserInputValues\n): InputOption[] {\n  switch (dynamicOptions.type) {\n    case 'static':\n      return dynamicOptions.options || [];\n\n    case 'providers': {\n      // Extract options from preset's Providers\n      const providers = presetConfig.Providers || [];\n      return providers.map((p: any) => ({\n        label: p.name || p.id || String(p),\n        value: p.name || p.id || String(p),\n        description: p.api_base_url,\n      }));\n    }\n\n    case 'models': {\n      // Extract from specified provider's models\n      const providerField = dynamicOptions.providerField;\n      if (!providerField) {\n        return [];\n      }\n\n      // Parse provider reference (e.g. #{selectedProvider})\n      const providerId = String(providerField).replace(/^#{(.+)}$/, '$1');\n      const selectedProvider = values[providerId];\n\n      if (!selectedProvider || !presetConfig.Providers) {\n        return [];\n      }\n\n      // Find corresponding provider\n      const provider = presetConfig.Providers.find(\n        (p: any) => p.name === selectedProvider || p.id === selectedProvider\n      );\n\n      if (!provider || !provider.models) {\n        return [];\n      }\n\n      return provider.models.map((model: string) => ({\n        label: model,\n        value: model,\n      }));\n    }\n\n    case 'custom':\n      // Reserved, not implemented yet\n      return [];\n\n    default:\n      return [];\n  }\n}\n\n/**\n * Resolve options (supports static and dynamic options)\n */\nexport function resolveOptions(\n  field: RequiredInput,\n  presetConfig: PresetConfigSection,\n  values: UserInputValues\n): InputOption[] {\n  if (!field.options) {\n    return [];\n  }\n\n  // Determine if static or dynamic options\n  const options = field.options as any;\n\n  if (Array.isArray(options)) {\n    // Static options array\n    return options as InputOption[];\n  }\n\n  if (options.type) {\n    // Dynamic options\n    return getDynamicOptions(options, presetConfig, values);\n  }\n\n  return [];\n}\n\n/**\n * Template variable replacement\n * Supports #{variable} syntax (different from statusline's {{variable}} format)\n */\nexport function replaceTemplateVariables(\n  template: any,\n  values: UserInputValues\n): any {\n  if (template === null || template === undefined) {\n    return template;\n  }\n\n  // Handle strings\n  if (typeof template === 'string') {\n    return template.replace(/#{(\\w+)}/g, (_, key) => {\n      return values[key] !== undefined ? String(values[key]) : '';\n    });\n  }\n\n  // Handle arrays\n  if (Array.isArray(template)) {\n    return template.map(item => replaceTemplateVariables(item, values));\n  }\n\n  // Handle objects\n  if (typeof template === 'object') {\n    const result: any = {};\n    for (const [key, value] of Object.entries(template)) {\n      result[key] = replaceTemplateVariables(value, values);\n    }\n    return result;\n  }\n\n  // Return other types directly\n  return template;\n}\n\n/**\n * Apply configuration mappings\n */\nexport function applyConfigMappings(\n  mappings: ConfigMapping[],\n  values: UserInputValues,\n  config: PresetConfigSection\n): PresetConfigSection {\n  const result = { ...config };\n\n  for (const mapping of mappings) {\n    // Check condition\n    if (mapping.when && !evaluateConditions(mapping.when, values)) {\n      continue;\n    }\n\n    // Resolve value\n    let value: any;\n    if (typeof mapping.value === 'string' && mapping.value.startsWith('#')) {\n      // Variable reference\n      const varName = mapping.value.replace(/^#{(.+)}$/, '$1');\n      value = values[varName];\n    } else {\n      // Fixed value\n      value = mapping.value;\n    }\n\n    // Apply to target path\n    setValueByPath(result, mapping.target, value);\n  }\n\n  return result;\n}\n\n/**\n * Get all field ids defined in schema\n */\nfunction getSchemaFields(schema?: RequiredInput[]): Set<string> {\n  if (!schema) return new Set();\n  return new Set(schema.map(field => field.id));\n}\n\n/**\n * Apply user inputs to preset configuration\n * This is the core function of the preset configuration system, uniformly handling\n * configuration application for both CLI and UI layers\n *\n * @param presetFile Preset file object\n * @param values User input values (schema id -> value)\n * @returns Applied configuration object\n */\nexport function applyUserInputs(\n  presetFile: PresetFile,\n  values: UserInputValues\n): PresetConfigSection {\n  let config: PresetConfigSection = {};\n\n  // Get field ids defined in schema, for subsequent filtering\n  const schemaFields = getSchemaFields(presetFile.schema);\n\n  // 1. First apply template (if exists)\n  // template completely defines configuration structure, using #{variable} placeholders\n  if (presetFile.template) {\n    config = replaceTemplateVariables(presetFile.template, values) as any;\n  } else {\n    // If no template, start from preset's existing config\n    // Keep all fields, including schema's id fields (because they may contain placeholders)\n    // These fields will be updated or replaced in subsequent configMappings\n    config = presetFile.config ? { ...presetFile.config } : {};\n\n    // Replace placeholders in config (e.g. #{apiKey} -> actual value)\n    config = replaceTemplateVariables(config, values) as any;\n\n    // Finally, remove schema id fields (they should not appear in final configuration)\n    for (const schemaField of schemaFields) {\n      delete config[schemaField];\n    }\n  }\n\n  // 2. Then apply configMappings (if exists)\n  // Map user inputs to specific configuration paths\n  if (presetFile.configMappings && presetFile.configMappings.length > 0) {\n    config = applyConfigMappings(presetFile.configMappings, values, config);\n  }\n\n  // 3. Compatible with legacy: apply to keys containing paths (e.g. \"Providers[0].api_key\")\n  for (const [key, value] of Object.entries(values)) {\n    if (key.includes('.') || key.includes('[')) {\n      setValueByPath(config, key, value);\n    }\n  }\n\n  return config;\n}\n\n/**\n * Validate user input\n */\nexport function validateInput(\n  field: RequiredInput,\n  value: any\n): { valid: boolean; error?: string } {\n  // Check required\n  if (field.required !== false && (value === undefined || value === null || value === '')) {\n    return {\n      valid: false,\n      error: `${field.label || field.id} is required`,\n    };\n  }\n\n  // If value is empty and not required, skip validation\n  if (!value && field.required === false) {\n    return { valid: true };\n  }\n\n  // Type check\n  switch (field.type) {\n    case InputType.NUMBER:\n      if (isNaN(Number(value))) {\n        return {\n          valid: false,\n          error: `${field.label || field.id} must be a number`,\n        };\n      }\n      const numValue = Number(value);\n      if (field.min !== undefined && numValue < field.min) {\n        return {\n          valid: false,\n          error: `${field.label || field.id} must be at least ${field.min}`,\n        };\n      }\n      if (field.max !== undefined && numValue > field.max) {\n        return {\n          valid: false,\n          error: `${field.label || field.id} must be at most ${field.max}`,\n        };\n      }\n      break;\n\n    case InputType.SELECT:\n    case InputType.MULTISELECT:\n      // Check if value is in options\n      // Skip here for now, as options need to be dynamically retrieved\n      break;\n  }\n\n  // Custom validator\n  if (field.validator) {\n    if (field.validator instanceof RegExp) {\n      if (!field.validator.test(String(value))) {\n        return {\n          valid: false,\n          error: `${field.label || field.id} format is invalid`,\n        };\n      }\n    } else if (typeof field.validator === 'string') {\n      const regex = new RegExp(field.validator);\n      if (!regex.test(String(value))) {\n        return {\n          valid: false,\n          error: `${field.label || field.id} format is invalid`,\n        };\n      }\n    } else if (typeof field.validator === 'function') {\n      const result = field.validator(value);\n      if (result === false) {\n        return {\n          valid: false,\n          error: `${field.label || field.id} is invalid`,\n        };\n      } else if (typeof result === 'string') {\n        return {\n          valid: false,\n          error: result,\n        };\n      }\n    }\n  }\n\n  return { valid: true };\n}\n\n/**\n * Get field default value\n */\nexport function getDefaultValue(field: RequiredInput): any {\n  if (field.defaultValue !== undefined) {\n    return field.defaultValue;\n  }\n\n  // Return default value based on type\n  switch (field.type) {\n    case InputType.CONFIRM:\n      return false;\n    case InputType.MULTISELECT:\n      return [];\n    case InputType.NUMBER:\n      return 0;\n    default:\n      return '';\n  }\n}\n\n/**\n * Sort fields by dependency\n * Ensure dependent fields are arranged first\n */\nexport function sortFieldsByDependencies(\n  fields: RequiredInput[]\n): RequiredInput[] {\n  const sorted: RequiredInput[] = [];\n  const visited = new Set<string>();\n\n  function visit(field: RequiredInput) {\n    if (visited.has(field.id)) {\n      return;\n    }\n\n    visited.add(field.id);\n\n    // First handle dependent fields\n    const dependencies = field.dependsOn || [];\n    for (const depId of dependencies) {\n      const depField = fields.find(f => f.id === depId);\n      if (depField) {\n        visit(depField);\n      }\n    }\n\n    // Extract dependencies from when conditions\n    if (field.when) {\n      const conditions = Array.isArray(field.when) ? field.when : [field.when];\n      for (const cond of conditions) {\n        const depField = fields.find(f => f.id === cond.field);\n        if (depField) {\n          visit(depField);\n        }\n      }\n    }\n\n    sorted.push(field);\n  }\n\n  for (const field of fields) {\n    visit(field);\n  }\n\n  return sorted;\n}\n\n/**\n * Build field dependency graph (for optimizing update order)\n */\nexport function buildDependencyGraph(\n  fields: RequiredInput[]\n): Map<string, Set<string>> {\n  const graph = new Map<string, Set<string>>();\n\n  for (const field of fields) {\n    const deps = new Set<string>();\n\n    // Extract from dependsOn\n    if (field.dependsOn) {\n      for (const dep of field.dependsOn) {\n        deps.add(dep);\n      }\n    }\n\n    // Extract dependencies from when conditions\n    if (field.when) {\n      const conditions = Array.isArray(field.when) ? field.when : [field.when];\n      for (const cond of conditions) {\n        deps.add(cond.field);\n      }\n    }\n\n    // Extract dependencies from dynamic options\n    if (field.options) {\n      const options = field.options as any;\n      if (options.type === 'models' && options.providerField) {\n        const providerId = String(options.providerField).replace(/^#{(.+)}$/, '$1');\n        deps.add(providerId);\n      }\n    }\n\n    graph.set(field.id, deps);\n  }\n\n  return graph;\n}\n\n/**\n * Get affected fields (when a field value changes, which fields need to be recalculated)\n */\nexport function getAffectedFields(\n  changedFieldId: string,\n  fields: RequiredInput[]\n): Set<string> {\n  const affected = new Set<string>();\n  const graph = buildDependencyGraph(fields);\n\n  // Find all fields that depend on changedFieldId\n  for (const [fieldId, deps] of graph.entries()) {\n    if (deps.has(changedFieldId)) {\n      affected.add(fieldId);\n    }\n  }\n\n  return affected;\n}\n\n/**\n * Process StatusLine configuration, convert relative scriptPath to absolute path\n * @param statusLineConfig StatusLine configuration\n * @param presetDir Preset directory path\n */\nfunction processStatusLineConfig(statusLineConfig: any, presetDir?: string): any {\n  if (!statusLineConfig || typeof statusLineConfig !== 'object') {\n    return statusLineConfig;\n  }\n\n  const result = { ...statusLineConfig };\n\n  // Process each theme's modules\n  for (const themeKey of Object.keys(result)) {\n    const theme = result[themeKey];\n    if (theme && typeof theme === 'object' && theme.modules) {\n      const modules = Array.isArray(theme.modules) ? theme.modules : [];\n      const processedModules = modules.map((module: any) => {\n        // If module has scriptPath and presetDir is provided, convert to absolute path\n        if (module.scriptPath && presetDir && !module.scriptPath.startsWith('/')) {\n          return {\n            ...module,\n            scriptPath: path.join(presetDir, module.scriptPath)\n          };\n        }\n        return module;\n      });\n      result[themeKey] = {\n        ...theme,\n        modules: processedModules\n      };\n    }\n  }\n\n  return result;\n}\n\n/**\n * Process transformers configuration, convert relative path to absolute path\n * @param transformersConfig Transformers configuration array\n * @param presetDir Preset directory path\n */\nfunction processTransformersConfig(transformersConfig: any[], presetDir?: string): any[] {\n  if (!transformersConfig || !Array.isArray(transformersConfig)) {\n    return transformersConfig;\n  }\n\n  if (!presetDir) {\n    return transformersConfig;\n  }\n\n  return transformersConfig.map((transformer: any) => {\n    // If transformer has path and it's a relative path, convert to absolute path\n    if (transformer.path && !transformer.path.startsWith('/')) {\n      return {\n        ...transformer,\n        path: path.join(presetDir, transformer.path)\n      };\n    }\n    return transformer;\n  });\n}\n\n/**\n * Load configuration from Manifest and apply userValues\n * Used when reading installed presets, applying user configuration values at runtime\n *\n * @param manifest Manifest object (contains original configuration and userValues)\n * @param presetDir Optional preset directory path (for resolving relative paths like scriptPath)\n * @returns Applied configuration object\n */\nexport function loadConfigFromManifest(manifest: ManifestFile, presetDir?: string): PresetConfigSection {\n  // Convert manifest to PresetFile format\n  const presetFile: PresetFile = {\n    metadata: {\n      name: manifest.name,\n      version: manifest.version,\n      description: manifest.description,\n      author: manifest.author,\n      homepage: manifest.homepage,\n      repository: manifest.repository,\n      license: manifest.license,\n      keywords: manifest.keywords,\n      ccrVersion: manifest.ccrVersion,\n      source: manifest.source,\n      sourceType: manifest.sourceType,\n      checksum: manifest.checksum,\n    },\n    config: {},\n    schema: manifest.schema,\n    template: manifest.template,\n    configMappings: manifest.configMappings,\n  };\n\n  // Extract configuration section from manifest (exclude metadata and dynamic configuration fields)\n  const METADATA_FIELDS = [\n    'name', 'version', 'description', 'author', 'homepage', 'repository',\n    'license', 'keywords', 'ccrVersion', 'source', 'sourceType', 'checksum',\n  ];\n  const DYNAMIC_CONFIG_FIELDS = ['schema', 'template', 'configMappings', 'userValues'];\n\n  for (const [key, value] of Object.entries(manifest)) {\n    if (!METADATA_FIELDS.includes(key) && !DYNAMIC_CONFIG_FIELDS.includes(key)) {\n      presetFile.config[key] = value;\n    }\n  }\n\n  let config: PresetConfigSection;\n\n  // If userValues exist, apply them\n  if (manifest.userValues && Object.keys(manifest.userValues).length > 0) {\n    config = applyUserInputs(presetFile, manifest.userValues);\n  } else {\n    // If no userValues, use original configuration directly\n    config = presetFile.config;\n  }\n\n  // Process StatusLine configuration (convert relative scriptPath to absolute path)\n  if (config.StatusLine) {\n    config.StatusLine = processStatusLineConfig(config.StatusLine, presetDir);\n  }\n\n  // Process transformers configuration (convert relative path to absolute path)\n  if (config.transformers) {\n    config.transformers = processTransformersConfig(config.transformers, presetDir);\n  }\n\n  return config;\n}\n"
  },
  {
    "path": "packages/shared/src/preset/sensitiveFields.ts",
    "content": "/**\n * Sensitive field identification and sanitization functionality\n */\n\nimport { SanitizeResult } from './types';\n\n// Sensitive field pattern list\nconst SENSITIVE_PATTERNS = [\n  'api_key', 'apikey', 'apiKey', 'APIKEY',\n  'api_secret', 'apisecret', 'apiSecret',\n  'secret', 'SECRET',\n  'token', 'TOKEN', 'auth_token',\n  'password', 'PASSWORD', 'passwd',\n  'private_key', 'privateKey',\n  'access_key', 'accessKey',\n];\n\n// Environment variable placeholder regex\nconst ENV_VAR_REGEX = /^\\$\\{?[A-Z_][A-Z0-9_]*\\}?$/;\n\n/**\n * Check if field name is sensitive\n */\nfunction isSensitiveField(fieldName: string): boolean {\n  const lowerFieldName = fieldName.toLowerCase();\n  return SENSITIVE_PATTERNS.some(pattern =>\n    lowerFieldName.includes(pattern.toLowerCase())\n  );\n}\n\n/**\n * Generate environment variable name\n * @param fieldType Field type (provider, transformer, global)\n * @param entityName Entity name (e.g., provider name)\n * @param fieldName Field name\n */\nexport function generateEnvVarName(\n  fieldType: 'provider' | 'transformer' | 'global',\n  entityName: string,\n  fieldName: string\n): string {\n  // Generate uppercase environment variable name\n  // e.g., DEEPSEEK_API_KEY, CUSTOM_TRANSFORMER_SECRET\n  const prefix = entityName.toUpperCase().replace(/[^A-Z0-9]/g, '_');\n  const field = fieldName.toUpperCase().replace(/[^A-Z0-9]/g, '_');\n\n  // If prefix and field name are the same (e.g., API_KEY), avoid duplication\n  if (prefix === field) {\n    return prefix;\n  }\n\n  return `${prefix}_${field}`;\n}\n\n/**\n * Check if value is already an environment variable placeholder\n */\nfunction isEnvPlaceholder(value: any): boolean {\n  if (typeof value !== 'string') {\n    return false;\n  }\n  return ENV_VAR_REGEX.test(value.trim());\n}\n\n/**\n * Extract variable name from environment variable placeholder\n * @param value Environment variable value (e.g., $VAR or ${VAR})\n */\nfunction extractEnvVarName(value: string): string | null {\n  const trimmed = value.trim();\n\n  // Match ${VAR_NAME} format\n  const bracedMatch = trimmed.match(/^\\$\\{([A-Z_][A-Z0-9_]*)\\}$/);\n  if (bracedMatch) {\n    return bracedMatch[1];\n  }\n\n  // Match $VAR_NAME format\n  const unbracedMatch = trimmed.match(/^\\$([A-Z_][A-Z0-9_]*)$/);\n  if (unbracedMatch) {\n    return unbracedMatch[1];\n  }\n\n  return null;\n}\n\n/**\n * Recursively traverse object to identify and sanitize sensitive fields\n * @param config Configuration object\n * @param path Current field path\n * @param sanitizedCount Sanitized field count\n */\nfunction sanitizeObject(\n  config: any,\n  path: string = '',\n  sanitizedCount: number = 0\n): { sanitized: any; count: number } {\n  if (!config || typeof config !== 'object') {\n    return { sanitized: config, count: sanitizedCount };\n  }\n\n  if (Array.isArray(config)) {\n    const sanitizedArray: any[] = [];\n    for (let i = 0; i < config.length; i++) {\n      const result = sanitizeObject(\n        config[i],\n        path ? `${path}[${i}]` : `[${i}]`,\n        sanitizedCount\n      );\n      sanitizedArray.push(result.sanitized);\n      sanitizedCount = result.count;\n    }\n    return { sanitized: sanitizedArray, count: sanitizedCount };\n  }\n\n  const sanitizedObj: any = {};\n  for (const [key, value] of Object.entries(config)) {\n    const currentPath = path ? `${path}.${key}` : key;\n\n    // Check if this is a sensitive field\n    if (isSensitiveField(key) && typeof value === 'string') {\n      // If value is already an environment variable, keep unchanged\n      if (isEnvPlaceholder(value)) {\n        sanitizedObj[key] = value;\n      } else {\n        // Sanitize: replace with environment variable placeholder\n        // Try to infer entity name from path\n        let entityName = 'CONFIG';\n        const pathParts = currentPath.split('.');\n\n        // If path contains Providers or transformers, try to extract entity name\n        for (let i = 0; i < pathParts.length; i++) {\n          if (pathParts[i] === 'Providers' || pathParts[i] === 'transformers') {\n            // Find name field\n            if (i + 1 < pathParts.length && pathParts[i + 1].match(/^\\d+$/)) {\n              // This is array index, find name field at same level\n              const parentPath = pathParts.slice(0, i + 2).join('.');\n              // Find name in current context\n              const context = config;\n              if (context.name) {\n                entityName = context.name;\n              }\n            }\n            break;\n          }\n        }\n\n        const envVarName = generateEnvVarName('global', entityName, key);\n        sanitizedObj[key] = `\\${${envVarName}}`;\n\n        sanitizedCount++;\n      }\n    } else if (typeof value === 'object' && value !== null) {\n      // Recursively process nested objects\n      const result = sanitizeObject(value, currentPath, sanitizedCount);\n      sanitizedObj[key] = result.sanitized;\n      sanitizedCount = result.count;\n    } else {\n      // Keep original value\n      sanitizedObj[key] = value;\n    }\n  }\n\n  return { sanitized: sanitizedObj, count: sanitizedCount };\n}\n\n/**\n * Sanitize configuration object\n * @param config Original configuration\n * @returns Sanitization result\n */\nexport async function sanitizeConfig(config: any): Promise<SanitizeResult> {\n  // Deep copy configuration to avoid modifying original object\n  const configCopy = JSON.parse(JSON.stringify(config));\n\n  const result = sanitizeObject(configCopy);\n\n  return {\n    sanitizedConfig: result.sanitized,\n    sanitizedCount: result.count,\n  };\n}\n\n/**\n * Fill sensitive information into configuration\n * @param config Preset configuration (containing environment variable placeholders)\n * @param inputs User input sensitive information\n * @returns Filled configuration\n */\nexport function fillSensitiveInputs(config: any, inputs: Record<string, string>): any {\n  const configCopy = JSON.parse(JSON.stringify(config));\n\n  function fillObject(obj: any, path: string = ''): any {\n    if (!obj || typeof obj !== 'object') {\n      return obj;\n    }\n\n    if (Array.isArray(obj)) {\n      return obj.map((item, index) =>\n        fillObject(item, path ? `${path}[${index}]` : `[${index}]`)\n      );\n    }\n\n    const result: any = {};\n    for (const [key, value] of Object.entries(obj)) {\n      const currentPath = path ? `${path}.${key}` : key;\n\n      if (typeof value === 'string' && isEnvPlaceholder(value)) {\n        // Check if there is user input\n        const input = inputs[currentPath];\n        if (input) {\n          result[key] = input;\n        } else {\n          result[key] = value;\n        }\n      } else if (typeof value === 'object' && value !== null) {\n        result[key] = fillObject(value, currentPath);\n      } else {\n        result[key] = value;\n      }\n    }\n\n    return result;\n  }\n\n  return fillObject(configCopy);\n}\n"
  },
  {
    "path": "packages/shared/src/preset/types.ts",
    "content": "/**\n * Type definitions for preset functionality\n */\n\n// Collection of user input values\nexport interface UserInputValues {\n  [inputId: string]: any;\n}\n\n// Input type enumeration\nexport enum InputType {\n  PASSWORD = 'password',         // Password input (hidden)\n  INPUT = 'input',               // Text input\n  SELECT = 'select',             // Single selection\n  MULTISELECT = 'multiselect',   // Multiple selection\n  CONFIRM = 'confirm',           // Confirmation checkbox\n  EDITOR = 'editor',             // Multi-line text editor\n  NUMBER = 'number',             // Number input\n}\n\n// Option definition\nexport interface InputOption {\n  label: string;                 // Display text\n  value: string | number | boolean; // Actual value\n  description?: string;          // Option description\n  disabled?: boolean;            // Whether disabled\n  icon?: string;                 // Icon\n}\n\n// Dynamic option source\nexport interface DynamicOptions {\n  type: 'static' | 'providers' | 'models' | 'custom';\n  // static: Use fixed options array\n  // providers: Dynamically retrieve from Providers configuration\n  // models: Retrieve from specified provider's models\n  // custom: Custom function (reserved, not implemented yet)\n\n  // Used when type is 'static'\n  options?: InputOption[];\n\n  // Used when type is 'providers'\n  // Automatically extract name and related configuration from preset's Providers\n\n  // Used when type is 'models'\n  providerField?: string;        // Point to provider selector field path (e.g. \"#{selectedProvider}\")\n\n  // Used when type is 'custom' (reserved)\n  source?: string;               // Custom data source\n}\n\n// Conditional expression\nexport interface Condition {\n  field: string;                 // Dependent field path\n  operator?: 'eq' | 'ne' | 'in' | 'nin' | 'gt' | 'lt' | 'gte' | 'lte' | 'exists';\n  value?: any;                   // Comparison value\n  // eq: equals\n  // ne: not equals\n  // in: included in (array)\n  // nin: not included in (array)\n  // gt: greater than\n  // lt: less than\n  // gte: greater than or equal to\n  // lte: less than or equal to\n  // exists: field exists (doesn't check value)\n}\n\n// Complex field input configuration\nexport interface RequiredInput {\n  id: string;                    // Unique identifier (for variable reference)\n  type?: InputType;              // Input type, defaults to password\n  label?: string;                // Display label\n  prompt?: string;               // Prompt information/description\n  placeholder?: string;          // Placeholder\n\n  // Option configuration (for select/multiselect)\n  options?: InputOption[] | DynamicOptions;\n\n  // Conditional display\n  when?: Condition | Condition[]; // Show this field only when conditions are met (supports AND/OR logic)\n\n  // Default value\n  defaultValue?: any;\n\n  // Validation rules\n  required?: boolean;            // Whether required, defaults to true\n  validator?: RegExp | string | ((value: any) => boolean | string);\n\n  // UI configuration\n  min?: number;                  // Minimum value (for number)\n  max?: number;                  // Maximum value (for number)\n  rows?: number;                 // Number of rows (for editor)\n\n  // Advanced configuration\n  dependsOn?: string[];          // Explicitly declare dependent fields (for optimizing update order)\n}\n\n// Provider configuration\nexport interface ProviderConfig {\n  name: string;\n  api_base_url: string;\n  api_key: string;\n  models: string[];\n  transformer?: any;\n  [key: string]: any;\n}\n\n// Router configuration\nexport interface RouterConfig {\n  default?: string;\n  background?: string;\n  think?: string;\n  longContext?: string;\n  longContextThreshold?: number;\n  webSearch?: string;\n  image?: string;\n  [key: string]: string | number | undefined;\n}\n\n// Transformer configuration\nexport interface TransformerConfig {\n  path?: string;\n  use: Array<string | [string, any]>;\n  options?: any;\n  [key: string]: any;\n}\n\n// Preset metadata (flattened structure, for manifest.json)\nexport interface PresetMetadata {\n  name: string;                   // Preset name\n  version: string;                // Version number (semver)\n  description?: string;           // Description\n  author?: string;                // Author\n  homepage?: string;              // Homepage\n  repository?: string;            // Source repository\n  license?: string;               // License\n  keywords?: string[];            // Keywords\n  ccrVersion?: string;            // Compatible CCR version\n  source?: string;                // Preset source URL\n  sourceType?: 'local' | 'gist' | 'registry';\n  checksum?: string;              // Preset content checksum\n}\n\n// Preset configuration section\nexport interface PresetConfigSection {\n  Providers?: ProviderConfig[];\n  Router?: RouterConfig;\n  transformers?: TransformerConfig[];\n  StatusLine?: any;\n  NON_INTERACTIVE_MODE?: boolean;\n\n  // CLI-only fields (not used by server)\n  noServer?: boolean;                // CLI: Whether to skip local server startup and use provider's API directly\n  claudeCodeSettings?: {             // CLI: Claude Code specific settings\n    env?: Record<string, any>;       // CLI: Environment variables to pass to Claude Code\n    statusLine?: any;                // CLI: Status line configuration\n    [key: string]: any;\n  };\n\n  [key: string]: any;\n}\n\n// Template configuration (for dynamically generating configuration based on user input)\nexport interface TemplateConfig {\n  // Template configuration using #{variable} syntax (different from statusline's {{variable}} format)\n  // Example: { \"Providers\": [{ \"name\": \"#{providerName}\", \"api_key\": \"#{apiKey}\" }] }\n  [key: string]: any;\n}\n\n// Configuration mapping (maps user input values to specific configuration locations)\nexport interface ConfigMapping {\n  // Field path (supports array syntax, e.g. \"Providers[0].api_key\")\n  target: string;\n\n  // Value source (references user input id, or uses fixed value)\n  value: string | any;  // If string and starts with #, treated as variable reference (e.g. #{fieldId})\n\n  // Condition (optional, apply this mapping only when condition is met)\n  when?: Condition | Condition[];\n}\n\n// Complete preset file format\nexport interface PresetFile {\n  metadata?: PresetMetadata;\n  config: PresetConfigSection;\n  secrets?: {\n    // Sensitive information storage, format: field path -> value\n    // Example: { \"Providers[0].api_key\": \"sk-xxx\", \"APIKEY\": \"my-secret\" }\n    [fieldPath: string]: string;\n  };\n\n  // === Dynamic configuration system ===\n  // Configuration input schema\n  schema?: RequiredInput[];\n\n  // Configuration template (uses variable replacement)\n  template?: TemplateConfig;\n\n  // Configuration mappings (maps user input to configuration)\n  configMappings?: ConfigMapping[];\n}\n\n// manifest.json format (file inside ZIP archive)\nexport interface ManifestFile extends PresetMetadata, PresetConfigSection {\n  // === Dynamic configuration system ===\n  schema?: RequiredInput[];\n  template?: TemplateConfig;\n  configMappings?: ConfigMapping[];\n\n  // === User configuration value storage ===\n  // User-filled configuration values are stored separately from original configuration\n  // Values collected during installation are stored here, applied at runtime\n  userValues?: UserInputValues;\n}\n\n// Online preset index entry\nexport interface PresetIndexEntry {\n  id: string;                     // Unique identifier\n  name: string;                   // Display name\n  description?: string;           // Short description\n  version: string;                // Latest version\n  author?: string;                // Author\n  downloads?: number;             // Download count\n  stars?: number;                 // Star count\n  tags?: string[];                // Tags\n  url: string;                    // Download address\n  repo?: string;                  // Repository (e.g., 'owner/repo')\n  checksum?: string;              // SHA256 checksum\n  ccrVersion?: string;            // Compatible version\n}\n\n// Online preset repository index\nexport interface PresetRegistry {\n  version: string;                // Index format version\n  lastUpdated: string;            // Last update time\n  presets: PresetIndexEntry[];\n}\n\n// Configuration validation result\nexport interface ValidationResult {\n  valid: boolean;\n  errors: string[];\n  warnings: string[];\n}\n\n// Merge strategy enumeration\nexport enum MergeStrategy {\n  ASK = 'ask',                    // Interactive prompt\n  OVERWRITE = 'overwrite',        // Overwrite existing\n  MERGE = 'merge',                // Intelligent merge\n  SKIP = 'skip',                  // Skip conflicting items\n}\n\n// Sanitization result\nexport interface SanitizeResult {\n  sanitizedConfig: any;\n  sanitizedCount: number;\n}\n\n// Preset information (for list display)\nexport interface PresetInfo {\n  name: string;                   // Preset name\n  version?: string;               // Version number\n  description?: string;           // Description\n  author?: string;                // Author\n  config: PresetConfigSection;\n}\n"
  },
  {
    "path": "packages/shared/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.base.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"./dist\",\n    \"rootDir\": \"./src\"\n  },\n  \"include\": [\"src/**/*\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/ui/PROJECT.md",
    "content": "# 项目指南\n\n> 这是一个用于设置配置的前端项目，配置格式参考config.example.json\n\n## 技术栈\n1. 使用pnpm作为包管理工具\n2. 使用vite.js作为构建工具\n3. 使用react.js + tailwindcss + shadcn-ui构建前端界面\n\n## UI设计\n采用现代化的UI风格，让界面整体体现出呼吸感。整体配置应该简洁和通俗易懂，需要有必要的校验，易用的交互体验。\n\n## 接口设计\n不需要实现任何接口，但你需要根据config.example.json文件的内容mock数据\n\n## 代码指引\n在使用任何库之前你都需要使用websearch工具查找最新的文档，不要使用你知识库的内容，即使是显而易见的你以为的确定性的知识。\n\n## 多语言设计\n项目需要同时支持中文和英文\n\n## 构建发布\n最后需要构建出一个HTML文件，其中所有的js和css采用内联的方式，构建产物应该只包含一个html文件。"
  },
  {
    "path": "packages/ui/README.md",
    "content": "# React + TypeScript + Vite\n\nThis template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.\n\nCurrently, two official plugins are available:\n\n- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh\n- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh\n\n## Expanding the ESLint configuration\n\nIf you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:\n\n```js\nexport default tseslint.config([\n  globalIgnores(['dist']),\n  {\n    files: ['**/*.{ts,tsx}'],\n    extends: [\n      // Other configs...\n\n      // Remove tseslint.configs.recommended and replace with this\n      ...tseslint.configs.recommendedTypeChecked,\n      // Alternatively, use this for stricter rules\n      ...tseslint.configs.strictTypeChecked,\n      // Optionally, add this for stylistic rules\n      ...tseslint.configs.stylisticTypeChecked,\n\n      // Other configs...\n    ],\n    languageOptions: {\n      parserOptions: {\n        project: ['./tsconfig.node.json', './tsconfig.app.json'],\n        tsconfigRootDir: import.meta.dirname,\n      },\n      // other options...\n    },\n  },\n])\n```\n\nYou can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:\n\n```js\n// eslint.config.js\nimport reactX from 'eslint-plugin-react-x'\nimport reactDom from 'eslint-plugin-react-dom'\n\nexport default tseslint.config([\n  globalIgnores(['dist']),\n  {\n    files: ['**/*.{ts,tsx}'],\n    extends: [\n      // Other configs...\n      // Enable lint rules for React\n      reactX.configs['recommended-typescript'],\n      // Enable lint rules for React DOM\n      reactDom.configs.recommended,\n    ],\n    languageOptions: {\n      parserOptions: {\n        project: ['./tsconfig.node.json', './tsconfig.app.json'],\n        tsconfigRootDir: import.meta.dirname,\n      },\n      // other options...\n    },\n  },\n])\n```\n"
  },
  {
    "path": "packages/ui/components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"new-york\",\n  \"rsc\": false,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"config\": \"\",\n    \"css\": \"src/index.css\",\n    \"baseColor\": \"neutral\",\n    \"cssVariables\": true,\n    \"prefix\": \"\"\n  },\n  \"aliases\": {\n    \"components\": \"@/components\",\n    \"utils\": \"@/lib/utils\",\n    \"ui\": \"@/components/ui\",\n    \"lib\": \"@/lib\",\n    \"hooks\": \"@/hooks\"\n  },\n  \"iconLibrary\": \"lucide\"\n}"
  },
  {
    "path": "packages/ui/config.example.json",
    "content": "{\n  \"LOG\": true,\n  \"CLAUDE_PATH\": \"/Users/jinhuilee/.claude/local/claude\",\n  \"HOST\": \"127.0.0.1\",\n  \"PORT\": 8080,\n  \"APIKEY\": \"1\",\n  \"API_TIMEOUT_MS\": 600000,\n  \"transformers\": [\n    {\n      \"path\": \"/Users/abc/.claude-code-router/plugins/gemini-cli.js\",\n      \"options\": {\n        \"project\": \"x\"\n      }\n    }\n  ],\n  \"Providers\": [\n    {\n      \"name\": \"siliconflow\",\n      \"api_base_url\": \"https://api.moonshot.cn/v1/chat/completions\",\n      \"api_key\": \"sk-\",\n      \"models\": [\n        \"kimi-k2-0711-preview\"\n      ],\n      \"transformer\": {\n        \"use\": [\n          [\n            \"maxtoken\",\n            {\n              \"max_tokens\": 130000\n            }\n          ]\n        ]\n      }\n    },\n    {\n      \"name\": \"kimi\",\n      \"api_base_url\": \"https://api.moonshot.cn/v1/chat/completions\",\n      \"api_key\": \"sk-\",\n      \"models\": [\n        \"kimi-k2-0711-preview\"\n      ]\n    },\n    {\n      \"name\": \"groq\",\n      \"api_base_url\": \"https://api.groq.com/openai/v1/chat/completions\",\n      \"api_key\": \"\",\n      \"models\": [\n        \"moonshotai/kimi-k2-instruct\"\n      ],\n      \"transformer\": {\n        \"use\": [\n          [\n            \"maxtoken\",\n            {\n              \"max_tokens\": 16384\n            }\n          ],\n          \"groq\"\n        ]\n      }\n    },\n    {\n      \"name\": \"openrouter\",\n      \"api_base_url\": \"https://openrouter.ai/api/v1/chat/completions\",\n      \"api_key\": \"sk-or-v1-\",\n      \"models\": [\n        \"google/gemini-2.5-pro-preview\",\n        \"anthropic/claude-sonnet-4\",\n        \"anthropic/claude-3.5-sonnet\",\n        \"anthropic/claude-3.7-sonnet:thinking\",\n        \"deepseek/deepseek-chat-v3-0324\",\n        \"@preset/kimi\"\n      ],\n      \"transformer\": {\n        \"use\": [\n          \"openrouter\"\n        ],\n        \"deepseek/deepseek-chat-v3-0324\": {\n          \"use\": [\n            \"tooluse\"\n          ]\n        }\n      }\n    },\n    {\n      \"name\": \"deepseek\",\n      \"api_base_url\": \"https://api.deepseek.com/chat/completions\",\n      \"api_key\": \"sk-\",\n      \"models\": [\n        \"deepseek-chat\",\n        \"deepseek-reasoner\"\n      ],\n      \"transformer\": {\n        \"use\": [\n          \"deepseek\"\n        ],\n        \"deepseek-chat\": {\n          \"use\": [\n            \"tooluse\"\n          ]\n        }\n      }\n    },\n    {\n      \"name\": \"test\",\n      \"api_base_url\": \"https://tbai.xin/v1/chat/completions\",\n      \"api_key\": \"sk-\",\n      \"models\": [\n        \"gemini-2.5-pro\"\n      ]\n    },\n    {\n      \"name\": \"ollama\",\n      \"api_base_url\": \"http://localhost:11434/v1/chat/completions\",\n      \"api_key\": \"ollama\",\n      \"models\": [\n        \"qwen2.5-coder:latest\"\n      ]\n    },\n    {\n      \"name\": \"gemini\",\n      \"api_base_url\": \"https://generativelanguage.googleapis.com/v1beta/models/\",\n      \"api_key\": \"\",\n      \"models\": [\n        \"gemini-2.5-flash\",\n        \"gemini-2.5-pro\"\n      ],\n      \"transformer\": {\n        \"use\": [\n          \"gemini\"\n        ]\n      }\n    },\n    {\n      \"name\": \"volcengine\",\n      \"api_base_url\": \"https://ark.cn-beijing.volces.com/api/v3/chat/completions\",\n      \"api_key\": \"sk-xxx\",\n      \"models\": [\n        \"deepseek-v3-250324\",\n        \"deepseek-r1-250528\"\n      ],\n      \"transformer\": {\n        \"use\": [\n          \"deepseek\"\n        ]\n      }\n    },\n    {\n      \"name\": \"gemini-cli\",\n      \"api_base_url\": \"https://cloudcode-pa.googleapis.com/v1internal\",\n      \"api_key\": \"sk-xxx\",\n      \"models\": [\n        \"gemini-2.5-flash\",\n        \"gemini-2.5-pro\"\n      ],\n      \"transformer\": {\n        \"use\": [\n          \"gemini-cli\"\n        ]\n      }\n    },\n    {\n      \"name\": \"azure\",\n      \"api_base_url\": \"https://your-resource-name.openai.azure.com/\",\n      \"api_key\": \"\",\n      \"models\": [\n        \"gpt-4\"\n      ]\n    }\n  ],\n  \"Router\": {\n    \"default\": \"gemini-cli,gemini-2.5-pro\",\n    \"background\": \"gemini-cli,gemini-2.5-flash\",\n    \"think\": \"gemini-cli,gemini-2.5-pro\",\n    \"longContext\": \"gemini-cli,gemini-2.5-pro\",\n    \"webSearch\": \"gemini-cli,gemini-2.5-flash\"\n  },\n  \"NON_INTERACTIVE_MODE\": false\n}\n"
  },
  {
    "path": "packages/ui/eslint.config.js",
    "content": "import js from '@eslint/js'\nimport globals from 'globals'\nimport reactHooks from 'eslint-plugin-react-hooks'\nimport reactRefresh from 'eslint-plugin-react-refresh'\nimport tseslint from 'typescript-eslint'\nimport { globalIgnores } from 'eslint/config'\n\nexport default tseslint.config([\n  globalIgnores(['dist']),\n  {\n    files: ['**/*.{ts,tsx}'],\n    extends: [\n      js.configs.recommended,\n      tseslint.configs.recommended,\n      reactHooks.configs['recommended-latest'],\n      reactRefresh.configs.vite,\n    ],\n    languageOptions: {\n      ecmaVersion: 2020,\n      globals: globals.browser,\n    },\n  },\n])\n"
  },
  {
    "path": "packages/ui/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>CCR UI</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "packages/ui/package.json",
    "content": "{\n  \"name\": \"@CCR/ui\",\n  \"private\": true,\n  \"version\": \"2.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc -b && vite build\",\n    \"lint\": \"eslint .\",\n    \"preview\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"@monaco-editor/react\": \"^4.7.0\",\n    \"@radix-ui/react-checkbox\": \"^1.3.3\",\n    \"@radix-ui/react-dialog\": \"^1.1.14\",\n    \"@radix-ui/react-label\": \"^2.1.7\",\n    \"@radix-ui/react-popover\": \"^1.1.14\",\n    \"@radix-ui/react-select\": \"^2.2.6\",\n    \"@radix-ui/react-slot\": \"^1.2.3\",\n    \"@radix-ui/react-switch\": \"^1.2.5\",\n    \"@radix-ui/react-tabs\": \"^1.1.13\",\n    \"@radix-ui/react-tooltip\": \"^1.2.7\",\n    \"@tailwindcss/vite\": \"^4.1.11\",\n    \"class-variance-authority\": \"^0.7.1\",\n    \"clsx\": \"^2.1.1\",\n    \"cmdk\": \"^1.1.1\",\n    \"i18next\": \"^25.3.2\",\n    \"i18next-browser-languagedetector\": \"^8.2.0\",\n    \"lucide-react\": \"^0.525.0\",\n    \"react\": \"^19.1.0\",\n    \"react-colorful\": \"^5.6.1\",\n    \"react-dnd\": \"^16.0.1\",\n    \"react-dnd-html5-backend\": \"^16.0.1\",\n    \"react-dom\": \"^19.1.0\",\n    \"react-i18next\": \"^15.6.1\",\n    \"react-router-dom\": \"^7.7.0\",\n    \"remixicon\": \"^4.7.0\",\n    \"tailwind-merge\": \"^3.3.1\",\n    \"tailwindcss-animate\": \"^1.0.7\"\n  },\n  \"devDependencies\": {\n    \"@eslint/js\": \"^9.30.1\",\n    \"@tailwindcss/postcss\": \"^4.1.11\",\n    \"@types/node\": \"^24.1.0\",\n    \"@types/react\": \"^19.1.8\",\n    \"@types/react-dom\": \"^19.1.6\",\n    \"@vitejs/plugin-react\": \"^4.6.0\",\n    \"autoprefixer\": \"^10.4.21\",\n    \"eslint\": \"^9.30.1\",\n    \"eslint-plugin-react-hooks\": \"^5.2.0\",\n    \"eslint-plugin-react-refresh\": \"^0.4.20\",\n    \"globals\": \"^16.3.0\",\n    \"postcss\": \"^8.5.6\",\n    \"tailwindcss\": \"^4.1.11\",\n    \"tw-animate-css\": \"^1.3.5\",\n    \"typescript\": \"~5.8.3\",\n    \"typescript-eslint\": \"^8.35.1\",\n    \"vite\": \"^7.0.4\",\n    \"vite-plugin-singlefile\": \"^2.3.0\"\n  }\n}\n"
  },
  {
    "path": "packages/ui/src/App.tsx",
    "content": "import { useState, useEffect, useCallback, useRef } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { useNavigate } from \"react-router-dom\";\nimport { SettingsDialog } from \"@/components/SettingsDialog\";\nimport { Transformers } from \"@/components/Transformers\";\nimport { Providers } from \"@/components/Providers\";\nimport { Router } from \"@/components/Router\";\nimport { JsonEditor } from \"@/components/JsonEditor\";\nimport { LogViewer } from \"@/components/LogViewer\";\nimport { Button } from \"@/components/ui/button\";\nimport { useConfig } from \"@/components/ConfigProvider\";\nimport { api } from \"@/lib/api\";\nimport { Settings, Languages, Save, RefreshCw, FileJson, CircleArrowUp, FileText, FileCog } from \"lucide-react\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { Toast } from \"@/components/ui/toast\";\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from \"@/components/ui/tooltip\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogHeader,\n  DialogTitle,\n  DialogFooter,\n  DialogDescription,\n} from \"@/components/ui/dialog\";\nimport \"@/styles/animations.css\";\n\nfunction App() {\n  const { t, i18n } = useTranslation();\n  const navigate = useNavigate();\n  const { config, error } = useConfig();\n  const [isSettingsOpen, setIsSettingsOpen] = useState(false);\n  const [isJsonEditorOpen, setIsJsonEditorOpen] = useState(false);\n  const [isLogViewerOpen, setIsLogViewerOpen] = useState(false);\n  const [isCheckingAuth, setIsCheckingAuth] = useState(true);\n  const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' | 'warning' } | null>(null);\n  // 版本检查状态\n  const [isNewVersionAvailable, setIsNewVersionAvailable] = useState(false);\n  const [isUpdateDialogOpen, setIsUpdateDialogOpen] = useState(false);\n  const [newVersionInfo, setNewVersionInfo] = useState<{ version: string; changelog: string } | null>(null);\n  const [isCheckingUpdate, setIsCheckingUpdate] = useState(false);\n  const [hasCheckedUpdate, setHasCheckedUpdate] = useState(false);\n  const [isUpdateFeatureAvailable, setIsUpdateFeatureAvailable] = useState(true);\n  const hasAutoCheckedUpdate = useRef(false);\n\n  const saveConfig = async () => {\n    // Handle case where config might be null or undefined\n    if (!config) {\n      setToast({ message: t('app.config_missing'), type: 'error' });\n      return;\n    }\n    \n    try {\n      // Save to API\n      const response = await api.updateConfig(config);\n      // Show success message or handle as needed\n      console.log('Config saved successfully');\n      \n      // 根据响应信息进行提示\n      if (response && typeof response === 'object' && 'success' in response) {\n        const apiResponse = response as { success: boolean; message?: string };\n        if (apiResponse.success) {\n          setToast({ message: apiResponse.message || t('app.config_saved_success'), type: 'success' });\n        } else {\n          setToast({ message: apiResponse.message || t('app.config_saved_failed'), type: 'error' });\n        }\n      } else {\n        // 默认成功提示\n        setToast({ message: t('app.config_saved_success'), type: 'success' });\n      }\n    } catch (error) {\n      console.error('Failed to save config:', error);\n      // Handle error appropriately\n      setToast({ message: t('app.config_saved_failed') + ': ' + (error as Error).message, type: 'error' });\n    }\n  };\n\n  const saveConfigAndRestart = async () => {\n    // Handle case where config might be null or undefined\n    if (!config) {\n      setToast({ message: t('app.config_missing'), type: 'error' });\n      return;\n    }\n    \n    try {\n      // Save to API\n      const response = await api.updateConfig(config);\n      \n      // Check if save was successful before restarting\n      let saveSuccessful = true;\n      if (response && typeof response === 'object' && 'success' in response) {\n        const apiResponse = response as { success: boolean; message?: string };\n        if (!apiResponse.success) {\n          saveSuccessful = false;\n          setToast({ message: apiResponse.message || t('app.config_saved_failed'), type: 'error' });\n        }\n      }\n      \n      // Only restart if save was successful\n      if (saveSuccessful) {\n        // Restart service\n        const response = await api.restartService();\n        \n        // Show success message or handle as needed\n        console.log('Config saved and service restarted successfully');\n        \n        // 根据响应信息进行提示\n        if (response && typeof response === 'object' && 'success' in response) {\n          const apiResponse = response as { success: boolean; message?: string };\n          if (apiResponse.success) {\n            setToast({ message: apiResponse.message || t('app.config_saved_restart_success'), type: 'success' });\n          }\n        } else {\n          // 默认成功提示\n          setToast({ message: t('app.config_saved_restart_success'), type: 'success' });\n        }\n      }\n    } catch (error) {\n      console.error('Failed to save config and restart:', error);\n      // Handle error appropriately\n      setToast({ message: t('app.config_saved_restart_failed') + ': ' + (error as Error).message, type: 'error' });\n    }\n  };\n  \n  // 检查更新函数\n  const checkForUpdates = useCallback(async (showDialog: boolean = true) => {\n    // 如果已经检查过且有新版本，根据参数决定是否显示对话框\n    if (hasCheckedUpdate && isNewVersionAvailable) {\n      if (showDialog) {\n        setIsUpdateDialogOpen(true);\n      }\n      return;\n    }\n    \n    setIsCheckingUpdate(true);\n    try {\n      const updateInfo = await api.checkForUpdates();\n      \n      if (updateInfo.hasUpdate && updateInfo.latestVersion && updateInfo.changelog) {\n        setIsNewVersionAvailable(true);\n        setNewVersionInfo({\n          version: updateInfo.latestVersion,\n          changelog: updateInfo.changelog\n        });\n        // 只有在showDialog为true时才显示对话框\n        if (showDialog) {\n          setIsUpdateDialogOpen(true);\n        }\n      } else if (showDialog) {\n        // 只有在showDialog为true时才显示没有更新的提示\n        setToast({ message: t('app.no_updates_available'), type: 'success' });\n      }\n      \n      setHasCheckedUpdate(true);\n    } catch (error) {\n      console.error('Failed to check for updates:', error);\n      setIsUpdateFeatureAvailable(false);\n      if (showDialog) {\n        setToast({ message: t('app.update_check_failed') + ': ' + (error as Error).message, type: 'error' });\n      }\n    } finally {\n      setIsCheckingUpdate(false);\n    }\n  }, [hasCheckedUpdate, isNewVersionAvailable, t]);\n\n  useEffect(() => {\n    const checkAuth = async () => {\n      // If we already have a config, we're authenticated\n      if (config) {\n        setIsCheckingAuth(false);\n        // 自动检查更新，但不显示对话框\n        if (!hasCheckedUpdate && !hasAutoCheckedUpdate.current) {\n          hasAutoCheckedUpdate.current = true;\n          checkForUpdates(false);\n        }\n        return;\n      }\n      \n      // For empty API key, allow access without checking config\n      const apiKey = localStorage.getItem('apiKey');\n      if (!apiKey) {\n        setIsCheckingAuth(false);\n        return;\n      }\n      \n      // If we don't have a config, try to fetch it\n      try {\n        await api.getConfig();\n        // If successful, we don't need to do anything special\n        // The ConfigProvider will handle setting the config\n      } catch (err) {\n        // If it's a 401, the API client will redirect to login\n        // For other errors, we still show the app to display the error\n        console.error('Error checking auth:', err);\n        // Redirect to login on authentication error\n        if ((err as Error).message === 'Unauthorized') {\n          navigate('/login');\n        }\n      } finally {\n        setIsCheckingAuth(false);\n        // 在获取配置完成后检查更新，但不显示对话框\n        if (!hasCheckedUpdate && !hasAutoCheckedUpdate.current) {\n          hasAutoCheckedUpdate.current = true;\n          checkForUpdates(false);\n        }\n      }\n    };\n\n    checkAuth();\n    \n    // Listen for unauthorized events\n    const handleUnauthorized = () => {\n      navigate('/login');\n    };\n    \n    window.addEventListener('unauthorized', handleUnauthorized);\n    \n    return () => {\n      window.removeEventListener('unauthorized', handleUnauthorized);\n    };\n  }, [config, navigate, hasCheckedUpdate, checkForUpdates]);\n  \n  // 执行更新函数\n  const performUpdate = async () => {\n    if (!newVersionInfo) return;\n    \n    try {\n      const result = await api.performUpdate();\n      \n      if (result.success) {\n        setToast({ message: t('app.update_successful'), type: 'success' });\n        setIsNewVersionAvailable(false);\n        setIsUpdateDialogOpen(false);\n        setHasCheckedUpdate(false); // 重置检查状态，以便下次重新检查\n      } else {\n        setToast({ message: t('app.update_failed') + ': ' + result.message, type: 'error' });\n      }\n    } catch (error) {\n      console.error('Failed to perform update:', error);\n      setToast({ message: t('app.update_failed') + ': ' + (error as Error).message, type: 'error' });\n    }\n  };\n\n  \n  if (isCheckingAuth) {\n    return (\n      <div className=\"h-screen bg-gray-50 font-sans flex items-center justify-center\">\n        <div className=\"text-gray-500\">Loading application...</div>\n      </div>\n    );\n  }\n\n  if (error) {\n    return (\n      <div className=\"h-screen bg-gray-50 font-sans flex items-center justify-center\">\n        <div className=\"text-red-500\">Error: {error.message}</div>\n      </div>\n    );\n  }\n\n  // Handle case where config is null or undefined\n  if (!config) {\n    return (\n      <div className=\"h-screen bg-gray-50 font-sans flex items-center justify-center\">\n        <div className=\"text-gray-500\">Loading configuration...</div>\n      </div>\n    );\n  }\n\n  return (\n    <TooltipProvider>\n      <div className=\"h-screen bg-gray-50 font-sans\">\n      <header className=\"flex h-16 items-center justify-between border-b bg-white px-6\">\n        <h1 className=\"text-xl font-semibold text-gray-800\">{t('app.title')}</h1>\n        <div className=\"flex items-center gap-2\">\n          <Tooltip>\n            <TooltipTrigger asChild>\n              <Button variant=\"ghost\" size=\"icon\" onClick={() => setIsSettingsOpen(true)} className=\"transition-all-ease hover:scale-110\">\n                <Settings className=\"h-5 w-5\" />\n              </Button>\n            </TooltipTrigger>\n            <TooltipContent>\n              <p>{t('app.settings')}</p>\n            </TooltipContent>\n          </Tooltip>\n          <Tooltip>\n            <TooltipTrigger asChild>\n              <Button variant=\"ghost\" size=\"icon\" onClick={() => setIsJsonEditorOpen(true)} className=\"transition-all-ease hover:scale-110\">\n                <FileJson className=\"h-5 w-5\" />\n              </Button>\n            </TooltipTrigger>\n            <TooltipContent>\n              <p>{t('app.json_editor')}</p>\n            </TooltipContent>\n          </Tooltip>\n          <Tooltip>\n            <TooltipTrigger asChild>\n              <Button variant=\"ghost\" size=\"icon\" onClick={() => setIsLogViewerOpen(true)} className=\"transition-all-ease hover:scale-110\">\n                <FileText className=\"h-5 w-5\" />\n              </Button>\n            </TooltipTrigger>\n            <TooltipContent>\n              <p>{t('app.log_viewer')}</p>\n            </TooltipContent>\n          </Tooltip>\n          <Tooltip>\n            <TooltipTrigger asChild>\n              <Button variant=\"ghost\" size=\"icon\" onClick={() => navigate('/presets')} className=\"transition-all-ease hover:scale-110\">\n                <FileCog className=\"h-5 w-5\" />\n              </Button>\n            </TooltipTrigger>\n            <TooltipContent>\n              <p>{t('app.presets')}</p>\n            </TooltipContent>\n          </Tooltip>\n          <Popover>\n            <PopoverTrigger asChild>\n              <Button variant=\"ghost\" size=\"icon\" className=\"transition-all-ease hover:scale-110\">\n                <Languages className=\"h-5 w-5\" />\n              </Button>\n            </PopoverTrigger>\n            <PopoverContent className=\"w-32 p-2\">\n              <div className=\"space-y-1\">\n                <Button\n                  variant={i18n.language.startsWith('en') ? 'default' : 'ghost'}\n                  className=\"w-full justify-start transition-all-ease hover:scale-[1.02]\"\n                  onClick={() => i18n.changeLanguage('en')}\n                >\n                  English\n                </Button>\n                <Button\n                  variant={i18n.language.startsWith('zh') ? 'default' : 'ghost'}\n                  className=\"w-full justify-start transition-all-ease hover:scale-[1.02]\"\n                  onClick={() => i18n.changeLanguage('zh')}\n                >\n                  中文\n                </Button>\n              </div>\n            </PopoverContent>\n          </Popover>\n          {/* 更新版本按钮 - 仅当更新功能可用时显示 */}\n          {isUpdateFeatureAvailable && (\n            <Tooltip>\n              <TooltipTrigger asChild>\n                <Button\n                  variant=\"ghost\"\n                  size=\"icon\"\n                  onClick={() => checkForUpdates(true)}\n                  disabled={isCheckingUpdate}\n                  className=\"transition-all-ease hover:scale-110 relative\"\n                >\n                  <div className=\"relative\">\n                    <CircleArrowUp className=\"h-5 w-5\" />\n                    {isNewVersionAvailable && !isCheckingUpdate && (\n                      <div className=\"absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full border-2 border-white\"></div>\n                    )}\n                  </div>\n                  {isCheckingUpdate && (\n                    <div className=\"absolute inset-0 flex items-center justify-center\">\n                      <div className=\"h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent\"></div>\n                    </div>\n                  )}\n                </Button>\n              </TooltipTrigger>\n              <TooltipContent>\n                <p>{t('app.check_updates')}</p>\n              </TooltipContent>\n            </Tooltip>\n          )}\n          <Button onClick={saveConfig} variant=\"outline\" className=\"transition-all-ease hover:scale-[1.02] active:scale-[0.98]\">\n            <Save className=\"mr-2 h-4 w-4\" />\n            {t('app.save')}\n          </Button>\n          <Button onClick={saveConfigAndRestart} className=\"transition-all-ease hover:scale-[1.02] active:scale-[0.98]\">\n            <RefreshCw className=\"mr-2 h-4 w-4\" />\n            {t('app.save_and_restart')}\n          </Button>\n        </div>\n      </header>\n      <main className=\"flex h-[calc(100vh-4rem)] gap-4 p-4 overflow-hidden\">\n        <div className=\"w-3/5\">\n          <Providers />\n        </div>\n        <div className=\"flex w-2/5 flex-col gap-4\">\n          <div className=\"h-3/5\">\n            <Router />\n          </div>\n          <div className=\"flex-1 overflow-hidden\">\n            <Transformers />\n          </div>\n        </div>\n      </main>\n      <SettingsDialog isOpen={isSettingsOpen} onOpenChange={setIsSettingsOpen} />\n      <JsonEditor \n        open={isJsonEditorOpen} \n        onOpenChange={setIsJsonEditorOpen} \n        showToast={(message, type) => setToast({ message, type })} \n      />\n      <LogViewer \n        open={isLogViewerOpen} \n        onOpenChange={setIsLogViewerOpen} \n        showToast={(message, type) => setToast({ message, type })} \n      />\n      {/* 版本更新对话框 */}\n      <Dialog open={isUpdateDialogOpen} onOpenChange={setIsUpdateDialogOpen}>\n        <DialogContent className=\"max-w-2xl\">\n          <DialogHeader>\n            <DialogTitle>\n              {t('app.new_version_available')}\n              {newVersionInfo && (\n                <span className=\"ml-2 text-sm font-normal text-muted-foreground\">\n                  v{newVersionInfo.version}\n                </span>\n              )}\n            </DialogTitle>\n            <DialogDescription>\n              {t('app.update_description')}\n            </DialogDescription>\n          </DialogHeader>\n          <div className=\"max-h-96 overflow-y-auto py-4\">\n            {newVersionInfo?.changelog ? (\n              <div className=\"whitespace-pre-wrap text-sm\">\n                {newVersionInfo.changelog}\n              </div>\n            ) : (\n              <div className=\"text-muted-foreground\">\n                {t('app.no_changelog_available')}\n              </div>\n            )}\n          </div>\n          <DialogFooter>\n            <Button\n              variant=\"outline\"\n              onClick={() => setIsUpdateDialogOpen(false)}\n            >\n              {t('app.later')}\n            </Button>\n            <Button onClick={performUpdate}>\n              {t('app.update_now')}\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n      {toast && (\n        <Toast \n          message={toast.message} \n          type={toast.type} \n          onClose={() => setToast(null)} \n        />\n      )}\n    </div>\n    </TooltipProvider>\n  );\n}\n\nexport default App;\n"
  },
  {
    "path": "packages/ui/src/components/ConfigProvider.tsx",
    "content": "import { createContext, useContext, useState, useEffect } from 'react';\nimport type { ReactNode, Dispatch, SetStateAction } from 'react';\nimport { api } from '@/lib/api';\nimport type { Config, StatusLineConfig } from '@/types';\n\ninterface ConfigContextType {\n  config: Config | null;\n  setConfig: Dispatch<SetStateAction<Config | null>>;\n  error: Error | null;\n}\n\nconst ConfigContext = createContext<ConfigContextType | undefined>(undefined);\n\n// eslint-disable-next-line react-refresh/only-export-components\nexport function useConfig() {\n  const context = useContext(ConfigContext);\n  if (context === undefined) {\n    throw new Error('useConfig must be used within a ConfigProvider');\n  }\n  return context;\n}\n\ninterface ConfigProviderProps {\n  children: ReactNode;\n}\n\nexport function ConfigProvider({ children }: ConfigProviderProps) {\n  const [config, setConfig] = useState<Config | null>(null);\n  const [error, setError] = useState<Error | null>(null);\n  const [hasFetched, setHasFetched] = useState<boolean>(false);\n  const [apiKey, setApiKey] = useState<string | null>(localStorage.getItem('apiKey'));\n\n  // Listen for localStorage changes\n  useEffect(() => {\n    const handleStorageChange = () => {\n      setApiKey(localStorage.getItem('apiKey'));\n    };\n\n    window.addEventListener('storage', handleStorageChange);\n    return () => {\n      window.removeEventListener('storage', handleStorageChange);\n    };\n  }, []);\n\n  useEffect(() => {\n    const fetchConfig = async () => {\n      // Reset fetch state when API key changes\n      setHasFetched(false);\n      setConfig(null);\n      setError(null);\n    };\n\n    fetchConfig();\n  }, [apiKey]);\n\n  useEffect(() => {\n    const fetchConfig = async () => {\n      // Prevent duplicate API calls in React StrictMode\n      // Skip if we've already fetched\n      if (hasFetched) {\n        return;\n      }\n      setHasFetched(true);\n      \n      try {\n        // Try to fetch config regardless of API key presence\n        const data = await api.getConfig();\n        \n        // Validate the received data to ensure it has the expected structure\n        const validConfig = {\n          LOG: typeof data.LOG === 'boolean' ? data.LOG : false,\n          LOG_LEVEL: typeof data.LOG_LEVEL === 'string' ? data.LOG_LEVEL : 'debug',\n          CLAUDE_PATH: typeof data.CLAUDE_PATH === 'string' ? data.CLAUDE_PATH : '',\n          HOST: typeof data.HOST === 'string' ? data.HOST : '127.0.0.1',\n          PORT: typeof data.PORT === 'number' ? data.PORT : 3456,\n          APIKEY: typeof data.APIKEY === 'string' ? data.APIKEY : '',\n          API_TIMEOUT_MS: typeof data.API_TIMEOUT_MS === 'string' ? data.API_TIMEOUT_MS : '600000',\n          PROXY_URL: typeof data.PROXY_URL === 'string' ? data.PROXY_URL : '',\n          transformers: Array.isArray(data.transformers) ? data.transformers : [],\n          Providers: Array.isArray(data.Providers) ? data.Providers : [],\n          StatusLine: data.StatusLine && typeof data.StatusLine === 'object' ? {\n            enabled: typeof data.StatusLine.enabled === 'boolean' ? data.StatusLine.enabled : false,\n            currentStyle: typeof data.StatusLine.currentStyle === 'string' ? data.StatusLine.currentStyle : 'default',\n            default: data.StatusLine.default && typeof data.StatusLine.default === 'object' && Array.isArray(data.StatusLine.default.modules) ? data.StatusLine.default : { modules: [] },\n            powerline: data.StatusLine.powerline && typeof data.StatusLine.powerline === 'object' && Array.isArray(data.StatusLine.powerline.modules) ? data.StatusLine.powerline : { modules: [] }\n          } : { \n            enabled: false,\n            currentStyle: 'default',\n            default: { modules: [] },\n            powerline: { modules: [] }\n          },\n          Router: data.Router && typeof data.Router === 'object' ? {\n            default: typeof data.Router.default === 'string' ? data.Router.default : '',\n            background: typeof data.Router.background === 'string' ? data.Router.background : '',\n            think: typeof data.Router.think === 'string' ? data.Router.think : '',\n            longContext: typeof data.Router.longContext === 'string' ? data.Router.longContext : '',\n            longContextThreshold: typeof data.Router.longContextThreshold === 'number' ? data.Router.longContextThreshold : 60000,\n            webSearch: typeof data.Router.webSearch === 'string' ? data.Router.webSearch : '',\n            image: typeof data.Router.image === 'string' ? data.Router.image : ''\n          } : {\n            default: '',\n            background: '',\n            think: '',\n            longContext: '',\n            longContextThreshold: 60000,\n            webSearch: '',\n            image: ''\n          },\n          CUSTOM_ROUTER_PATH: typeof data.CUSTOM_ROUTER_PATH === 'string' ? data.CUSTOM_ROUTER_PATH : ''\n        };\n        \n        setConfig(validConfig);\n      } catch (err) {\n        console.error('Failed to fetch config:', err);\n        // If we get a 401, the API client will redirect to login\n        // Otherwise, set an empty config or error\n        if ((err as Error).message !== 'Unauthorized') {\n          // Set default empty config when fetch fails\n          setConfig({\n            LOG: false,\n            LOG_LEVEL: 'debug',\n            CLAUDE_PATH: '',\n            HOST: '127.0.0.1',\n            PORT: 3456,\n            APIKEY: '',\n            API_TIMEOUT_MS: '600000',\n            PROXY_URL: '',\n            transformers: [],\n            Providers: [],\n            StatusLine: undefined,\n            Router: {\n              default: '',\n              background: '',\n              think: '',\n              longContext: '',\n              longContextThreshold: 60000,\n              webSearch: '',\n              image: ''\n            },\n            CUSTOM_ROUTER_PATH: ''\n          });\n          setError(err as Error);\n        }\n      }\n    };\n\n    fetchConfig();\n  }, [hasFetched, apiKey]);\n\n  return (\n    <ConfigContext.Provider value={{ config, setConfig, error }}>\n      {children}\n    </ConfigContext.Provider>\n  );\n}\n"
  },
  {
    "path": "packages/ui/src/components/DebugPage.tsx",
    "content": "import React, { useState, useEffect, useRef } from 'react';\nimport { useNavigate, useLocation } from 'react-router-dom';\nimport { Button } from '@/components/ui/button';\nimport { ArrowLeft, Send, Copy, Square, History, Maximize } from 'lucide-react';\nimport { useTranslation } from 'react-i18next';\nimport MonacoEditor from '@monaco-editor/react';\nimport { RequestHistoryDrawer } from './RequestHistoryDrawer';\nimport { requestHistoryDB } from '@/lib/db';\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';\n\nexport function DebugPage() {\n  const navigate = useNavigate();\n  const location = useLocation();\n  const [requestData, setRequestData] = useState({\n    url: '',\n    method: 'POST',\n    headers: '{}',\n    body: '{}'\n  });\n  const [responseData, setResponseData] = useState({\n    status: 0,\n    responseTime: 0,\n    body: '',\n    headers: '{}'\n  });\n  const [isLoading, setIsLoading] = useState(false);\n  const [isHistoryDrawerOpen, setIsHistoryDrawerOpen] = useState(false);\n  const [fullscreenEditor, setFullscreenEditor] = useState<'headers' | 'body' | null>(null);\n  const headersEditorRef = useRef<any>(null);\n  const bodyEditorRef = useRef<any>(null);\n\n  // 切换全屏模式\n  const toggleFullscreen = (editorType: 'headers' | 'body') => {\n    const isEnteringFullscreen = fullscreenEditor !== editorType;\n    setFullscreenEditor(isEnteringFullscreen ? editorType : null);\n\n    // 延迟触发Monaco编辑器的重新布局，等待DOM更新完成\n    setTimeout(() => {\n      if (headersEditorRef.current) {\n        headersEditorRef.current.layout();\n      }\n      if (bodyEditorRef.current) {\n        bodyEditorRef.current.layout();\n      }\n    }, 300);\n  };\n\n  // 从URL参数中解析日志数据\n  useEffect(() => {\n    const params = new URLSearchParams(location.search);\n    const logDataParam = params.get('logData');\n\n    if (logDataParam) {\n      try {\n        const parsedData = JSON.parse(decodeURIComponent(logDataParam));\n\n        // 解析URL - 支持多种字段名\n        const url = parsedData.url || parsedData.requestUrl || parsedData.endpoint || '';\n\n        // 解析Method - 支持多种字段名和大小写\n        const method = (parsedData.method || parsedData.requestMethod || 'POST').toUpperCase();\n\n        // 解析Headers - 支持多种格式\n        let headers: Record<string, string> = {};\n        if (parsedData.headers) {\n          if (typeof parsedData.headers === 'string') {\n            try {\n              headers = JSON.parse(parsedData.headers);\n            } catch {\n              // 如果是字符串格式，尝试解析为键值对\n              const headerLines = parsedData.headers.split('\\n');\n              headerLines.forEach((line: string) => {\n                const [key, ...values] = line.split(':');\n                if (key && values.length > 0) {\n                  headers[key.trim()] = values.join(':').trim();\n                }\n              });\n            }\n          } else {\n            headers = parsedData.headers;\n          }\n        }\n\n        // 解析Body - 支持多种格式和嵌套结构\n        let body: Record<string, unknown> = {};\n        let bodyData = null;\n\n        // 支持多种字段名和嵌套结构\n        if (parsedData.body) {\n          bodyData = parsedData.body;\n        } else if (parsedData.request && parsedData.request.body) {\n          bodyData = parsedData.request.body;\n        }\n\n        if (bodyData) {\n          if (typeof bodyData === 'string') {\n            try {\n              // 尝试解析为JSON对象\n              const parsed = JSON.parse(bodyData);\n              body = parsed;\n            } catch {\n              // 如果不是JSON，检查是否是纯文本\n              const trimmed = bodyData.trim();\n              if (trimmed.startsWith('{') || trimmed.startsWith('[')) {\n                // 看起来像JSON但解析失败，作为字符串保存\n                body = { raw: bodyData };\n              } else {\n                // 普通文本，直接保存\n                body = { content: bodyData };\n              }\n            }\n          } else if (typeof bodyData === 'object') {\n            // 已经是对象，直接使用\n            body = bodyData;\n          } else {\n            // 其他类型，转换为字符串\n            body = { content: String(bodyData) };\n          }\n        }\n\n        // 预填充请求表单\n        setRequestData({\n          url,\n          method,\n          headers: JSON.stringify(headers, null, 2),\n          body: JSON.stringify(body, null, 2)\n        });\n\n        console.log('Log data parsed successfully:', { url, method, headers, body });\n      } catch (error) {\n        console.error('Failed to parse log data:', error);\n        console.error('Raw log data:', logDataParam);\n      }\n    }\n  }, [location.search]);\n\n  // 发送请求\n  const sendRequest = async () => {\n    try {\n      setIsLoading(true);\n\n      const headers = JSON.parse(requestData.headers);\n      const body = JSON.parse(requestData.body);\n\n      const startTime = Date.now();\n\n      const response = await fetch(requestData.url, {\n        method: requestData.method,\n        headers: {\n          'Content-Type': 'application/json',\n          ...headers\n        },\n        body: requestData.method !== 'GET' ? JSON.stringify(body) : undefined\n      });\n\n      const endTime = Date.now();\n      const responseTime = endTime - startTime;\n\n      const responseHeaders: Record<string, string> = {};\n      response.headers.forEach((value, key) => {\n        responseHeaders[key] = value;\n      });\n\n      const responseText = await response.text();\n      let responseBody = responseText;\n\n      // 尝试解析JSON响应\n      try {\n        const jsonResponse = JSON.parse(responseText);\n        responseBody = JSON.stringify(jsonResponse, null, 2);\n      } catch {\n        // 如果不是JSON，保持原样\n      }\n\n      const responseHeadersString = JSON.stringify(responseHeaders, null, 2);\n\n      setResponseData({\n        status: response.status,\n        responseTime,\n        body: responseBody,\n        headers: responseHeadersString\n      });\n\n      // 保存到IndexedDB\n      await requestHistoryDB.saveRequest({\n        url: requestData.url,\n        method: requestData.method,\n        headers: requestData.headers,\n        body: requestData.body,\n        status: response.status,\n        responseTime,\n        responseBody,\n        responseHeaders: responseHeadersString\n      });\n\n    } catch (error) {\n      console.error('Request failed:', error);\n      setResponseData({\n        status: 0,\n        responseTime: 0,\n        body: `请求失败: ${error instanceof Error ? error.message : '未知错误'}`,\n        headers: '{}'\n      });\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  // 从历史记录中选择请求\n  const handleSelectRequest = (request: import('@/lib/db').RequestHistoryItem) => {\n    setRequestData({\n      url: request.url,\n      method: request.method,\n      headers: request.headers,\n      body: request.body\n    });\n\n    setResponseData({\n      status: request.status,\n      responseTime: request.responseTime,\n      body: request.responseBody,\n      headers: request.responseHeaders\n    });\n  };\n\n  // 复制cURL命令\n  const copyCurl = () => {\n    try {\n      const headers = JSON.parse(requestData.headers);\n      const body = JSON.parse(requestData.body);\n\n      let curlCommand = `curl -X ${requestData.method} \"${requestData.url}\"`;\n\n      // 添加headers\n      Object.entries(headers).forEach(([key, value]) => {\n        curlCommand += ` \\\\\\n  -H \"${key}: ${value}\"`;\n      });\n\n      // 添加body\n      if (requestData.method !== 'GET' && Object.keys(body).length > 0) {\n        curlCommand += ` \\\\\\n  -d '${JSON.stringify(body)}'`;\n      }\n\n      navigator.clipboard.writeText(curlCommand);\n      alert('cURL命令已复制到剪贴板');\n    } catch (error) {\n      console.error('Failed to copy cURL:', error);\n      alert('复制cURL命令失败');\n    }\n  };\n\n\n  return (\n    <div className=\"h-screen bg-gray-50 font-sans\">\n      {/* 头部 */}\n      <header className=\"flex h-16 items-center justify-between border-b bg-white px-6\">\n        <div className=\"flex items-center gap-4\">\n          <Button variant=\"ghost\" size=\"sm\" onClick={() => navigate('/dashboard')}>\n            <ArrowLeft className=\"h-4 w-4 mr-2\" />\n            返回\n          </Button>\n          <h1 className=\"text-xl font-semibold text-gray-800\">HTTP 调试器</h1>\n        </div>\n        <div className=\"flex items-center gap-2\">\n          <Button variant=\"outline\" onClick={() => setIsHistoryDrawerOpen(true)}>\n            <History className=\"h-4 w-4 mr-2\" />\n            历史记录\n          </Button>\n          <Button variant=\"outline\" onClick={copyCurl}>\n            <Copy className=\"h-4 w-4 mr-2\" />\n            复制 cURL\n          </Button>\n        </div>\n      </header>\n\n      {/* 主要内容 */}\n      <main className=\"flex h-[calc(100vh-4rem)] flex-col gap-4 p-4 overflow-hidden\">\n        {/* 上部分：请求参数配置 - 上中下布局 */}\n        <div className=\"h-1/2 flex flex-col gap-4\">\n          <div className=\"bg-white rounded-lg border p-4 flex-1 flex flex-col\">\n            <h3 className=\"font-medium mb-4\">请求参数配置</h3>\n            <div className=\"flex flex-col gap-4 flex-1\">\n              {/* 上：Method、URL和发送请求按钮配置 */}\n              <div className=\"flex gap-4 items-end\">\n                <div className=\"w-32\">\n                  <label className=\"block text-sm font-medium mb-1\">Method</label>\n                  <select\n                    className=\"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm\"\n                    value={requestData.method}\n                    onChange={(e) => setRequestData(prev => ({ ...prev, method: e.target.value }))}\n                  >\n                    <option value=\"GET\">GET</option>\n                    <option value=\"POST\">POST</option>\n                    <option value=\"PUT\">PUT</option>\n                    <option value=\"DELETE\">DELETE</option>\n                    <option value=\"PATCH\">PATCH</option>\n                  </select>\n                </div>\n                <div className=\"flex-1\">\n                  <label className=\"block text-sm font-medium mb-1\">URL</label>\n                  <input\n                    type=\"text\"\n                    className=\"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm\"\n                    value={requestData.url}\n                    onChange={(e) => setRequestData(prev => ({ ...prev, url: e.target.value }))}\n                    placeholder=\"https://api.example.com/endpoint\"\n                  />\n                </div>\n                <Button\n                  variant={isLoading ? \"destructive\" : \"default\"}\n                  onClick={isLoading ? () => {} : sendRequest}\n                  disabled={isLoading || !requestData.url.trim()}\n                >\n                  {isLoading ? (\n                    <>\n                      <Square className=\"h-4 w-4 mr-2\" />\n                      请求中...\n                    </>\n                  ) : (\n                    <>\n                      <Send className=\"h-4 w-4 mr-2\" />\n                      发送请求\n                    </>\n                  )}\n                </Button>\n              </div>\n\n              {/* Headers和Body配置 - 使用tab布局 */}\n              <div className=\"flex-1\">\n                <Tabs defaultValue=\"headers\" className=\"h-full flex flex-col\">\n                  <TabsList className=\"grid w-full grid-cols-2\">\n                    <TabsTrigger value=\"headers\">Headers</TabsTrigger>\n                    <TabsTrigger value=\"body\">Body</TabsTrigger>\n                  </TabsList>\n\n                  <TabsContent value=\"headers\" className=\"flex-1 mt-2\">\n                    <div\n                      className={`${fullscreenEditor === 'headers' ? '' : 'h-full'} flex flex-col ${\n                        fullscreenEditor === 'headers' ? 'fixed bg-white w-[100vw] h-[100vh] z-[9999] top-0 left-0 p-4' : ''\n                      }`}\n                    >\n                      <div className=\"flex items-center justify-between mb-2\">\n                        <label className=\"block text-sm font-medium\">Headers (JSON)</label>\n                        <Button\n                            variant=\"ghost\"\n                            size=\"sm\"\n                            onClick={() => toggleFullscreen('headers')}\n                        >\n                          <Maximize className=\"h-4 w-4 mr-1\" />\n                          {fullscreenEditor === 'headers' ? '退出全屏' : '全屏'}\n                        </Button>\n                      </div>\n                      <div\n                        id=\"fullscreen-headers\"\n                        className={`${fullscreenEditor === 'headers' ? 'h-full' : 'flex-1'} border border-gray-300 rounded-md overflow-hidden relative`}\n                      >\n                        <MonacoEditor\n                          height=\"100%\"\n                          language=\"json\"\n                          value={requestData.headers}\n                          onChange={(value) => setRequestData(prev => ({ ...prev, headers: value || '{}' }))}\n                          onMount={(editor) => {\n                            headersEditorRef.current = editor;\n                          }}\n                          options={{\n                            minimap: { enabled: fullscreenEditor === 'headers' },\n                            scrollBeyondLastLine: false,\n                            fontSize: 14,\n                            lineNumbers: 'on',\n                            wordWrap: 'on',\n                            automaticLayout: true,\n                            formatOnPaste: true,\n                            formatOnType: true,\n                          }}\n                        />\n                      </div>\n                    </div>\n                  </TabsContent>\n\n                  <TabsContent value=\"body\" className=\"flex-1 mt-2\">\n                    <div\n                      className={`${fullscreenEditor === 'body' ? '' : 'h-full'} flex flex-col ${\n                        fullscreenEditor === 'body' ? 'fixed bg-white w-[100vw] h-[100vh] z-[9999] top-0 left-0 p-4' : ''\n                      }`}\n                    >\n                      <div className=\"flex items-center justify-between mb-2\">\n                        <label className=\"block text-sm font-medium\">Body (JSON)</label>\n                        <Button\n                            variant=\"ghost\"\n                            size=\"sm\"\n                            onClick={() => toggleFullscreen('body')}\n                        >\n                          <Maximize className=\"h-4 w-4 mr-1\" />\n                          {fullscreenEditor === 'body' ? '退出全屏' : '全屏'}\n                        </Button>\n                      </div>\n                      <div\n                        id=\"fullscreen-body\"\n                        className={`${fullscreenEditor === 'body' ? 'h-full' : 'flex-1'} border border-gray-300 rounded-md overflow-hidden relative`}\n                      >\n                        <MonacoEditor\n                          height=\"100%\"\n                          language=\"json\"\n                          value={requestData.body}\n                          onChange={(value) => setRequestData(prev => ({ ...prev, body: value || '{}' }))}\n                          onMount={(editor) => {\n                            bodyEditorRef.current = editor;\n                          }}\n                          options={{\n                            minimap: { enabled: fullscreenEditor === 'body' },\n                            scrollBeyondLastLine: false,\n                            fontSize: 14,\n                            lineNumbers: 'on',\n                            wordWrap: 'on',\n                            automaticLayout: true,\n                            formatOnPaste: true,\n                            formatOnType: true,\n                          }}\n                        />\n                      </div>\n                    </div>\n                  </TabsContent>\n                </Tabs>\n              </div>\n            </div>\n          </div>\n        </div>\n\n        {/* 下部分：响应信息查看 */}\n        <div className=\"h-1/2 flex flex-col gap-4\">\n          <div className=\"flex-1 bg-white rounded-lg border p-4 flex flex-col\">\n            <div className=\"flex items-center justify-between mb-4\">\n              <h3 className=\"font-medium\">响应信息</h3>\n              {responseData.status > 0 && (\n                <div className=\"flex items-center gap-4 text-sm\">\n                  <span className=\"flex items-center gap-1\">\n                    状态码: <span className={`font-mono px-2 py-1 rounded ${\n                      responseData.status >= 200 && responseData.status < 300 \n                        ? 'bg-green-100 text-green-800' \n                        : responseData.status >= 400 \n                        ? 'bg-red-100 text-red-800' \n                        : 'bg-yellow-100 text-yellow-800'\n                    }`}>\n                      {responseData.status}\n                    </span>\n                  </span>\n                  <span>\n                    响应时间: <span className=\"font-mono\">{responseData.responseTime}ms</span>\n                  </span>\n                </div>\n              )}\n            </div>\n\n            {responseData.body ? (\n              <div className=\"flex-1\">\n                <Tabs defaultValue=\"body\" className=\"h-full flex flex-col\">\n                  <TabsList className=\"grid w-full grid-cols-2\">\n                    <TabsTrigger value=\"body\">响应体</TabsTrigger>\n                    <TabsTrigger value=\"headers\">响应头</TabsTrigger>\n                  </TabsList>\n\n                  <TabsContent value=\"body\" className=\"flex-1 mt-2\">\n                    <div className=\"bg-gray-50 border rounded-md p-3 h-full overflow-auto\">\n                      <pre className=\"text-sm whitespace-pre-wrap\">\n                        {responseData.body}\n                      </pre>\n                    </div>\n                  </TabsContent>\n\n                  <TabsContent value=\"headers\" className=\"flex-1 mt-2\">\n                    <div className=\"bg-gray-50 border rounded-md p-3 h-full overflow-auto\">\n                      <pre className=\"text-sm\">\n                        {responseData.headers}\n                      </pre>\n                    </div>\n                  </TabsContent>\n                </Tabs>\n              </div>\n            ) : (\n              <div className=\"flex-1 flex items-center justify-center text-gray-500\">\n                {isLoading ? '发送请求中...' : '发送请求后将在此显示响应'}\n              </div>\n            )}\n          </div>\n        </div>\n      </main>\n\n      {/* 请求历史抽屉 */}\n      <RequestHistoryDrawer\n        isOpen={isHistoryDrawerOpen}\n        onClose={() => setIsHistoryDrawerOpen(false)}\n        onSelectRequest={handleSelectRequest}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/ui/src/components/JsonEditor.tsx",
    "content": "import { useState, useEffect, useRef } from 'react';\nimport Editor from '@monaco-editor/react';\nimport { Button } from '@/components/ui/button';\nimport { useConfig } from '@/components/ConfigProvider';\nimport { api } from '@/lib/api';\nimport { useTranslation } from 'react-i18next';\nimport { Save, X, RefreshCw } from 'lucide-react';\n\ninterface JsonEditorProps {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n  showToast?: (message: string, type: 'success' | 'error' | 'warning') => void;\n}\n\nexport function JsonEditor({ open, onOpenChange, showToast }: JsonEditorProps) {\n  const { t } = useTranslation();\n  const { config } = useConfig();\n  const [jsonValue, setJsonValue] = useState<string>('');\n  const [isSaving, setIsSaving] = useState(false);\n  const [isVisible, setIsVisible] = useState(false);\n  const [isAnimating, setIsAnimating] = useState(false);\n  const containerRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    if (config && open) {\n      setJsonValue(JSON.stringify(config, null, 2));\n    }\n  }, [config, open]);\n\n  // Handle open/close animations\n  useEffect(() => {\n    if (open) {\n      setIsVisible(true);\n      // Trigger the animation after a small delay to ensure the element is rendered\n      requestAnimationFrame(() => {\n        setIsAnimating(true);\n      });\n    } else {\n      setIsAnimating(false);\n      // Wait for the animation to complete before hiding\n      const timer = setTimeout(() => {\n        setIsVisible(false);\n      }, 300);\n      return () => clearTimeout(timer);\n    }\n  }, [open]);\n\n  const handleSaveResponse = (response: unknown, successMessage: string, errorMessage: string) => {\n    // 根据响应信息进行提示\n    if (response && typeof response === 'object' && 'success' in response) {\n      const apiResponse = response as { success: boolean; message?: string };\n      if (apiResponse.success) {\n        if (showToast) {\n          showToast(apiResponse.message || successMessage, 'success');\n        }\n        return true;\n      } else {\n        if (showToast) {\n          showToast(apiResponse.message || errorMessage, 'error');\n        }\n        return false;\n      }\n    } else {\n      // 默认成功提示\n      if (showToast) {\n        showToast(successMessage, 'success');\n      }\n      return true;\n    }\n  };\n\n  const handleSave = async () => {\n    if (!jsonValue) return;\n    \n    try {\n      setIsSaving(true);\n      const parsedConfig = JSON.parse(jsonValue);\n      const response = await api.updateConfig(parsedConfig);\n      \n      const success = handleSaveResponse(\n        response,\n        t('app.config_saved_success'),\n        t('app.config_saved_failed')\n      );\n      \n      if (success) {\n        onOpenChange(false);\n      }\n    } catch (error) {\n      console.error('Failed to save config:', error);\n      if (showToast) {\n        showToast(t('app.config_saved_failed') + ': ' + (error as Error).message, 'error');\n      }\n    } finally {\n      setIsSaving(false);\n    }\n  };\n\n  const handleSaveAndRestart = async () => {\n    if (!jsonValue) return;\n    \n    try {\n      setIsSaving(true);\n      const parsedConfig = JSON.parse(jsonValue);\n      \n      // Save config first\n      const saveResponse = await api.updateConfig(parsedConfig);\n      const saveSuccessful = handleSaveResponse(\n        saveResponse,\n        t('app.config_saved_success'),\n        t('app.config_saved_failed')\n      );\n      \n      // Only restart if save was successful\n      if (saveSuccessful) {\n        // Restart service\n        const restartResponse = await api.restartService();\n        \n        handleSaveResponse(\n          restartResponse,\n          t('app.config_saved_restart_success'),\n          t('app.config_saved_restart_failed')\n        );\n        \n        onOpenChange(false);\n      }\n    } catch (error) {\n      console.error('Failed to save config and restart:', error);\n      if (showToast) {\n        showToast(t('app.config_saved_restart_failed') + ': ' + (error as Error).message, 'error');\n      }\n    } finally {\n      setIsSaving(false);\n    }\n  };\n\n  if (!isVisible && !open) {\n    return null;\n  }\n\n  return (\n    <>\n      {(isVisible || open) && (\n        <div \n          className={`fixed inset-0 z-50 transition-all duration-300 ease-out ${\n            isAnimating && open ? 'bg-black/50 opacity-100' : 'bg-black/0 opacity-0 pointer-events-none'\n          }`}\n          onClick={() => onOpenChange(false)}\n        />\n      )}\n      \n      <div \n        ref={containerRef}\n        className={`fixed bottom-0 left-0 right-0 z-50 flex flex-col bg-white shadow-2xl transition-all duration-300 ease-out transform ${\n          isAnimating && open ? 'translate-y-0' : 'translate-y-full'\n        }`}\n        style={{ \n          height: '100vh',\n          maxHeight: '100vh'\n        }}\n      >\n        <div className=\"flex items-center justify-between border-b p-4\">\n          <h2 className=\"text-lg font-semibold\">{t('json_editor.title')}</h2>\n          <div className=\"flex gap-2\">\n            <Button\n              variant=\"outline\"\n              size=\"sm\"\n              onClick={() => onOpenChange(false)}\n              disabled={isSaving}\n            >\n              <X className=\"h-4 w-4 mr-2\" />\n              {t('json_editor.cancel')}\n            </Button>\n            <Button\n              variant=\"outline\"\n              size=\"sm\"\n              onClick={handleSave}\n              disabled={isSaving}\n            >\n              <Save className=\"h-4 w-4 mr-2\" />\n              {isSaving ? t('json_editor.saving') : t('json_editor.save')}\n            </Button>\n            <Button\n              variant=\"default\"\n              size=\"sm\"\n              onClick={handleSaveAndRestart}\n              disabled={isSaving}\n            >\n              <RefreshCw className=\"h-4 w-4 mr-2\" />\n              {isSaving ? t('json_editor.saving') : t('json_editor.save_and_restart')}\n            </Button>\n          </div>\n        </div>\n        \n        <div className=\"flex-1 min-h-0 bg-gray-50\">\n          <Editor\n            height=\"100%\"\n            defaultLanguage=\"json\"\n            value={jsonValue}\n            onChange={(value) => setJsonValue(value || '')}\n            theme=\"vs\"\n            options={{\n              minimap: { enabled: true },\n              fontSize: 14,\n              scrollBeyondLastLine: false,\n              automaticLayout: true,\n              wordWrap: 'on',\n              formatOnPaste: true,\n              formatOnType: true,\n              suggest: {\n                showKeywords: true,\n                showSnippets: true,\n              },\n            }}\n          />\n        </div>\n      </div>\n    </>\n  );\n}"
  },
  {
    "path": "packages/ui/src/components/LogViewer.tsx",
    "content": "import React, { useState, useEffect, useRef } from 'react';\nimport { useNavigate } from 'react-router-dom';\nimport Editor from '@monaco-editor/react';\nimport { Button } from '@/components/ui/button';\nimport { api } from '@/lib/api';\nimport { useTranslation } from 'react-i18next';\nimport { X, RefreshCw, Download, Trash2, ArrowLeft, File, Layers, Bug } from 'lucide-react';\n\ninterface LogViewerProps {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n  showToast?: (message: string, type: 'success' | 'error' | 'warning') => void;\n}\n\ninterface LogEntry {\n  timestamp: string;\n  level: 'info' | 'warn' | 'error' | 'debug';\n  message: string; // 现在这个字段直接包含原始JSON字符串\n  source?: string;\n  reqId?: string;\n  [key: string]: any; // 允许动态属性，如msg、url、body等\n}\n\ninterface LogFile {\n  name: string;\n  path: string;\n  size: number;\n  lastModified: string;\n}\n\ninterface GroupedLogs {\n  [reqId: string]: LogEntry[];\n}\n\ninterface LogGroupSummary {\n  reqId: string;\n  logCount: number;\n  firstLog: string;\n  lastLog: string;\n  model?: string;\n}\n\ninterface GroupedLogsResponse {\n  grouped: boolean;\n  groups: { [reqId: string]: LogEntry[] };\n  summary: {\n    totalRequests: number;\n    totalLogs: number;\n    requests: LogGroupSummary[];\n  };\n}\n\nexport function LogViewer({ open, onOpenChange, showToast }: LogViewerProps) {\n  const { t } = useTranslation();\n  const navigate = useNavigate();\n  const [logs, setLogs] = useState<string[]>([]);\n  const [logFiles, setLogFiles] = useState<LogFile[]>([]);\n  const [selectedFile, setSelectedFile] = useState<LogFile | null>(null);\n  const [isLoading, setIsLoading] = useState(false);\n  const [isVisible, setIsVisible] = useState(false);\n  const [isAnimating, setIsAnimating] = useState(false);\n  const [autoRefresh, setAutoRefresh] = useState(false);\n  const [groupByReqId, setGroupByReqId] = useState(false);\n  const [groupedLogs, setGroupedLogs] = useState<GroupedLogsResponse | null>(null);\n  const [selectedReqId, setSelectedReqId] = useState<string | null>(null);\n  const containerRef = useRef<HTMLDivElement>(null);\n  const refreshInterval = useRef<NodeJS.Timeout | null>(null);\n  const workerRef = useRef<Worker | null>(null);\n  const editorRef = useRef<any>(null);\n\n  useEffect(() => {\n    if (open) {\n      loadLogFiles();\n    }\n  }, [open]);\n\n  // 创建内联 Web Worker\n  const createInlineWorker = (): Worker => {\n    const workerCode = `\n      // 日志聚合Web Worker\n      self.onmessage = function(event) {\n        const { type, data } = event.data;\n        \n        if (type === 'groupLogsByReqId') {\n          try {\n            const { logs } = data;\n            \n            // 按reqId聚合日志\n            const groupedLogs = {};\n            \n            logs.forEach((log, index) => {\n              log = JSON.parse(log);\n              let reqId = log.reqId || 'no-req-id';\n              \n              if (!groupedLogs[reqId]) {\n                groupedLogs[reqId] = [];\n              }\n              groupedLogs[reqId].push(log);\n            });\n\n            // 按时间戳排序每个组的日志\n            Object.keys(groupedLogs).forEach(reqId => {\n              groupedLogs[reqId].sort((a, b) => a.time - b.time);\n            });\n\n            // 提取model信息\n            const extractModelInfo = (reqId) => {\n              const logGroup = groupedLogs[reqId];\n              for (const log of logGroup) {\n                try {\n                  // 尝试从message字段解析JSON\n                  if (log.type === 'request body' && log.data && log.data.model) {\n                    return log.data.model;\n                  }\n                } catch (e) {\n                  // 解析失败，继续尝试下一条日志\n                }\n              }\n              return undefined;\n            };\n\n            // 生成摘要信息\n            const summary = {\n              totalRequests: Object.keys(groupedLogs).length,\n              totalLogs: logs.length,\n              requests: Object.keys(groupedLogs).map(reqId => ({\n                reqId,\n                logCount: groupedLogs[reqId].length,\n                firstLog: groupedLogs[reqId][0]?.time,\n                lastLog: groupedLogs[reqId][groupedLogs[reqId].length - 1]?.time,\n                model: extractModelInfo(reqId)\n              }))\n            };\n\n            const response = {\n              grouped: true,\n              groups: groupedLogs,\n              summary\n            };\n\n            // 发送结果回主线程\n            self.postMessage({\n              type: 'groupLogsResult',\n              data: response\n            });\n          } catch (error) {\n            // 发送错误回主线程\n            self.postMessage({\n              type: 'error',\n              error: error instanceof Error ? error.message : 'Unknown error occurred'\n            });\n          }\n        }\n      };\n    `;\n\n    const blob = new Blob([workerCode], { type: 'application/javascript' });\n    const workerUrl = URL.createObjectURL(blob);\n    return new Worker(workerUrl);\n  };\n\n  // 初始化Web Worker\n  useEffect(() => {\n    if (typeof Worker !== 'undefined') {\n      try {\n        // 创建内联Web Worker\n        workerRef.current = createInlineWorker();\n\n        // 监听Worker消息\n        workerRef.current.onmessage = (event) => {\n          const { type, data, error } = event.data;\n\n          if (type === 'groupLogsResult') {\n            setGroupedLogs(data);\n          } else if (type === 'error') {\n            console.error('Worker error:', error);\n            if (showToast) {\n              showToast(t('log_viewer.worker_error') + ': ' + error, 'error');\n            }\n          }\n        };\n\n        // 监听Worker错误\n        workerRef.current.onerror = (error) => {\n          console.error('Worker error:', error);\n          if (showToast) {\n            showToast(t('log_viewer.worker_init_failed'), 'error');\n          }\n        };\n      } catch (error) {\n        console.error('Failed to create worker:', error);\n        if (showToast) {\n          showToast(t('log_viewer.worker_init_failed'), 'error');\n        }\n      }\n    }\n\n    // 清理Worker\n    return () => {\n      if (workerRef.current) {\n        workerRef.current.terminate();\n        workerRef.current = null;\n      }\n    };\n  }, [showToast, t]);\n\n  useEffect(() => {\n    if (autoRefresh && open && selectedFile) {\n      refreshInterval.current = setInterval(() => {\n        loadLogs();\n      }, 5000); // Refresh every 5 seconds\n    } else if (refreshInterval.current) {\n      clearInterval(refreshInterval.current);\n    }\n\n    return () => {\n      if (refreshInterval.current) {\n        clearInterval(refreshInterval.current);\n      }\n    };\n  }, [autoRefresh, open, selectedFile]);\n\n  // Load logs when selected file changes\n  useEffect(() => {\n    if (selectedFile && open) {\n      setLogs([]); // Clear existing logs\n      loadLogs();\n    }\n  }, [selectedFile, open]);\n\n  // Handle open/close animations\n  useEffect(() => {\n    if (open) {\n      setIsVisible(true);\n      // Trigger the animation after a small delay to ensure the element is rendered\n      requestAnimationFrame(() => {\n        setIsAnimating(true);\n      });\n    } else {\n      setIsAnimating(false);\n      // Wait for the animation to complete before hiding\n      const timer = setTimeout(() => {\n        setIsVisible(false);\n      }, 300);\n      return () => clearTimeout(timer);\n    }\n  }, [open]);\n\n  const loadLogFiles = async () => {\n    try {\n      setIsLoading(true);\n      const response = await api.getLogFiles();\n\n      if (response && Array.isArray(response)) {\n        setLogFiles(response);\n        setSelectedFile(null);\n        setLogs([]);\n      } else {\n        setLogFiles([]);\n        if (showToast) {\n          showToast(t('log_viewer.no_log_files_available'), 'warning');\n        }\n      }\n    } catch (error) {\n      console.error('Failed to load log files:', error);\n      if (showToast) {\n        showToast(t('log_viewer.load_files_failed') + ': ' + (error as Error).message, 'error');\n      }\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  const loadLogs = async () => {\n    if (!selectedFile) return;\n\n    try {\n      setIsLoading(true);\n      setGroupedLogs(null);\n      setSelectedReqId(null);\n\n      // 始终加载原始日志数据\n      const response = await api.getLogs(selectedFile.path);\n\n      if (response && Array.isArray(response)) {\n        // 现在接口返回的是原始日志字符串数组，直接存储\n        setLogs(response);\n\n        // 如果启用了分组，使用Web Worker进行聚合（需要转换为LogEntry格式供Worker使用）\n        if (groupByReqId && workerRef.current) {\n          // const workerLogs: LogEntry[] = response.map((logLine, index) => ({\n          //   timestamp: new Date().toISOString(),\n          //   level: 'info',\n          //   message: logLine,\n          //   source: undefined,\n          //   reqId: undefined\n          // }));\n\n          workerRef.current.postMessage({\n            type: 'groupLogsByReqId',\n            data: { logs: response }\n          });\n        } else {\n          setGroupedLogs(null);\n        }\n      } else {\n        setLogs([]);\n        setGroupedLogs(null);\n        if (showToast) {\n          showToast(t('log_viewer.no_logs_available'), 'warning');\n        }\n      }\n    } catch (error) {\n      console.error('Failed to load logs:', error);\n      if (showToast) {\n        showToast(t('log_viewer.load_failed') + ': ' + (error as Error).message, 'error');\n      }\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  const clearLogs = async () => {\n    if (!selectedFile) return;\n\n    try {\n      await api.clearLogs(selectedFile.path);\n      setLogs([]);\n      if (showToast) {\n        showToast(t('log_viewer.logs_cleared'), 'success');\n      }\n    } catch (error) {\n      console.error('Failed to clear logs:', error);\n      if (showToast) {\n        showToast(t('log_viewer.clear_failed') + ': ' + (error as Error).message, 'error');\n      }\n    }\n  };\n\n  const selectFile = (file: LogFile) => {\n    setSelectedFile(file);\n    setAutoRefresh(false); // Reset auto refresh when changing files\n  };\n\n\n  const toggleGroupByReqId = () => {\n    const newValue = !groupByReqId;\n    setGroupByReqId(newValue);\n\n    if (newValue && selectedFile && logs.length > 0) {\n      // 启用聚合时，如果已有日志，则使用Worker进行聚合\n      if (workerRef.current) {\n        workerRef.current.postMessage({\n          type: 'groupLogsByReqId',\n          data: { logs }\n        });\n      }\n    } else if (!newValue) {\n      // 禁用聚合时，清除聚合结果\n      setGroupedLogs(null);\n      setSelectedReqId(null);\n    }\n  };\n\n  const selectReqId = (reqId: string) => {\n    setSelectedReqId(reqId);\n  };\n\n\n  const getDisplayLogs = () => {\n    if (groupByReqId && groupedLogs) {\n      if (selectedReqId && groupedLogs.groups[selectedReqId]) {\n        return groupedLogs.groups[selectedReqId];\n      }\n      // 当在分组模式但没有选中具体请求时，显示原始日志字符串数组\n      return logs.map(logLine => ({\n        timestamp: new Date().toISOString(),\n        level: 'info',\n        message: logLine,\n        source: undefined,\n        reqId: undefined\n      }));\n    }\n    // 当不在分组模式时，显示原始日志字符串数组\n    return logs.map(logLine => ({\n      timestamp: new Date().toISOString(),\n      level: 'info',\n      message: logLine,\n      source: undefined,\n      reqId: undefined\n    }));\n  };\n\n  const downloadLogs = () => {\n    if (!selectedFile || logs.length === 0) return;\n\n    // 直接下载原始日志字符串，每行一个日志\n    const logText = logs.join('\\n');\n\n    const blob = new Blob([logText], { type: 'text/plain' });\n    const url = URL.createObjectURL(blob);\n    const a = document.createElement('a');\n    a.href = url;\n    a.download = `${selectedFile.name}-${new Date().toISOString().split('T')[0]}.txt`;\n    document.body.appendChild(a);\n    a.click();\n    document.body.removeChild(a);\n    URL.revokeObjectURL(url);\n\n    if (showToast) {\n      showToast(t('log_viewer.logs_downloaded'), 'success');\n    }\n  };\n\n  const formatFileSize = (bytes: number) => {\n    if (bytes === 0) return '0 Bytes';\n    const k = 1024;\n    const sizes = ['Bytes', 'KB', 'MB', 'GB'];\n    const i = Math.floor(Math.log(bytes) / Math.log(k));\n    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];\n  };\n\n  const formatDate = (dateString: string) => {\n    return new Date(dateString).toLocaleString();\n  };\n\n  // 面包屑导航项类型\n  interface BreadcrumbItem {\n    id: string;\n    label: string;\n    onClick: () => void;\n  }\n\n  // 获取面包屑导航项\n  const getBreadcrumbs = (): BreadcrumbItem[] => {\n    const breadcrumbs: BreadcrumbItem[] = [\n      {\n        id: 'root',\n        label: t('log_viewer.title'),\n        onClick: () => {\n          setSelectedFile(null);\n          setAutoRefresh(false);\n          setLogs([]);\n          setGroupedLogs(null);\n          setSelectedReqId(null);\n          setGroupByReqId(false);\n        }\n      }\n    ];\n\n    if (selectedFile) {\n      breadcrumbs.push({\n        id: 'file',\n        label: selectedFile.name,\n        onClick: () => {\n          if (groupByReqId) {\n            // 如果在分组模式下，点击文件层级应该返回到分组列表\n            setSelectedReqId(null);\n          } else {\n            // 如果不在分组模式下，点击文件层级关闭分组功能\n            setSelectedReqId(null);\n            setGroupedLogs(null);\n            setGroupByReqId(false);\n          }\n        }\n      });\n    }\n\n    if (selectedReqId) {\n      breadcrumbs.push({\n        id: 'req',\n        label: `${t('log_viewer.request')} ${selectedReqId}`,\n        onClick: () => {\n          // 点击当前层级时不做任何操作\n        }\n      });\n    }\n\n    return breadcrumbs;\n  };\n\n  // 获取返回按钮的处理函数\n  const getBackAction = (): (() => void) | null => {\n    if (selectedReqId) {\n      return () => {\n        setSelectedReqId(null);\n      };\n    } else if (selectedFile) {\n      return () => {\n        setSelectedFile(null);\n        setAutoRefresh(false);\n        setLogs([]);\n        setGroupedLogs(null);\n        setSelectedReqId(null);\n        setGroupByReqId(false);\n      };\n    }\n    return null;\n  };\n\n  const formatLogsForEditor = () => {\n    // 如果在分组模式且选中了具体请求，显示该请求的日志\n    if (groupByReqId && groupedLogs && selectedReqId && groupedLogs.groups[selectedReqId]) {\n      const requestLogs = groupedLogs.groups[selectedReqId];\n      // 提取原始JSON字符串并每行一个\n      return requestLogs.map(log => JSON.stringify(log)).join('\\n');\n    }\n\n    // 其他情况，直接显示原始日志字符串数组，每行一个\n    return logs.join('\\n');\n  };\n\n  // 解析日志行，获取final request的行号\n  const getFinalRequestLines = () => {\n    const lines: number[] = [];\n\n    if (groupByReqId && groupedLogs && selectedReqId && groupedLogs.groups[selectedReqId]) {\n      // 分组模式下，检查选中的请求日志\n      const requestLogs = groupedLogs.groups[selectedReqId];\n      requestLogs.forEach((log, index) => {\n        try {\n          // @ts-ignore\n          log = JSON.parse(log)\n          // 检查日志的msg字段是否等于\"final request\"\n          if (log.msg === \"final request\") {\n            lines.push(index + 1); // 行号从1开始\n          }\n        } catch (e) {\n          // 解析失败，跳过\n        }\n      });\n    } else {\n      // 非分组模式下，检查原始日志\n      logs.forEach((logLine, index) => {\n        try {\n          const log = JSON.parse(logLine);\n          // 检查日志的msg字段是否等于\"final request\"\n          if (log.msg === \"final request\") {\n            lines.push(index + 1); // 行号从1开始\n          }\n        } catch (e) {\n          // 解析失败，跳过\n        }\n      });\n    }\n\n    return lines;\n  };\n\n  // 处理调试按钮点击\n  const handleDebugClick = (lineNumber: number) => {\n    console.log('handleDebugClick called with lineNumber:', lineNumber);\n    console.log('Current state:', { groupByReqId, selectedReqId, logsLength: logs.length });\n\n    let logData = null;\n\n    if (groupByReqId && groupedLogs && selectedReqId && groupedLogs.groups[selectedReqId]) {\n      // 分组模式下获取日志数据\n      const requestLogs = groupedLogs.groups[selectedReqId];\n      console.log('Group mode - requestLogs length:', requestLogs.length);\n      logData = requestLogs[lineNumber - 1]; // 行号转换为数组索引\n      console.log('Group mode - logData:', logData);\n    } else {\n      // 非分组模式下获取日志数据\n      console.log('Non-group mode - logs length:', logs.length);\n      try {\n        const logLine = logs[lineNumber - 1];\n        console.log('Log line:', logLine);\n        logData = JSON.parse(logLine);\n        console.log('Parsed logData:', logData);\n      } catch (e) {\n        console.error('Failed to parse log data:', e);\n      }\n    }\n\n    if (logData) {\n      console.log('Navigating to debug page with logData:', logData);\n      // 导航到调试页面，并传递日志数据作为URL参数\n      const logDataParam = encodeURIComponent(JSON.stringify(logData));\n      console.log('Encoded logDataParam length:', logDataParam.length);\n      navigate(`/debug?logData=${logDataParam}`);\n    } else {\n      console.error('No log data found for line:', lineNumber);\n    }\n  };\n\n  // 配置Monaco Editor\n  const configureEditor = (editor: any) => {\n    editorRef.current = editor;\n\n    // 启用glyph margin\n    editor.updateOptions({\n      glyphMargin: true,\n    });\n\n    // 存储当前的装饰ID\n    let currentDecorations: string[] = [];\n\n    // 添加glyph margin装饰\n    const updateDecorations = () => {\n      const finalRequestLines = getFinalRequestLines();\n      const decorations = finalRequestLines.map(lineNumber => ({\n        range: {\n          startLineNumber: lineNumber,\n          startColumn: 1,\n          endLineNumber: lineNumber,\n          endColumn: 1\n        },\n        options: {\n          glyphMarginClassName: 'debug-button-glyph',\n          glyphMarginHoverMessage: { value: '点击调试此请求' }\n        }\n      }));\n\n      // 使用deltaDecorations正确更新装饰，清理旧的装饰\n      currentDecorations = editor.deltaDecorations(currentDecorations, decorations);\n    };\n\n    // 初始更新装饰\n    updateDecorations();\n\n    // 监听glyph margin点击 - 使用正确的事件监听方式\n    editor.onMouseDown((e: any) => {\n      console.log('Mouse down event:', e.target);\n      console.log('Event details:', {\n        type: e.target.type,\n        hasDetail: !!e.target.detail,\n        glyphMarginLane: e.target.detail?.glyphMarginLane,\n        offsetX: e.target.detail?.offsetX,\n        glyphMarginLeft: e.target.detail?.glyphMarginLeft,\n        glyphMarginWidth: e.target.detail?.glyphMarginWidth\n      });\n\n      // 检查是否点击在glyph margin区域\n      const isGlyphMarginClick = e.target.detail &&\n        e.target.detail.glyphMarginLane !== undefined &&\n        e.target.detail.offsetX !== undefined &&\n        e.target.detail.offsetX <= e.target.detail.glyphMarginLeft + e.target.detail.glyphMarginWidth;\n\n      console.log('Is glyph margin click:', isGlyphMarginClick);\n\n      if (e.target.position && isGlyphMarginClick) {\n        const finalRequestLines = getFinalRequestLines();\n        console.log('Final request lines:', finalRequestLines);\n        console.log('Clicked line number:', e.target.position.lineNumber);\n        if (finalRequestLines.includes(e.target.position.lineNumber)) {\n          console.log('Opening debug page for line:', e.target.position.lineNumber);\n          handleDebugClick(e.target.position.lineNumber);\n        }\n      }\n    });\n\n    // 尝试使用 onGlyphMarginClick 如果可用\n    if (typeof editor.onGlyphMarginClick === 'function') {\n      editor.onGlyphMarginClick((e: any) => {\n        console.log('Glyph margin click event:', e);\n        const finalRequestLines = getFinalRequestLines();\n        if (finalRequestLines.includes(e.target.position.lineNumber)) {\n          console.log('Opening debug page for line (glyph):', e.target.position.lineNumber);\n          handleDebugClick(e.target.position.lineNumber);\n        }\n      });\n    }\n\n    // 添加鼠标移动事件来检测悬停在调试按钮上\n    editor.onMouseMove((e: any) => {\n      if (e.target.position && (e.target.type === 4 || e.target.type === 'glyph-margin')) {\n        const finalRequestLines = getFinalRequestLines();\n        if (finalRequestLines.includes(e.target.position.lineNumber)) {\n          // 可以在这里添加悬停效果\n          editor.updateOptions({\n            glyphMargin: true,\n          });\n        }\n      }\n    });\n\n    // 当日志变化时更新装饰\n    const interval = setInterval(updateDecorations, 1000);\n\n    return () => {\n      clearInterval(interval);\n      // 清理装饰\n      if (editorRef.current) {\n        editorRef.current.deltaDecorations(currentDecorations, []);\n      }\n    };\n  };\n\n  if (!isVisible && !open) {\n    return null;\n  }\n\n  return (\n    <>\n      {(isVisible || open) && (\n        <div\n          className={`fixed inset-0 z-50 transition-all duration-300 ease-out ${\n            isAnimating && open ? 'bg-black/50 opacity-100' : 'bg-black/0 opacity-0 pointer-events-none'\n          }`}\n          onClick={() => onOpenChange(false)}\n        />\n      )}\n\n      <div\n        ref={containerRef}\n        className={`fixed bottom-0 left-0 right-0 z-50 flex flex-col bg-white shadow-2xl transition-all duration-300 ease-out transform ${\n          isAnimating && open ? 'translate-y-0' : 'translate-y-full'\n        }`}\n        style={{\n          height: '100vh',\n          maxHeight: '100vh'\n        }}\n      >\n        <div className=\"flex items-center justify-between border-b p-4\">\n          <div className=\"flex items-center gap-2\">\n            {getBackAction() && (\n              <Button\n                variant=\"ghost\"\n                size=\"sm\"\n                onClick={getBackAction()!}\n              >\n                <ArrowLeft className=\"h-4 w-4 mr-2\" />\n                {t('log_viewer.back')}\n              </Button>\n            )}\n\n            {/* 面包屑导航 */}\n            <nav className=\"flex items-center space-x-1 text-sm\">\n              {getBreadcrumbs().map((breadcrumb, index) => (\n                <React.Fragment key={breadcrumb.id}>\n                  {index > 0 && (\n                    <span className=\"text-gray-400 mx-1\">/</span>\n                  )}\n                  {index === getBreadcrumbs().length - 1 ? (\n                    <span className=\"text-gray-900 font-medium\">\n                      {breadcrumb.label}\n                    </span>\n                  ) : (\n                    <button\n                      onClick={breadcrumb.onClick}\n                      className=\"text-blue-600 hover:text-blue-800 transition-colors\"\n                    >\n                      {breadcrumb.label}\n                    </button>\n                  )}\n                </React.Fragment>\n              ))}\n            </nav>\n          </div>\n          <div className=\"flex gap-2\">\n            {selectedFile && (\n              <>\n                <Button\n                  variant=\"ghost\"\n                  size=\"sm\"\n                  onClick={toggleGroupByReqId}\n                  className={groupByReqId ? 'bg-blue-100 text-blue-700' : ''}\n                >\n                  <Layers className=\"h-4 w-4 mr-2\" />\n                  {groupByReqId ? t('log_viewer.grouped_on') : t('log_viewer.group_by_req_id')}\n                </Button>\n                <Button\n                  variant=\"ghost\"\n                  size=\"sm\"\n                  onClick={() => setAutoRefresh(!autoRefresh)}\n                  className={autoRefresh ? 'bg-blue-100 text-blue-700' : ''}\n                >\n                  <RefreshCw className={`h-4 w-4 mr-2 ${autoRefresh ? 'animate-spin' : ''}`} />\n                  {autoRefresh ? t('log_viewer.auto_refresh_on') : t('log_viewer.auto_refresh_off')}\n                </Button>\n                <Button\n                  variant=\"outline\"\n                  size=\"sm\"\n                  onClick={downloadLogs}\n                  disabled={logs.length === 0}\n                >\n                  <Download className=\"h-4 w-4 mr-2\" />\n                  {t('log_viewer.download')}\n                </Button>\n                <Button\n                  variant=\"outline\"\n                  size=\"sm\"\n                  onClick={clearLogs}\n                  disabled={logs.length === 0}\n                >\n                  <Trash2 className=\"h-4 w-4 mr-2\" />\n                  {t('log_viewer.clear')}\n                </Button>\n              </>\n            )}\n            <Button\n              variant=\"outline\"\n              size=\"sm\"\n              onClick={() => onOpenChange(false)}\n            >\n              <X className=\"h-4 w-4 mr-2\" />\n              {t('log_viewer.close')}\n            </Button>\n          </div>\n        </div>\n\n        <div className=\"flex-1 min-h-0 bg-gray-50\">\n          {isLoading ? (\n            <div className=\"flex items-center justify-center h-full\">\n              <div className=\"animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600\"></div>\n            </div>\n          ) : selectedFile ? (\n            <>\n              {groupByReqId && groupedLogs && !selectedReqId ? (\n                // 显示日志组列表\n                <div className=\"flex flex-col h-full p-6\">\n                  <div className=\"mb-4 flex-shrink-0\">\n                    <h3 className=\"text-lg font-medium mb-2\">{t('log_viewer.request_groups')}</h3>\n                    <p className=\"text-sm text-gray-600\">\n                      {t('log_viewer.total_requests')}: {groupedLogs.summary.totalRequests} |\n                      {t('log_viewer.total_logs')}: {groupedLogs.summary.totalLogs}\n                    </p>\n                  </div>\n                  <div className=\"flex-1 min-h-0 overflow-y-auto space-y-3\">\n                    {groupedLogs.summary.requests.map((request) => (\n                      <div\n                        key={request.reqId}\n                        className=\"border rounded-lg p-4 hover:bg-gray-50 cursor-pointer transition-colors\"\n                        onClick={() => selectReqId(request.reqId)}\n                      >\n                        <div className=\"flex items-center justify-between mb-2\">\n                          <div className=\"flex items-center gap-2\">\n                            <File className=\"h-5 w-5 text-blue-600\" />\n                            <span className=\"font-medium text-sm\">{request.reqId}</span>\n                            {request.model && (\n                              <span className=\"text-xs bg-green-100 text-green-800 px-2 py-1 rounded\">\n                                {request.model}\n                              </span>\n                            )}\n                          </div>\n                          <span className=\"text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded\">\n                            {request.logCount} {t('log_viewer.logs')}\n                          </span>\n                        </div>\n                        <div className=\"text-xs text-gray-500 space-y-1\">\n                          <div>{t('log_viewer.first_log')}: {formatDate(request.firstLog)}</div>\n                          <div>{t('log_viewer.last_log')}: {formatDate(request.lastLog)}</div>\n                        </div>\n                      </div>\n                    ))}\n                  </div>\n                </div>\n              ) : (\n                // 显示日志内容\n                <div className=\"relative h-full\">\n                  <Editor\n                    height=\"100%\"\n                    defaultLanguage=\"json\"\n                    value={formatLogsForEditor()}\n                    theme=\"vs\"\n                    options={{\n                      minimap: { enabled: true },\n                      fontSize: 14,\n                      scrollBeyondLastLine: false,\n                      automaticLayout: true,\n                      wordWrap: 'on',\n                      readOnly: true,\n                      lineNumbers: 'on',\n                      folding: true,\n                      renderWhitespace: 'all',\n                      glyphMargin: true,\n                    }}\n                    onMount={configureEditor}\n                  />\n                </div>\n              )}\n            </>\n          ) : (\n            <div className=\"p-6\">\n              <h3 className=\"text-lg font-medium mb-4\">{t('log_viewer.select_file')}</h3>\n              {logFiles.length === 0 ? (\n                <div className=\"text-gray-500 text-center py-8\">\n                  <File className=\"h-12 w-12 mx-auto mb-4 text-gray-400\" />\n                  <p>{t('log_viewer.no_log_files_available')}</p>\n                </div>\n              ) : (\n                <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4\">\n                  {logFiles.map((file) => (\n                    <div\n                      key={file.path}\n                      className=\"border rounded-lg p-4 hover:bg-gray-50 cursor-pointer transition-colors\"\n                      onClick={() => selectFile(file)}\n                    >\n                      <div className=\"flex items-start justify-between mb-2\">\n                        <div className=\"flex items-center gap-2\">\n                          <File className=\"h-5 w-5 text-blue-600\" />\n                          <span className=\"font-medium text-sm\">{file.name}</span>\n                        </div>\n                      </div>\n                      <div className=\"text-xs text-gray-500 space-y-1\">\n                        <div>{formatFileSize(file.size)}</div>\n                        <div>{formatDate(file.lastModified)}</div>\n                      </div>\n                    </div>\n                  ))}\n                </div>\n              )}\n            </div>\n          )}\n        </div>\n      </div>\n    </>\n  );\n}\n"
  },
  {
    "path": "packages/ui/src/components/Login.tsx",
    "content": "import { useState, useEffect } from 'react';\nimport { useNavigate } from 'react-router-dom';\nimport { useTranslation } from 'react-i18next';\nimport { Button } from '@/components/ui/button';\nimport { Input } from '@/components/ui/input';\nimport { Label } from '@/components/ui/label';\nimport { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';\nimport { api } from '@/lib/api';\n\nexport function Login() {\n  const { t } = useTranslation();\n  const navigate = useNavigate();\n  const [apiKey, setApiKey] = useState('');\n  const [error, setError] = useState('');\n  const [isLoading, setIsLoading] = useState(false);\n\n  // Check if user is already authenticated\n  useEffect(() => {\n    const checkAuth = async () => {\n      const apiKey = localStorage.getItem('apiKey');\n      if (apiKey) {\n        setIsLoading(true);\n        // Verify the API key is still valid\n        try {\n          await api.getConfig();\n          navigate('/dashboard');\n        } catch {\n          // If verification fails, remove the API key\n          localStorage.removeItem('apiKey');\n        } finally {\n          setIsLoading(false);\n        }\n      }\n    };\n\n    checkAuth();\n    \n    // Listen for unauthorized events\n    const handleUnauthorized = () => {\n      navigate('/login');\n    };\n    \n    window.addEventListener('unauthorized', handleUnauthorized);\n    \n    return () => {\n      window.removeEventListener('unauthorized', handleUnauthorized);\n    };\n  }, [navigate]);\n\n  const handleLogin = async (e: React.FormEvent) => {\n    e.preventDefault();\n    \n    try {\n      // Set the API key\n      api.setApiKey(apiKey);\n      \n      // Dispatch storage event to notify other components of the change\n      window.dispatchEvent(new StorageEvent('storage', {\n        key: 'apiKey',\n        newValue: apiKey,\n        url: window.location.href\n      }));\n      \n      // Test the API key by fetching config\n      await api.getConfig();\n      \n      // Navigate to dashboard\n      // The ConfigProvider will handle fetching the config\n      navigate('/dashboard');\n    } catch (error: any) {\n      // Clear the API key on failure\n      api.setApiKey('');\n      \n      // Check if it's an unauthorized error\n      if (error.message && error.message.includes('401')) {\n        setError(t('login.invalidApiKey'));\n      } else {\n        // For other errors, still allow access (restricted mode)\n        navigate('/dashboard');\n      }\n    }\n  };\n\n  if (isLoading) {\n    return (\n      <div className=\"flex min-h-screen items-center justify-center bg-gray-50\">\n        <Card className=\"w-full max-w-md\">\n          <CardHeader className=\"space-y-1\">\n            <CardTitle className=\"text-2xl\">{t('login.title')}</CardTitle>\n          </CardHeader>\n          <CardContent>\n            <div className=\"flex justify-center py-8\">\n              <div className=\"h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent\"></div>\n            </div>\n            <p className=\"text-center text-sm text-gray-500\">{t('login.validating')}</p>\n          </CardContent>\n        </Card>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex min-h-screen items-center justify-center bg-gray-50\">\n      <Card className=\"w-full max-w-md\">\n        <CardHeader className=\"space-y-1\">\n          <CardTitle className=\"text-2xl\">{t('login.title')}</CardTitle>\n          <CardDescription>\n            {t('login.description')}\n          </CardDescription>\n        </CardHeader>\n        <form onSubmit={handleLogin}>\n          <CardContent className=\"space-y-4\">\n            <div className=\"space-y-2\">\n              <Label htmlFor=\"apiKey\">{t('login.apiKey')}</Label>\n              <Input\n                id=\"apiKey\"\n                type=\"password\"\n                value={apiKey}\n                onChange={(e) => setApiKey(e.target.value)}\n                placeholder={t('login.apiKeyPlaceholder')}\n              />\n            </div>\n            {error && <div className=\"text-sm text-red-500\">{error}</div>}\n          </CardContent>\n          <CardFooter>\n            <Button className=\"w-full\" type=\"submit\">\n              {t('login.signIn')}\n            </Button>\n          </CardFooter>\n        </form>\n      </Card>\n    </div>\n  );\n}"
  },
  {
    "path": "packages/ui/src/components/Presets.tsx",
    "content": "import { useState, useEffect } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { useNavigate } from \"react-router-dom\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { api } from \"@/lib/api\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from \"@/components/ui/dialog\";\nimport { Upload, Link, Trash2, Info, Download, Check, CheckCircle2, AlertCircle, Loader2, ArrowLeft, Store, Search, Package } from \"lucide-react\";\nimport { Toast } from \"@/components/ui/toast\";\nimport { DynamicConfigForm } from \"./preset/DynamicConfigForm\";\n\n// Schema types\ninterface InputOption {\n  label: string;\n  value: string | number | boolean;\n  description?: string;\n  disabled?: boolean;\n}\n\ninterface DynamicOptions {\n  type: 'static' | 'providers' | 'models' | 'custom';\n  options?: InputOption[];\n  providerField?: string;\n}\n\ninterface Condition {\n  field: string;\n  operator?: 'eq' | 'ne' | 'in' | 'nin' | 'gt' | 'lt' | 'gte' | 'lte' | 'exists';\n  value?: any;\n}\n\ninterface RequiredInput {\n  id: string;\n  type?: 'password' | 'input' | 'select' | 'multiselect' | 'confirm' | 'editor' | 'number';\n  label?: string;\n  prompt?: string;\n  placeholder?: string;\n  options?: InputOption[] | DynamicOptions;\n  when?: Condition | Condition[];\n  defaultValue?: any;\n  required?: boolean;\n  validator?: RegExp | string;\n  min?: number;\n  max?: number;\n  rows?: number;\n  dependsOn?: string[];\n}\n\ninterface PresetMetadata {\n  id: string;\n  name: string;\n  version: string;\n  description?: string;\n  author?: string;\n  homepage?: string;\n  repository?: string;\n  license?: string;\n  keywords?: string[];\n  ccrVersion?: string;\n  source?: string;\n  sourceType?: 'local' | 'gist' | 'registry';\n  checksum?: string;\n  installed: boolean;\n}\n\ninterface PresetConfigSection {\n  Providers?: Array<{\n    name: string;\n    api_base_url?: string;\n    models?: string[];\n    [key: string]: any;\n  }>;\n  [key: string]: any;\n}\n\ninterface PresetDetail extends PresetMetadata {\n  config?: PresetConfigSection;\n  schema?: RequiredInput[];\n  template?: any;\n  configMappings?: any[];\n  userValues?: Record<string, any>;\n}\n\ninterface MarketPreset {\n  id: string;\n  name: string;\n  author?: string;\n  description?: string;\n  repo: string;\n}\n\nexport function Presets() {\n  const { t } = useTranslation();\n  const navigate = useNavigate();\n  const [presets, setPresets] = useState<PresetMetadata[]>([]);\n  const [loading, setLoading] = useState(true);\n  const [installDialogOpen, setInstallDialogOpen] = useState(false);\n  const [detailDialogOpen, setDetailDialogOpen] = useState(false);\n  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);\n  const [marketDialogOpen, setMarketDialogOpen] = useState(false);\n  const [selectedPreset, setSelectedPreset] = useState<PresetDetail | null>(null);\n  const [presetToDelete, setPresetToDelete] = useState<string | null>(null);\n  const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' | 'warning' } | null>(null);\n  const [installMethod, setInstallMethod] = useState<'file' | 'url'>('file');\n  const [installUrl, setInstallUrl] = useState('');\n  const [installFile, setInstallFile] = useState<File | null>(null);\n  const [installName, setInstallName] = useState('');\n  const [isInstalling, setIsInstalling] = useState(false);\n  const [secrets, setSecrets] = useState<Record<string, string>>({});\n  const [isApplying, setIsApplying] = useState(false);\n  const [marketSearch, setMarketSearch] = useState('');\n  const [marketPresets, setMarketPresets] = useState<MarketPreset[]>([]);\n  const [marketLoading, setMarketLoading] = useState(false);\n  const [installingFromMarket, setInstallingFromMarket] = useState<string | null>(null);\n\n  // 返回上一页\n  const handleGoBack = () => {\n    navigate('/dashboard');\n  };\n\n  // Load market presets\n  const loadMarketPresets = async () => {\n    setMarketLoading(true);\n    try {\n      const response = await api.getMarketPresets();\n      setMarketPresets(response.presets || []);\n    } catch (error) {\n      console.error('Failed to load market presets:', error);\n      setToast({ message: t('presets.load_market_failed'), type: 'error' });\n    } finally {\n      setMarketLoading(false);\n    }\n  };\n\n  // Install preset from market\n  const handleInstallFromMarket = async (preset: MarketPreset) => {\n    try {\n      setInstallingFromMarket(preset.id);\n\n      // Step 1: Install preset (extract to directory)\n      const installResult = await api.installPresetFromGitHub(preset.repo);\n\n      // Step 2: Get preset details (check if configuration is required)\n      try {\n        const installedPresetName = installResult.presetName || preset.name;\n        const detail = await api.getPreset(installedPresetName);\n        const presetDetail: PresetDetail = { ...preset, ...detail, id: installedPresetName };\n\n        // Check if configuration is required\n        if (detail.schema && detail.schema.length > 0) {\n          // Configuration required, open configuration dialog\n          setSelectedPreset(presetDetail);\n\n          // Initialize form values: prefer saved userValues, otherwise use defaultValue\n          const initialValues: Record<string, any> = {};\n          for (const input of detail.schema) {\n            // Prefer saved values\n            if (detail.userValues && detail.userValues[input.id] !== undefined) {\n              initialValues[input.id] = detail.userValues[input.id];\n            } else {\n              // Otherwise use default value\n              initialValues[input.id] = input.defaultValue ?? '';\n            }\n          }\n          setSecrets(initialValues);\n\n          // Close market dialog, open details dialog\n          setMarketDialogOpen(false);\n          setDetailDialogOpen(true);\n\n          setToast({ message: t('presets.preset_installed_config_required'), type: 'warning' });\n        } else {\n          // No configuration required, complete directly\n          setToast({ message: t('presets.preset_installed'), type: 'success' });\n          setMarketDialogOpen(false);\n          await loadPresets();\n        }\n      } catch (error) {\n        // Failed to get details, but installation succeeded, refresh list\n        console.error('Failed to get preset details after installation:', error);\n        setToast({ message: t('presets.preset_installed'), type: 'success' });\n        setMarketDialogOpen(false);\n        await loadPresets();\n      }\n    } catch (error: any) {\n      console.error('Failed to install preset:', error);\n      // Check if it's an \"already installed\" error\n      const errorMessage = error.message || '';\n      if (errorMessage.includes('already installed') || errorMessage.includes('已安装')) {\n        setToast({ message: t('presets.preset_already_installed'), type: 'warning' });\n      } else {\n        setToast({ message: t('presets.preset_install_failed', { error: errorMessage }), type: 'error' });\n      }\n    } finally {\n      setInstallingFromMarket(null);\n    }\n  };\n\n  // Load presets when opening market dialog\n  useEffect(() => {\n    if (marketDialogOpen && marketPresets.length === 0) {\n      loadMarketPresets();\n    }\n  }, [marketDialogOpen]);\n\n  // Filter market presets\n  const filteredMarketPresets = marketPresets.filter(preset =>\n    preset.name.toLowerCase().includes(marketSearch.toLowerCase()) ||\n    preset.description?.toLowerCase().includes(marketSearch.toLowerCase()) ||\n    preset.author?.toLowerCase().includes(marketSearch.toLowerCase())\n  );\n\n  // Load presets list\n  const loadPresets = async () => {\n    try {\n      setLoading(true);\n      const response = await api.getPresets();\n      setPresets(response.presets || []);\n    } catch (error) {\n      console.error('Failed to load presets:', error);\n      setToast({ message: t('presets.load_presets_failed'), type: 'error' });\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  useEffect(() => {\n    loadPresets();\n  }, []);\n\n  // View preset details\n  const handleViewDetail = async (preset: PresetMetadata) => {\n    try {\n      const detail = await api.getPreset(preset.id);\n      setSelectedPreset({ ...preset, ...detail });\n      setDetailDialogOpen(true);\n\n      // 初始化表单值：优先使用已保存的 userValues，否则使用 defaultValue\n      if (detail.schema && detail.schema.length > 0) {\n        const initialValues: Record<string, any> = {};\n        for (const input of detail.schema) {\n          // 优先使用已保存的值\n          if (detail.userValues && detail.userValues[input.id] !== undefined) {\n            initialValues[input.id] = detail.userValues[input.id];\n          } else {\n            // Otherwise use default value\n            initialValues[input.id] = input.defaultValue ?? '';\n          }\n        }\n        setSecrets(initialValues);\n      }\n    } catch (error) {\n      console.error('Failed to load preset details:', error);\n      setToast({ message: t('presets.load_preset_details_failed'), type: 'error' });\n    }\n  };\n\n  // 安装预设\n  const handleInstall = async () => {\n    try {\n      setIsInstalling(true);\n\n      // 验证输入\n      if (installMethod === 'url' && !installUrl) {\n        setToast({ message: t('presets.please_provide_url'), type: 'warning' });\n        return;\n      }\n      if (installMethod === 'file' && !installFile) {\n        setToast({ message: t('presets.please_provide_file'), type: 'warning' });\n        return;\n      }\n\n      // 确定预设名称\n      const presetName = installName || '';\n\n      // Step 1: Install preset from GitHub repository\n      let installResult;\n      if (installMethod === 'url' && installUrl) {\n        // Install from GitHub repository\n        installResult = await api.installPresetFromGitHub(installUrl, presetName);\n      } else {\n        setToast({ message: t('presets.please_provide_url'), type: 'warning' });\n        return;\n      }\n\n      // Step 2: Get preset details (check if configuration is required)\n      try {\n        // 使用服务器返回的实际预设名称\n        const actualPresetName = installResult?.presetName || presetName;\n        const detail = await api.getPreset(actualPresetName);\n\n        // Check if configuration is required\n        if (detail.schema && detail.schema.length > 0) {\n          // Configuration required, open configuration dialog\n          setSelectedPreset({\n            id: actualPresetName,\n            name: detail.name || actualPresetName,\n            version: detail.version || '1.0.0',\n            installed: true,\n            ...detail\n          });\n\n          // Initialize form values: prefer saved userValues, otherwise use defaultValue\n          const initialValues: Record<string, any> = {};\n          for (const input of detail.schema) {\n            // Prefer saved values\n            if (detail.userValues && detail.userValues[input.id] !== undefined) {\n              initialValues[input.id] = detail.userValues[input.id];\n            } else {\n              // Otherwise use default value\n              initialValues[input.id] = input.defaultValue ?? '';\n            }\n          }\n          setSecrets(initialValues);\n\n          // Close installation dialog, open details dialog\n          setInstallDialogOpen(false);\n          setInstallUrl('');\n          setInstallFile(null);\n          setInstallName('');\n          setDetailDialogOpen(true);\n\n          setToast({ message: t('presets.preset_installed_config_required'), type: 'warning' });\n        } else {\n          // No configuration required, complete directly\n          setToast({ message: t('presets.preset_installed'), type: 'success' });\n          setInstallDialogOpen(false);\n          setInstallUrl('');\n          setInstallFile(null);\n          setInstallName('');\n          await loadPresets();\n        }\n      } catch (error) {\n        // Failed to get details, but installation succeeded, refresh list\n        console.error('Failed to get preset details after installation:', error);\n        setToast({ message: t('presets.preset_installed'), type: 'success' });\n        setInstallDialogOpen(false);\n        setInstallUrl('');\n        setInstallFile(null);\n        setInstallName('');\n        await loadPresets();\n      }\n    } catch (error: any) {\n      console.error('Failed to install preset:', error);\n      // Check if it's an \"already installed\" error\n      const errorMessage = error.message || '';\n      if (errorMessage.includes('already installed') || errorMessage.includes('已安装')) {\n        setToast({ message: t('presets.preset_already_installed'), type: 'warning' });\n      } else {\n        setToast({ message: t('presets.preset_install_failed', { error: errorMessage }), type: 'error' });\n      }\n    } finally {\n      setIsInstalling(false);\n    }\n  };\n\n  // Apply preset (configure sensitive information)\n  const handleApplyPreset = async (values?: Record<string, any>) => {\n    try {\n      setIsApplying(true);\n\n      // Use passed values or existing secrets\n      const inputValues = values || secrets;\n\n      // Verify all required fields are filled\n      if (selectedPreset?.schema && selectedPreset.schema.length > 0) {\n        // Validation completed in DynamicConfigForm\n        // 这里只做简单检查（对于 confirm 类型，false 是有效值）\n        for (const input of selectedPreset.schema) {\n          const value = inputValues[input.id];\n          const isEmpty = value === undefined || value === null || value === '' ||\n            (Array.isArray(value) && value.length === 0);\n\n          if (input.required !== false && isEmpty) {\n            setToast({ message: t('presets.please_fill_field', { field: input.label || input.id }), type: 'warning' });\n            setIsApplying(false);\n            return;\n          }\n        }\n      }\n\n      await api.applyPreset(selectedPreset!.id, inputValues);\n      setToast({ message: t('presets.preset_applied'), type: 'success' });\n      setDetailDialogOpen(false);\n      setSecrets({});\n      // Refresh presets list\n      await loadPresets();\n    } catch (error: any) {\n      console.error('Failed to apply preset:', error);\n      setToast({ message: t('presets.preset_apply_failed', { error: error.message }), type: 'error' });\n    } finally {\n      setIsApplying(false);\n    }\n  };\n\n  // Delete preset\n  const handleDelete = async () => {\n    if (!presetToDelete) return;\n\n    try {\n      await api.deletePreset(presetToDelete);\n      setToast({ message: t('presets.preset_deleted'), type: 'success' });\n      setDeleteDialogOpen(false);\n      setPresetToDelete(null);\n      await loadPresets();\n    } catch (error: any) {\n      console.error('Failed to delete preset:', error);\n      setToast({ message: t('presets.preset_delete_failed', { error: error.message }), type: 'error' });\n    }\n  };\n\n  return (\n    <Card className=\"flex h-full flex-col rounded-lg border shadow-sm\">\n      <CardHeader className=\"flex flex-row items-center justify-between border-b p-4\">\n        <Button variant=\"ghost\" size=\"icon\" onClick={handleGoBack}>\n          <ArrowLeft className=\"h-5 w-5\" />\n        </Button>\n        <CardTitle className=\"text-lg\">{t('presets.title')} <span className=\"text-sm font-normal text-gray-500\">({presets.length})</span></CardTitle>\n        <Button variant=\"ghost\" size=\"icon\" onClick={() => setMarketDialogOpen(true)}>\n          <Store className=\"h-5 w-5\" />\n        </Button>\n      </CardHeader>\n      <CardContent className=\"flex-grow overflow-y-auto p-4\">\n        {loading ? (\n          <div className=\"flex items-center justify-center h-full\">\n            <Loader2 className=\"h-6 w-6 animate-spin text-gray-500\" />\n          </div>\n        ) : presets.length === 0 ? (\n          <div className=\"flex flex-col items-center justify-center h-full text-gray-500\">\n            <Download className=\"h-12 w-12 mb-4 opacity-50\" />\n            <p>{t('presets.no_presets')}</p>\n            <p className=\"text-sm\">{t('presets.no_presets_hint')}</p>\n          </div>\n        ) : (\n          <div className=\"space-y-3\">\n            {presets.map((preset) => (\n              <div\n                key={preset.name}\n                className=\"flex items-center justify-between p-4 border rounded-lg hover:bg-gray-50 transition-colors\"\n              >\n                <div className=\"flex-1\">\n                  <div className=\"flex items-center gap-2\">\n                    <h3 className=\"font-medium\">{preset.name}</h3>\n                    <span className=\"text-xs text-gray-500\">v{preset.version}</span>\n                  </div>\n                  {preset.description && (\n                    <p className=\"text-sm text-gray-600 mt-1\">{preset.description}</p>\n                  )}\n                  {preset.author && (\n                    <p className=\"text-xs text-gray-500 mt-1\">by {preset.author}</p>\n                  )}\n                </div>\n                <div className=\"flex items-center gap-2\">\n                  <Button\n                    variant=\"ghost\"\n                    size=\"icon\"\n                    onClick={() => handleViewDetail(preset)}\n                  >\n                    <Info className=\"h-4 w-4\" />\n                  </Button>\n                  <Button\n                    variant=\"ghost\"\n                    size=\"icon\"\n                    onClick={() => {\n                      setPresetToDelete(preset.id);\n                      setDeleteDialogOpen(true);\n                    }}\n                  >\n                    <Trash2 className=\"h-4 w-4 text-red-500\" />\n                  </Button>\n                </div>\n              </div>\n            ))}\n          </div>\n        )}\n      </CardContent>\n\n      {/* Install Dialog */}\n      <Dialog open={installDialogOpen} onOpenChange={setInstallDialogOpen}>\n        <DialogContent>\n          <DialogHeader>\n            <DialogTitle>{t('presets.install_dialog_title')}</DialogTitle>\n            <DialogDescription>\n              {t('presets.install_dialog_description')}\n            </DialogDescription>\n          </DialogHeader>\n          <div className=\"space-y-4 py-4\">\n            <div className=\"flex gap-2\">\n              <Button\n                variant=\"default\"\n                onClick={() => setInstallMethod('url')}\n                className=\"flex-1\"\n              >\n                <Link className=\"mr-2 h-4 w-4\" />\n                {t('presets.from_url')}\n              </Button>\n            </div>\n\n            <div className=\"space-y-2\">\n              <Label htmlFor=\"preset-url\">{t('presets.github_repository')}</Label>\n              <Input\n                id=\"preset-url\"\n                type=\"url\"\n                placeholder={t('presets.preset_url_placeholder')}\n                value={installUrl}\n                onChange={(e) => setInstallUrl(e.target.value)}\n              />\n              <p className=\"text-xs text-gray-500\">{t('presets.github_url_hint')}</p>\n            </div>\n\n            <div className=\"space-y-2\">\n              <Label htmlFor=\"preset-name\">{t('presets.preset_name')}</Label>\n              <Input\n                id=\"preset-name\"\n                placeholder={t('presets.preset_name_placeholder')}\n                value={installName}\n                onChange={(e) => setInstallName(e.target.value)}\n              />\n            </div>\n          </div>\n          <DialogFooter>\n            <Button variant=\"outline\" onClick={() => setInstallDialogOpen(false)}>\n              {t('presets.close')}\n            </Button>\n            <Button onClick={handleInstall} disabled={isInstalling}>\n              {isInstalling ? (\n                <>\n                  <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n                  {t('presets.installing')}\n                </>\n              ) : (\n                t('presets.install')\n              )}\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n\n      {/* Detail Dialog */}\n      <Dialog open={detailDialogOpen} onOpenChange={setDetailDialogOpen}>\n        <DialogContent className=\"max-w-2xl max-h-[80vh] overflow-hidden flex flex-col\">\n          <DialogHeader>\n            <DialogTitle className=\"flex items-center gap-2\">\n              {selectedPreset?.name}\n              {selectedPreset?.version && (\n                <span className=\"text-sm font-normal text-gray-500\">v{selectedPreset.version}</span>\n              )}\n            </DialogTitle>\n          </DialogHeader>\n          <div className=\"flex-1 overflow-y-auto py-4 px-2\">\n            {selectedPreset?.description && (\n              <p className=\"text-gray-700 mb-4\">{selectedPreset.description}</p>\n            )}\n\n            {selectedPreset?.author && (\n              <p className=\"text-sm text-gray-600 mb-1\">\n                <strong>Author:</strong> {selectedPreset.author}\n              </p>\n            )}\n\n            {selectedPreset?.homepage && (\n              <p className=\"text-sm text-gray-600 mb-1\">\n                <strong>Homepage:</strong> <a href={selectedPreset.homepage} target=\"_blank\" rel=\"noopener noreferrer\" className=\"text-blue-600 hover:underline\">{selectedPreset.homepage}</a>\n              </p>\n            )}\n\n            {selectedPreset?.repository && (\n              <p className=\"text-sm text-gray-600 mb-1\">\n                <strong>Repository:</strong> <a href={selectedPreset.repository} target=\"_blank\" rel=\"noopener noreferrer\" className=\"text-blue-600 hover:underline\">{selectedPreset.repository}</a>\n              </p>\n            )}\n\n            {selectedPreset?.keywords && selectedPreset.keywords.length > 0 && (\n              <div className=\"mt-4\">\n                <strong>Keywords:</strong>\n                <div className=\"flex flex-wrap gap-2 mt-2\">\n                  {selectedPreset.keywords.map((keyword) => (\n                    <span key={keyword} className=\"px-2 py-1 bg-gray-100 rounded text-sm\">\n                      {keyword}\n                    </span>\n                  ))}\n                </div>\n              </div>\n            )}\n\n            {/* Configuration form */}\n            {selectedPreset?.schema && selectedPreset.schema.length > 0 && (\n              <div className=\"mt-6\">\n                <h4 className=\"font-medium text-sm mb-4\">{t('presets.required_information')}</h4>\n                <DynamicConfigForm\n                  schema={selectedPreset.schema}\n                  presetConfig={selectedPreset.config || {}}\n                  onSubmit={(values) => handleApplyPreset(values)}\n                  onCancel={() => setDetailDialogOpen(false)}\n                  isSubmitting={isApplying}\n                  initialValues={secrets}\n                />\n              </div>\n            )}\n          </div>\n        </DialogContent>\n      </Dialog>\n\n      {/* Market Presets Dialog */}\n      <Dialog open={marketDialogOpen} onOpenChange={setMarketDialogOpen}>\n        <DialogContent className=\"max-w-4xl max-h-[80vh] overflow-hidden flex flex-col\">\n          <DialogHeader>\n            <DialogTitle className=\"flex items-center gap-2\">\n              <Store className=\"h-5 w-5\" />\n              {t('presets.market_title')}\n            </DialogTitle>\n            <DialogDescription>\n              {t('presets.market_description')}\n            </DialogDescription>\n          </DialogHeader>\n\n          <div className=\"flex items-center gap-2 py-4\">\n            <div className=\"relative flex-1\">\n              <Search className=\"absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400\" />\n              <Input\n                placeholder={t('presets.search_placeholder')}\n                value={marketSearch}\n                onChange={(e) => setMarketSearch(e.target.value)}\n                className=\"pl-9\"\n              />\n            </div>\n          </div>\n\n          <div className=\"flex-1 overflow-y-auto\">\n            {marketLoading ? (\n              <div className=\"flex items-center justify-center h-64\">\n                <Loader2 className=\"h-8 w-8 animate-spin text-gray-500\" />\n              </div>\n            ) : filteredMarketPresets.length === 0 ? (\n              <div className=\"flex flex-col items-center justify-center h-64 text-gray-500\">\n                <Package className=\"h-12 w-12 mb-4 opacity-50\" />\n                <p>{t('presets.no_presets_found')}</p>\n                <p className=\"text-sm\">{t('presets.no_presets_found_hint')}</p>\n              </div>\n            ) : (\n              <div className=\"space-y-3\">\n                {filteredMarketPresets.map((preset) => {\n                  // Check if this preset is already installed by repo\n                  const isInstalled = presets.some(p => {\n                    // Extract repo from repository field (handle both formats)\n                    let installedRepo = '';\n                    if (p.repository) {\n                      // Remove GitHub URL prefix if present\n                      installedRepo = p.repository.replace(/^https:\\/\\/github\\.com\\//, '').replace(/\\.git$/, '');\n                    }\n                    // Match by repo (preferred), or name as fallback\n                    return installedRepo === preset.repo || p.name === preset.name;\n                  });\n\n                  return (\n                    <div\n                      key={preset.id}\n                      className=\"p-4 border rounded-lg hover:bg-gray-50 transition-colors\"\n                    >\n                      <div className=\"flex items-start justify-between gap-4\">\n                        <div className=\"flex-1\">\n                          <div className=\"flex items-center gap-2 mb-2\">\n                            <h3 className=\"font-semibold text-lg\">{preset.name}</h3>\n                          </div>\n                          {preset.description && (\n                            <p className=\"text-sm text-gray-600 mb-2\">{preset.description}</p>\n                          )}\n                          <div className=\"flex items-center gap-4 text-sm text-gray-500\">\n                            {preset.author && (\n                              <div className=\"flex items-center gap-1.5\">\n                                <span className=\"font-medium\">{t('presets.by', { author: preset.author })}</span>\n                                <a\n                                  href={`https://github.com/${preset.repo}`}\n                                  target=\"_blank\"\n                                  rel=\"noopener noreferrer\"\n                                  className=\"text-gray-600 hover:text-gray-900 transition-colors\"\n                                  title={t('presets.github_repository')}\n                                >\n                                  <i className=\"ri-github-fill text-xl\"></i>\n                                </a>\n                              </div>\n                            )}\n                          </div>\n                        </div>\n                        <Button\n                          onClick={() => handleInstallFromMarket(preset)}\n                          disabled={installingFromMarket === preset.id || isInstalled}\n                          variant={isInstalled ? \"secondary\" : \"default\"}\n                          className=\"shrink-0\"\n                        >\n                          {installingFromMarket === preset.id ? (\n                            <>\n                              <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n                              {t('presets.installing')}\n                            </>\n                          ) : isInstalled ? (\n                            <>\n                              <Check className=\"mr-2 h-4 w-4\" />\n                              {t('presets.installed_label')}\n                            </>\n                          ) : (\n                            <>\n                              <Download className=\"mr-2 h-4 w-4\" />\n                              {t('presets.install')}\n                            </>\n                          )}\n                        </Button>\n                      </div>\n                    </div>\n                  );\n                })}\n              </div>\n            )}\n          </div>\n        </DialogContent>\n      </Dialog>\n\n      {/* Delete Confirmation Dialog */}\n      <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>\n        <DialogContent>\n          <DialogHeader>\n            <DialogTitle>{t('presets.delete_dialog_title')}</DialogTitle>\n            <DialogDescription>\n              {t('presets.delete_dialog_description', { name: presetToDelete })}\n            </DialogDescription>\n          </DialogHeader>\n          <DialogFooter>\n            <Button variant=\"outline\" onClick={() => setDeleteDialogOpen(false)}>\n              {t('presets.close')}\n            </Button>\n            <Button variant=\"destructive\" onClick={handleDelete}>\n              {t('presets.delete')}\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n\n      {toast && (\n        <Toast\n          message={toast.message}\n          type={toast.type}\n          onClose={() => setToast(null)}\n        />\n      )}\n    </Card>\n  );\n}\n"
  },
  {
    "path": "packages/ui/src/components/ProtectedRoute.tsx",
    "content": "const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {\n  // For this application, we allow access without an API key\n  // The App component will handle loading and error states\n  return children;\n};\n\nexport default ProtectedRoute;"
  },
  {
    "path": "packages/ui/src/components/ProviderList.tsx",
    "content": "import { Pencil, Trash2 } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Badge } from \"@/components/ui/badge\";\nimport type { Provider } from \"@/types\";\n\ninterface ProviderListProps {\n  providers: Provider[];\n  onEdit: (index: number) => void;\n  onRemove: (index: number) => void;\n}\n\nexport function ProviderList({ providers, onEdit, onRemove }: ProviderListProps) {\n  // Handle case where providers might be null or undefined\n  if (!providers || !Array.isArray(providers)) {\n    return (\n      <div className=\"space-y-3\">\n        <div className=\"flex items-center justify-center rounded-md border bg-white p-8 text-gray-500\">\n          No providers configured\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"space-y-3\">\n      {providers.map((provider, index) => {\n        // Handle case where individual provider might be null or undefined\n        if (!provider) {\n          return (\n            <div key={index} className=\"flex items-start justify-between rounded-md border bg-white p-4 transition-all hover:shadow-md animate-slide-in hover:scale-[1.01]\">\n              <div className=\"flex-1 space-y-1.5\">\n                <p className=\"text-md font-semibold text-gray-800\">Invalid Provider</p>\n                <p className=\"text-sm text-gray-500\">Provider data is missing</p>\n              </div>\n              <div className=\"ml-4 flex flex-shrink-0 items-center gap-2\">\n                <Button variant=\"ghost\" size=\"icon\" onClick={() => onEdit(index)} className=\"transition-all-ease hover:scale-110\" disabled>\n                  <Pencil className=\"h-4 w-4\" />\n                </Button>\n                <Button variant=\"destructive\" size=\"icon\" onClick={() => onRemove(index)} className=\"transition-all duration-200 hover:scale-110\">\n                  <Trash2 className=\"h-4 w-4 text-current transition-colors duration-200\" />\n                </Button>\n              </div>\n            </div>\n          );\n        }\n\n        // Handle case where provider.name might be null or undefined\n        const providerName = provider.name || \"Unnamed Provider\";\n        \n        // Handle case where provider.api_base_url might be null or undefined\n        const apiBaseUrl = provider.api_base_url || \"No API URL\";\n        \n        // Handle case where provider.models might be null or undefined\n        const models = Array.isArray(provider.models) ? provider.models : [];\n\n        return (\n          <div key={index} className=\"flex items-start justify-between rounded-md border bg-white p-4 transition-all hover:shadow-md animate-slide-in hover:scale-[1.01]\">\n            <div className=\"flex-1 space-y-1.5\">\n              <p className=\"text-md font-semibold text-gray-800\">{providerName}</p>\n              <p className=\"text-sm text-gray-500\">{apiBaseUrl}</p>\n              <div className=\"flex flex-wrap gap-2 pt-2\">\n                {models.map((model, modelIndex) => (\n                  // Handle case where model might be null or undefined\n                  <Badge key={modelIndex} variant=\"outline\" className=\"font-normal transition-all-ease hover:scale-105\">\n                    {model || \"Unnamed Model\"}\n                  </Badge>\n                ))}\n              </div>\n            </div>\n            <div className=\"ml-4 flex flex-shrink-0 items-center gap-2\">\n              <Button variant=\"ghost\" size=\"icon\" onClick={() => onEdit(index)} className=\"transition-all-ease hover:scale-110\">\n                <Pencil className=\"h-4 w-4\" />\n              </Button>\n              <Button variant=\"destructive\" size=\"icon\" onClick={() => onRemove(index)} className=\"transition-all duration-200 hover:scale-110\">\n                <Trash2 className=\"h-4 w-4 text-current transition-colors duration-200\" />\n              </Button>\n            </div>\n          </div>\n        );\n      })}\n    </div>\n  );\n}"
  },
  {
    "path": "packages/ui/src/components/Providers.tsx",
    "content": "import { useState, useRef, useEffect } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { Button } from \"@/components/ui/button\";\nimport { useConfig } from \"./ConfigProvider\";\nimport { ProviderList } from \"./ProviderList\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from \"@/components/ui/dialog\";\nimport { X, Trash2, Plus, Eye, EyeOff, Search, XCircle } from \"lucide-react\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Combobox } from \"@/components/ui/combobox\";\nimport { ComboInput } from \"@/components/ui/combo-input\";\nimport { api } from \"@/lib/api\";\nimport type { Provider } from \"@/types\";\n\ninterface ProviderType extends Provider {}\n\nexport function Providers() {\n  const { t } = useTranslation();\n  const { config, setConfig } = useConfig();\n  const [editingProviderIndex, setEditingProviderIndex] = useState<number | null>(null);\n  const [deletingProviderIndex, setDeletingProviderIndex] = useState<number | null>(null);\n  const [hasFetchedModels, setHasFetchedModels] = useState<Record<number, boolean>>({});\n  const [providerParamInputs, setProviderParamInputs] = useState<Record<string, {name: string, value: string}>>({});\n  const [modelParamInputs, setModelParamInputs] = useState<Record<string, {name: string, value: string}>>({});\n  const [availableTransformers, setAvailableTransformers] = useState<{name: string; endpoint: string | null;}[]>([]);\n  const [editingProviderData, setEditingProviderData] = useState<ProviderType | null>(null);\n  const [isNewProvider, setIsNewProvider] = useState<boolean>(false);\n  const [providerTemplates, setProviderTemplates] = useState<ProviderType[]>([]);\n  const [showApiKey, setShowApiKey] = useState<Record<number, boolean>>({});\n  const [apiKeyError, setApiKeyError] = useState<string | null>(null);\n  const [nameError, setNameError] = useState<string | null>(null);\n  const [searchTerm, setSearchTerm] = useState<string>(\"\");\n  const comboInputRef = useRef<HTMLInputElement>(null);\n\n  useEffect(() => {\n    const fetchProviderTemplates = async () => {\n      try {\n        const response = await fetch('https://pub-0dc3e1677e894f07bbea11b17a29e032.r2.dev/providers.json');\n        if (response.ok) {\n          const data = await response.json();\n          setProviderTemplates(data || []);\n        } else {\n          console.error('Failed to fetch provider templates');\n        }\n      } catch (error) {\n        console.error('Failed to fetch provider templates:', error);\n      }\n    };\n\n    fetchProviderTemplates();\n  }, []);\n\n  // Fetch available transformers when component mounts\n  useEffect(() => {\n    const fetchTransformers = async () => {\n      try {\n        const response = await api.get<{transformers: {name: string; endpoint: string | null;}[]}>('/transformers');\n        setAvailableTransformers(response.transformers);\n      } catch (error) {\n        console.error('Failed to fetch transformers:', error);\n      }\n    };\n\n    fetchTransformers();\n  }, []);\n\n  // Handle case where config is null or undefined\n  if (!config) {\n    return (\n      <Card className=\"flex h-full flex-col rounded-lg border shadow-sm\">\n        <CardHeader className=\"flex flex-row items-center justify-between border-b p-4\">\n          <CardTitle className=\"text-lg\">{t(\"providers.title\")}</CardTitle>\n        </CardHeader>\n        <CardContent className=\"flex-grow flex items-center justify-center p-4\">\n          <div className=\"text-gray-500\">Loading providers configuration...</div>\n        </CardContent>\n      </Card>\n    );\n  }\n\n  // Validate config.Providers to ensure it's an array\n  const validProviders = Array.isArray(config.Providers) ? config.Providers : [];\n\n\n  const handleAddProvider = () => {\n    const newProvider: ProviderType = { name: \"\", api_base_url: \"\", api_key: \"\", models: [] };\n    setEditingProviderIndex(config.Providers.length);\n    setEditingProviderData(newProvider);\n    setIsNewProvider(true);\n    // Reset API key visibility and error when adding new provider\n    setShowApiKey(prev => ({\n      ...prev,\n      [config.Providers.length]: false\n    }));\n    setApiKeyError(null);\n    setNameError(null);\n  };\n\n  const handleEditProvider = (index: number) => {\n    // Find the actual index in the original providers array\n    const actualIndex = validProviders.indexOf(filteredProviders[index]);\n    const provider = config.Providers[actualIndex];\n    setEditingProviderIndex(actualIndex);\n    setEditingProviderData(JSON.parse(JSON.stringify(provider))); // 深拷贝\n    setIsNewProvider(false);\n    // Reset API key visibility and error when opening edit dialog\n    setShowApiKey(prev => ({\n      ...prev,\n      [actualIndex]: false\n    }));\n    setApiKeyError(null);\n    setNameError(null);\n  };\n\n  const handleSaveProvider = () => {\n    if (!editingProviderData) return;\n    \n    // Validate name\n    if (!editingProviderData.name || editingProviderData.name.trim() === '') {\n      setNameError(t(\"providers.name_required\"));\n      return;\n    }\n    \n    // Check for duplicate names (case-insensitive)\n    const trimmedName = editingProviderData.name.trim();\n    const isDuplicate = config.Providers.some((provider, index) => {\n      // For edit mode, skip checking the current provider being edited\n      if (!isNewProvider && index === editingProviderIndex) {\n        return false;\n      }\n      return provider.name.toLowerCase() === trimmedName.toLowerCase();\n    });\n    \n    if (isDuplicate) {\n      setNameError(t(\"providers.name_duplicate\"));\n      return;\n    }\n    \n    // Validate API key\n    if (!editingProviderData.api_key || editingProviderData.api_key.trim() === '') {\n      setApiKeyError(t(\"providers.api_key_required\"));\n      return;\n    }\n    \n    // Clear errors if validation passes\n    setApiKeyError(null);\n    setNameError(null);\n    \n    if (editingProviderIndex !== null && editingProviderData) {\n      const newProviders = [...config.Providers];\n      if (isNewProvider) {\n        newProviders.push(editingProviderData);\n      } else {\n        newProviders[editingProviderIndex] = editingProviderData;\n      }\n      setConfig({ ...config, Providers: newProviders });\n    }\n    // Reset API key visibility for this provider\n    if (editingProviderIndex !== null) {\n      setShowApiKey(prev => {\n        const newState = { ...prev };\n        delete newState[editingProviderIndex];\n        return newState;\n      });\n    }\n    setEditingProviderIndex(null);\n    setEditingProviderData(null);\n    setIsNewProvider(false);\n  };\n\n  const handleCancelAddProvider = () => {\n    // Reset fetched models state for this provider\n    if (editingProviderIndex !== null) {\n      setHasFetchedModels(prev => {\n        const newState = { ...prev };\n        delete newState[editingProviderIndex];\n        return newState;\n      });\n      // Reset API key visibility for this provider\n      setShowApiKey(prev => {\n        const newState = { ...prev };\n        delete newState[editingProviderIndex];\n        return newState;\n      });\n    }\n    setEditingProviderIndex(null);\n    setEditingProviderData(null);\n    setIsNewProvider(false);\n    setApiKeyError(null);\n    setNameError(null);\n  };\n\n  // Handle deletion by setting the correct index in the state\n  const handleSetDeletingProviderIndex = (filteredIndex: number) => {\n    setDeletingProviderIndex(filteredIndex);\n  };\n\n  // Handle deletion by passing the filtered index to get the actual index in the original array\n  const handleRemoveProvider = (filteredIndex: number) => {\n    // Find the actual index in the original providers array\n    const actualIndex = validProviders.indexOf(filteredProviders[filteredIndex]);\n    const newProviders = [...config.Providers];\n    newProviders.splice(actualIndex, 1);\n    setConfig({ ...config, Providers: newProviders });\n    setDeletingProviderIndex(null);\n  };\n\n  const handleProviderChange = (_index: number, field: string, value: string) => {\n    if (editingProviderData) {\n      const updatedProvider = { ...editingProviderData, [field]: value };\n      setEditingProviderData(updatedProvider);\n    }\n  };\n\n  const handleProviderTransformerChange = (_index: number, transformerPath: string) => {\n    if (!transformerPath || !editingProviderData) return; // Don't add empty transformers\n    \n    const updatedProvider = { ...editingProviderData };\n    \n    if (!updatedProvider.transformer) {\n      updatedProvider.transformer = { use: [] };\n    }\n    \n    // Add transformer to the use array\n    updatedProvider.transformer.use = [...updatedProvider.transformer.use, transformerPath];\n    setEditingProviderData(updatedProvider);\n  };\n\n  const removeProviderTransformerAtIndex = (_index: number, transformerIndex: number) => {\n    if (!editingProviderData) return;\n    \n    const updatedProvider = { ...editingProviderData };\n    \n    if (updatedProvider.transformer) {\n      const newUseArray = [...updatedProvider.transformer.use];\n      newUseArray.splice(transformerIndex, 1);\n      updatedProvider.transformer.use = newUseArray;\n      \n      // If use array is now empty and no other properties, remove transformer entirely\n      if (newUseArray.length === 0 && Object.keys(updatedProvider.transformer).length === 1) {\n        delete updatedProvider.transformer;\n      }\n    }\n    \n    setEditingProviderData(updatedProvider);\n  };\n\n  const handleModelTransformerChange = (_providerIndex: number, model: string, transformerPath: string) => {\n    if (!transformerPath || !editingProviderData) return; // Don't add empty transformers\n    \n    const updatedProvider = { ...editingProviderData };\n    \n    if (!updatedProvider.transformer) {\n      updatedProvider.transformer = { use: [] };\n    }\n    \n    // Initialize model transformer if it doesn't exist\n    if (!updatedProvider.transformer[model]) {\n      updatedProvider.transformer[model] = { use: [] };\n    }\n    \n    // Add transformer to the use array\n    updatedProvider.transformer[model].use = [...updatedProvider.transformer[model].use, transformerPath];\n    setEditingProviderData(updatedProvider);\n  };\n\n  const removeModelTransformerAtIndex = (_providerIndex: number, model: string, transformerIndex: number) => {\n    if (!editingProviderData) return;\n    \n    const updatedProvider = { ...editingProviderData };\n    \n    if (updatedProvider.transformer && updatedProvider.transformer[model]) {\n      const newUseArray = [...updatedProvider.transformer[model].use];\n      newUseArray.splice(transformerIndex, 1);\n      updatedProvider.transformer[model].use = newUseArray;\n      \n      // If use array is now empty and no other properties, remove model transformer entirely\n      if (newUseArray.length === 0 && Object.keys(updatedProvider.transformer[model]).length === 1) {\n        delete updatedProvider.transformer[model];\n      }\n    }\n    \n    setEditingProviderData(updatedProvider);\n  };\n\n\n  const addProviderTransformerParameter = (_providerIndex: number, transformerIndex: number, paramName: string, paramValue: string) => {\n    if (!editingProviderData) return;\n    \n    const updatedProvider = { ...editingProviderData };\n    \n    if (!updatedProvider.transformer) {\n      updatedProvider.transformer = { use: [] };\n    }\n    \n    // Add parameter to the specified transformer in use array\n    if (updatedProvider.transformer.use && updatedProvider.transformer.use.length > transformerIndex) {\n      const targetTransformer = updatedProvider.transformer.use[transformerIndex];\n      \n      // If it's already an array with parameters, update it\n      if (Array.isArray(targetTransformer)) {\n        const transformerArray = [...targetTransformer];\n        // Check if the second element is an object (parameters object)\n        if (transformerArray.length > 1 && typeof transformerArray[1] === 'object' && transformerArray[1] !== null) {\n          // Update the existing parameters object\n          const existingParams = transformerArray[1] as Record<string, unknown>;\n          const paramsObj: Record<string, unknown> = { ...existingParams, [paramName]: paramValue };\n          transformerArray[1] = paramsObj;\n        } else if (transformerArray.length > 1) {\n          // If there are other elements, add the parameters object\n          const paramsObj = { [paramName]: paramValue };\n          transformerArray.splice(1, transformerArray.length - 1, paramsObj);\n        } else {\n          // Add a new parameters object\n          const paramsObj = { [paramName]: paramValue };\n          transformerArray.push(paramsObj);\n        }\n        \n        updatedProvider.transformer.use[transformerIndex] = transformerArray as string | (string | Record<string, unknown> | { max_tokens: number })[];\n      } else {\n        // Convert to array format with parameters\n        const paramsObj = { [paramName]: paramValue };\n        updatedProvider.transformer.use[transformerIndex] = [targetTransformer as string, paramsObj];\n      }\n    }\n    \n    setEditingProviderData(updatedProvider);\n  };\n\n\n  const removeProviderTransformerParameterAtIndex = (_providerIndex: number, transformerIndex: number, paramName: string) => {\n    if (!editingProviderData) return;\n    \n    const updatedProvider = { ...editingProviderData };\n    \n    if (!updatedProvider.transformer?.use || updatedProvider.transformer.use.length <= transformerIndex) {\n      return;\n    }\n    \n    const targetTransformer = updatedProvider.transformer.use[transformerIndex];\n    if (Array.isArray(targetTransformer) && targetTransformer.length > 1) {\n      const transformerArray = [...targetTransformer];\n      // Check if the second element is an object (parameters object)\n      if (typeof transformerArray[1] === 'object' && transformerArray[1] !== null) {\n        const paramsObj = { ...(transformerArray[1] as Record<string, unknown>) };\n        delete paramsObj[paramName];\n        \n        // If the parameters object is now empty, remove it\n        if (Object.keys(paramsObj).length === 0) {\n          transformerArray.splice(1, 1);\n        } else {\n          transformerArray[1] = paramsObj;\n        }\n        \n        updatedProvider.transformer.use[transformerIndex] = transformerArray;\n        setEditingProviderData(updatedProvider);\n      }\n    }\n  };\n\n  const addModelTransformerParameter = (_providerIndex: number, model: string, transformerIndex: number, paramName: string, paramValue: string) => {\n    if (!editingProviderData) return;\n    \n    const updatedProvider = { ...editingProviderData };\n    \n    if (!updatedProvider.transformer) {\n      updatedProvider.transformer = { use: [] };\n    }\n    \n    if (!updatedProvider.transformer[model]) {\n      updatedProvider.transformer[model] = { use: [] };\n    }\n    \n    // Add parameter to the specified transformer in use array\n    if (updatedProvider.transformer[model].use && updatedProvider.transformer[model].use.length > transformerIndex) {\n      const targetTransformer = updatedProvider.transformer[model].use[transformerIndex];\n      \n      // If it's already an array with parameters, update it\n      if (Array.isArray(targetTransformer)) {\n        const transformerArray = [...targetTransformer];\n        // Check if the second element is an object (parameters object)\n        if (transformerArray.length > 1 && typeof transformerArray[1] === 'object' && transformerArray[1] !== null) {\n          // Update the existing parameters object\n          const existingParams = transformerArray[1] as Record<string, unknown>;\n          const paramsObj: Record<string, unknown> = { ...existingParams, [paramName]: paramValue };\n          transformerArray[1] = paramsObj;\n        } else if (transformerArray.length > 1) {\n          // If there are other elements, add the parameters object\n          const paramsObj = { [paramName]: paramValue };\n          transformerArray.splice(1, transformerArray.length - 1, paramsObj);\n        } else {\n          // Add a new parameters object\n          const paramsObj = { [paramName]: paramValue };\n          transformerArray.push(paramsObj);\n        }\n        \n        updatedProvider.transformer[model].use[transformerIndex] = transformerArray as string | (string | Record<string, unknown> | { max_tokens: number })[];\n      } else {\n        // Convert to array format with parameters\n        const paramsObj = { [paramName]: paramValue };\n        updatedProvider.transformer[model].use[transformerIndex] = [targetTransformer as string, paramsObj];\n      }\n    }\n    \n    setEditingProviderData(updatedProvider);\n  };\n\n\n  const removeModelTransformerParameterAtIndex = (_providerIndex: number, model: string, transformerIndex: number, paramName: string) => {\n    if (!editingProviderData) return;\n    \n    const updatedProvider = { ...editingProviderData };\n    \n    if (!updatedProvider.transformer?.[model]?.use || updatedProvider.transformer[model].use.length <= transformerIndex) {\n      return;\n    }\n    \n    const targetTransformer = updatedProvider.transformer[model].use[transformerIndex];\n    if (Array.isArray(targetTransformer) && targetTransformer.length > 1) {\n      const transformerArray = [...targetTransformer];\n      // Check if the second element is an object (parameters object)\n      if (typeof transformerArray[1] === 'object' && transformerArray[1] !== null) {\n        const paramsObj = { ...(transformerArray[1] as Record<string, unknown>) };\n        delete paramsObj[paramName];\n        \n        // If the parameters object is now empty, remove it\n        if (Object.keys(paramsObj).length === 0) {\n          transformerArray.splice(1, 1);\n        } else {\n          transformerArray[1] = paramsObj;\n        }\n        \n        updatedProvider.transformer[model].use[transformerIndex] = transformerArray;\n        setEditingProviderData(updatedProvider);\n      }\n    }\n  };\n\n  const handleAddModel = (_index: number, model: string) => {\n    if (!model.trim() || !editingProviderData) return;\n    \n    const updatedProvider = { ...editingProviderData };\n    \n    // Handle case where provider.models might be null or undefined\n    const models = Array.isArray(updatedProvider.models) ? [...updatedProvider.models] : [];\n    \n    // Check if model already exists\n    if (!models.includes(model.trim())) {\n      models.push(model.trim());\n      updatedProvider.models = models;\n      setEditingProviderData(updatedProvider);\n    }\n  };\n\n    const handleTemplateImport = (value: string) => {\n    if (!value) return;\n    try {\n      const selectedTemplate = JSON.parse(value);\n      if (selectedTemplate) {\n        const currentName = editingProviderData?.name;\n        const newProviderData = JSON.parse(JSON.stringify(selectedTemplate));\n\n        if (!isNewProvider && currentName) {\n          newProviderData.name = currentName;\n        }\n        \n        setEditingProviderData(newProviderData as ProviderType);\n      }\n    } catch (e) {\n      console.error(\"Failed to parse template\", e);\n    }\n  };\n\n  const handleRemoveModel = (_providerIndex: number, modelIndex: number) => {\n    if (!editingProviderData) return;\n    \n    const updatedProvider = { ...editingProviderData };\n    \n    // Handle case where provider.models might be null or undefined\n    const models = Array.isArray(updatedProvider.models) ? [...updatedProvider.models] : [];\n    \n    // Handle case where modelIndex might be out of bounds\n    if (modelIndex >= 0 && modelIndex < models.length) {\n      models.splice(modelIndex, 1);\n      updatedProvider.models = models;\n      setEditingProviderData(updatedProvider);\n    }\n  };\n\n  const editingProvider = editingProviderData || (editingProviderIndex !== null ? validProviders[editingProviderIndex] : null);\n\n  // Filter providers based on search term\n  const filteredProviders = validProviders.filter(provider => {\n    if (!searchTerm) return true;\n    const term = searchTerm.toLowerCase();\n    // Check provider name and URL\n    if (\n      (provider.name && provider.name.toLowerCase().includes(term)) ||\n      (provider.api_base_url && provider.api_base_url.toLowerCase().includes(term))\n    ) {\n      return true;\n    }\n    // Check models\n    if (provider.models && Array.isArray(provider.models)) {\n      return provider.models.some(model => \n        model && model.toLowerCase().includes(term)\n      );\n    }\n    return false;\n  });\n\n  return (\n    <Card className=\"flex h-full flex-col rounded-lg border shadow-sm\">\n      <CardHeader className=\"flex flex-col border-b p-4 gap-3\">\n        <div className=\"flex flex-row items-center justify-between\">\n          <CardTitle className=\"text-lg\">{t(\"providers.title\")} <span className=\"text-sm font-normal text-gray-500\">({filteredProviders.length}/{validProviders.length})</span></CardTitle>\n          <Button onClick={handleAddProvider}>{t(\"providers.add\")}</Button>\n        </div>\n        <div className=\"flex items-center gap-2\">\n          <div className=\"relative flex-1\">\n            <Search className=\"absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-500\" />\n            <Input\n              placeholder={t(\"providers.search\")}\n              value={searchTerm}\n              onChange={(e) => setSearchTerm(e.target.value)}\n              className=\"pl-8\"\n            />\n          </div>\n          {searchTerm && (\n            <Button \n              variant=\"ghost\" \n              size=\"icon\"\n              onClick={() => setSearchTerm(\"\")}\n            >\n              <XCircle className=\"h-4 w-4\" />\n            </Button>\n          )}\n        </div>\n      </CardHeader>\n      <CardContent className=\"flex-grow overflow-y-auto p-4\">\n        <ProviderList\n          providers={filteredProviders}\n          onEdit={handleEditProvider}\n          onRemove={handleSetDeletingProviderIndex}\n        />\n      </CardContent>\n\n      {/* Edit Dialog */}\n      <Dialog open={editingProviderIndex !== null} onOpenChange={(open) => {\n        if (!open) {\n          handleCancelAddProvider();\n        }\n      }}>\n        <DialogContent className=\"max-h-[80vh] flex flex-col sm:max-w-2xl\">\n          <DialogHeader>\n            <DialogTitle>{t(\"providers.edit\")}</DialogTitle>\n          </DialogHeader>\n          {editingProvider && editingProviderIndex !== null && (\n            <div className=\"space-y-4 p-4 overflow-y-auto flex-grow\">\n              {providerTemplates.length > 0 && (\n                <div className=\"space-y-2\">\n                  <Label>{t(\"providers.import_from_template\")}</Label>\n                  <Combobox\n                    options={providerTemplates.map(p => ({ label: p.name, value: JSON.stringify(p) }))}\n                    value=\"\"\n                    onChange={handleTemplateImport}\n                    placeholder={t(\"providers.select_template\")}\n                    emptyPlaceholder={t(\"providers.no_templates_found\")}\n                  />\n                </div>\n              )}\n              <div className=\"space-y-2\">\n                <Label htmlFor=\"name\">{t(\"providers.name\")}</Label>\n                <Input \n                  id=\"name\" \n                  value={editingProvider.name || ''} \n                  onChange={(e) => {\n                    handleProviderChange(editingProviderIndex, 'name', e.target.value);\n                    // Clear name error when user starts typing\n                    if (nameError) {\n                      setNameError(null);\n                    }\n                  }}\n                  className={nameError ? \"border-red-500\" : \"\"}\n                />\n                {nameError && (\n                  <p className=\"text-sm text-red-500\">{nameError}</p>\n                )}\n              </div>\n              <div className=\"space-y-2\">\n                <Label htmlFor=\"api_base_url\">{t(\"providers.api_base_url\")}</Label>\n                <Input id=\"api_base_url\" value={editingProvider.api_base_url || ''} onChange={(e) => handleProviderChange(editingProviderIndex, 'api_base_url', e.target.value)} />\n              </div>\n              <div className=\"space-y-2\">\n                <Label htmlFor=\"api_key\">{t(\"providers.api_key\")}</Label>\n                <div className=\"relative\">\n                  <Input \n                    id=\"api_key\" \n                    type={showApiKey[editingProviderIndex || 0] ? \"text\" : \"password\"} \n                    value={editingProvider.api_key || ''} \n                    onChange={(e) => handleProviderChange(editingProviderIndex, 'api_key', e.target.value)} \n                    className={apiKeyError ? \"border-red-500\" : \"\"}\n                  />\n                  <Button\n                    type=\"button\"\n                    variant=\"ghost\"\n                    size=\"icon\"\n                    className=\"absolute right-2 top-1/2 transform -translate-y-1/2 h-8 w-8\"\n                    onClick={() => {\n                      const index = editingProviderIndex || 0;\n                      setShowApiKey(prev => ({\n                        ...prev,\n                        [index]: !prev[index]\n                      }));\n                    }}\n                  >\n                    {showApiKey[editingProviderIndex || 0] ? (\n                      <EyeOff className=\"h-4 w-4\" />\n                    ) : (\n                      <Eye className=\"h-4 w-4\" />\n                    )}\n                  </Button>\n                </div>\n                {apiKeyError && (\n                  <p className=\"text-sm text-red-500\">{apiKeyError}</p>\n                )}\n              </div>\n              <div className=\"space-y-2\">\n                <Label htmlFor=\"models\">{t(\"providers.models\")}</Label>\n                <div className=\"space-y-2\">\n                  <div className=\"flex gap-2\">\n                    <div className=\"flex-1\">\n                      {hasFetchedModels[editingProviderIndex] ? (\n                        <ComboInput\n                          ref={comboInputRef}\n                          options={(editingProvider.models || []).map((model: string) => ({ label: model, value: model }))}\n                          value=\"\"\n                          onChange={() => {\n                            // 只更新输入值，不添加模型\n                          }}\n                          onEnter={(value) => {\n                            if (editingProviderIndex !== null) {\n                              handleAddModel(editingProviderIndex, value);\n                            }\n                          }}\n                          inputPlaceholder={t(\"providers.models_placeholder\")}\n                        />\n                      ) : (\n                        <Input \n                          id=\"models\" \n                          placeholder={t(\"providers.models_placeholder\")} \n                          onKeyDown={(e) => {\n                            if (e.key === 'Enter' && e.currentTarget.value.trim() && editingProviderIndex !== null) {\n                              handleAddModel(editingProviderIndex, e.currentTarget.value);\n                              e.currentTarget.value = '';\n                            }\n                          }}\n                        />\n                      )}\n                    </div>\n                    <Button \n                      onClick={() => {\n                        if (hasFetchedModels[editingProviderIndex] && comboInputRef.current) {\n                          // 使用ComboInput的逻辑\n                          const comboInput = comboInputRef.current as unknown as { getCurrentValue(): string; clearInput(): void };\n                          const currentValue = comboInput.getCurrentValue();\n                          if (currentValue && currentValue.trim() && editingProviderIndex !== null) {\n                            handleAddModel(editingProviderIndex, currentValue.trim());\n                            // 清空ComboInput\n                            comboInput.clearInput();\n                          }\n                        } else {\n                          // 使用普通Input的逻辑\n                          const input = document.getElementById('models') as HTMLInputElement;\n                          if (input && input.value.trim() && editingProviderIndex !== null) {\n                            handleAddModel(editingProviderIndex, input.value);\n                            input.value = '';\n                          }\n                        }\n                      }}\n                    >\n                      {t(\"providers.add_model\")}\n                    </Button>\n                    {/* <Button \n                      onClick={() => editingProvider && fetchAvailableModels(editingProvider)}\n                      disabled={isFetchingModels}\n                      variant=\"outline\"\n                    >\n                      {isFetchingModels ? t(\"providers.fetching_models\") : t(\"providers.fetch_available_models\")}\n                    </Button> */}\n                  </div>\n                  <div className=\"flex flex-wrap gap-2 pt-2\">\n                    {(editingProvider.models || []).map((model: string, modelIndex: number) => (\n                      <Badge key={modelIndex} variant=\"outline\" className=\"font-normal flex items-center gap-1\">\n                        {model}\n                        <button \n                          type=\"button\" \n                          className=\"ml-1 rounded-full hover:bg-gray-200\"\n                          onClick={() => editingProviderIndex !== null && handleRemoveModel(editingProviderIndex, modelIndex)}\n                        >\n                          <X className=\"h-3 w-3\" />\n                        </button>\n                      </Badge>\n                    ))}\n                  </div>\n                </div>\n              </div>\n              \n              {/* Provider Transformer Selection */}\n              <div className=\"space-y-2\">\n                <Label>{t(\"providers.provider_transformer\")}</Label>\n                \n                {/* Add new transformer */}\n                <div className=\"flex gap-2\">\n                  <Combobox\n                    options={availableTransformers.map(t => ({\n                      label: t.name,\n                      value: t.name\n                    }))}\n                    value=\"\"\n                    onChange={(value) => {\n                      if (editingProviderIndex !== null) {\n                        handleProviderTransformerChange(editingProviderIndex, value);\n                      }\n                    }}\n                    placeholder={t(\"providers.select_transformer\")}\n                    emptyPlaceholder={t(\"providers.no_transformers\")}\n                  />\n                </div>\n                \n                {/* Display existing transformers */}\n                {editingProvider.transformer?.use && editingProvider.transformer.use.length > 0 && (\n                  <div className=\"space-y-2 mt-2\">\n                    <div className=\"text-sm font-medium text-gray-700\">{t(\"providers.selected_transformers\")}</div>\n                    {editingProvider.transformer.use.map((transformer: string | (string | Record<string, unknown> | { max_tokens: number })[], transformerIndex: number) => (\n                      <div key={transformerIndex} className=\"border rounded-md p-3\">\n                        <div className=\"flex gap-2 items-center mb-2\">\n                          <div className=\"flex-1 bg-gray-50 rounded p-2 text-sm\">\n                            {typeof transformer === 'string' ? transformer : Array.isArray(transformer) ? String(transformer[0]) : String(transformer)}\n                          </div>\n                          <Button \n                            variant=\"outline\" \n                            size=\"icon\"\n                            onClick={() => {\n                              if (editingProviderIndex !== null) {\n                                removeProviderTransformerAtIndex(editingProviderIndex, transformerIndex);\n                              }\n                            }}\n                          >\n                            <Trash2 className=\"h-4 w-4\" />\n                          </Button>\n                        </div>\n                        \n                        {/* Transformer-specific Parameters */}\n                        <div className=\"mt-2 pl-4 border-l-2 border-gray-200\">\n                          <Label className=\"text-sm\">{t(\"providers.transformer_parameters\")}</Label>\n                          <div className=\"space-y-2 mt-1\">\n                            <div className=\"flex gap-2\">\n                              <Input \n                                placeholder={t(\"providers.parameter_name\")}\n                                value={providerParamInputs[`provider-${editingProviderIndex}-transformer-${transformerIndex}`]?.name || \"\"}\n                                onChange={(e) => {\n                                  const key = `provider-${editingProviderIndex}-transformer-${transformerIndex}`;\n                                  setProviderParamInputs(prev => ({\n                                    ...prev,\n                                    [key]: {\n                                      ...prev[key] || {name: \"\", value: \"\"},\n                                      name: e.target.value\n                                    }\n                                  }));\n                                }}\n                              />\n                              <Input \n                                placeholder={t(\"providers.parameter_value\")}\n                                value={providerParamInputs[`provider-${editingProviderIndex}-transformer-${transformerIndex}`]?.value || \"\"}\n                                onChange={(e) => {\n                                  const key = `provider-${editingProviderIndex}-transformer-${transformerIndex}`;\n                                  setProviderParamInputs(prev => ({\n                                    ...prev,\n                                    [key]: {\n                                      ...prev[key] || {name: \"\", value: \"\"},\n                                      value: e.target.value\n                                    }\n                                  }));\n                                }}\n                              />\n                              <Button \n                                size=\"sm\"\n                                onClick={() => {\n                                  if (editingProviderIndex !== null) {\n                                    const key = `provider-${editingProviderIndex}-transformer-${transformerIndex}`;\n                                    const paramInput = providerParamInputs[key];\n                                    if (paramInput && paramInput.name && paramInput.value) {\n                                      addProviderTransformerParameter(editingProviderIndex, transformerIndex, paramInput.name, paramInput.value);\n                                      setProviderParamInputs(prev => ({\n                                        ...prev,\n                                        [key]: {name: \"\", value: \"\"}\n                                      }));\n                                    }\n                                  }\n                                }}\n                              >\n                                <Plus className=\"h-4 w-4\" />\n                              </Button>\n                            </div>\n                            \n                            {/* Display existing parameters for this transformer */}\n                            {(() => {\n                              // Get parameters for this specific transformer\n                              if (!editingProvider.transformer?.use || editingProvider.transformer.use.length <= transformerIndex) {\n                                return null;\n                              }\n                              \n                              const targetTransformer = editingProvider.transformer.use[transformerIndex];\n                              let params = {};\n                              \n                              if (Array.isArray(targetTransformer) && targetTransformer.length > 1) {\n                                // Check if the second element is an object (parameters object)\n                                if (typeof targetTransformer[1] === 'object' && targetTransformer[1] !== null) {\n                                  params = targetTransformer[1] as Record<string, unknown>;\n                                }\n                              }\n                              \n                              return Object.keys(params).length > 0 ? (\n                                <div className=\"space-y-1\">\n                                  {Object.entries(params).map(([key, value]) => (\n                                    <div key={key} className=\"flex items-center justify-between bg-gray-50 rounded p-2\">\n                                      <div className=\"text-sm\">\n                                        <span className=\"font-medium\">{key}:</span> {String(value)}\n                                      </div>\n                                      <Button \n                                        variant=\"ghost\" \n                                        size=\"sm\"\n                                        className=\"h-6 w-6 p-0\"\n                                        onClick={() => {\n                                          if (editingProviderIndex !== null) {\n                                            // We need a function to remove parameters from a specific transformer\n                                            removeProviderTransformerParameterAtIndex(editingProviderIndex, transformerIndex, key);\n                                          }\n                                        }}\n                                      >\n                                        <X className=\"h-3 w-3\" />\n                                      </Button>\n                                    </div>\n                                  ))}\n                                </div>\n                              ) : null;\n                            })()}\n                          </div>\n                        </div>\n                      </div>\n                    ))}\n                  </div>\n                )}\n              </div>\n              \n              {/* Model-specific Transformers */}\n              {editingProvider.models && editingProvider.models.length > 0 && (\n                <div className=\"space-y-2\">\n                  <Label>{t(\"providers.model_transformers\")}</Label>\n                  <div className=\"space-y-3\">\n                    {(editingProvider.models || []).map((model: string, modelIndex: number) => (\n                      <div key={modelIndex} className=\"border rounded-md p-3\">\n                        <div className=\"font-medium text-sm mb-2\">{model}</div>\n                        {/* Add new transformer */}\n                        <div className=\"flex gap-2\">\n                          <div className=\"flex-1 flex gap-2\">\n                            <Combobox\n                              options={availableTransformers.map(t => ({\n                                label: t.name,\n                                value: t.name\n                              }))}\n                              value=\"\"\n                              onChange={(value) => {\n                                if (editingProviderIndex !== null) {\n                                  handleModelTransformerChange(editingProviderIndex, model, value);\n                                }\n                              }}\n                              placeholder={t(\"providers.select_transformer\")}\n                              emptyPlaceholder={t(\"providers.no_transformers\")}\n                            />\n                          </div>\n                        </div>\n                        \n                        {/* Display existing transformers */}\n                        {editingProvider.transformer?.[model]?.use && editingProvider.transformer[model].use.length > 0 && (\n                          <div className=\"space-y-2 mt-2\">\n                            <div className=\"text-sm font-medium text-gray-700\">{t(\"providers.selected_transformers\")}</div>\n                            {editingProvider.transformer[model].use.map((transformer: string | (string | Record<string, unknown> | { max_tokens: number })[], transformerIndex: number) => (\n                              <div key={transformerIndex} className=\"border rounded-md p-3\">\n                                <div className=\"flex gap-2 items-center mb-2\">\n                                  <div className=\"flex-1 bg-gray-50 rounded p-2 text-sm\">\n                                    {typeof transformer === 'string' ? transformer : Array.isArray(transformer) ? String(transformer[0]) : String(transformer)}\n                                  </div>\n                                  <Button \n                                    variant=\"outline\" \n                                    size=\"icon\"\n                                    onClick={() => {\n                                      if (editingProviderIndex !== null) {\n                                        removeModelTransformerAtIndex(editingProviderIndex, model, transformerIndex);\n                                      }\n                                    }}\n                                  >\n                                    <Trash2 className=\"h-4 w-4\" />\n                                  </Button>\n                                </div>\n                                \n                                {/* Transformer-specific Parameters */}\n                                <div className=\"mt-2 pl-4 border-l-2 border-gray-200\">\n                                  <Label className=\"text-sm\">{t(\"providers.transformer_parameters\")}</Label>\n                                  <div className=\"space-y-2 mt-1\">\n                                    <div className=\"flex gap-2\">\n                                      <Input \n                                        placeholder={t(\"providers.parameter_name\")}\n                                        value={modelParamInputs[`model-${editingProviderIndex}-${model}-transformer-${transformerIndex}`]?.name || \"\"}\n                                        onChange={(e) => {\n                                          const key = `model-${editingProviderIndex}-${model}-transformer-${transformerIndex}`;\n                                          setModelParamInputs(prev => ({\n                                            ...prev,\n                                            [key]: {\n                                              ...prev[key] || {name: \"\", value: \"\"},\n                                              name: e.target.value\n                                            }\n                                          }));\n                                        }}\n                                      />\n                                      <Input \n                                        placeholder={t(\"providers.parameter_value\")}\n                                        value={modelParamInputs[`model-${editingProviderIndex}-${model}-transformer-${transformerIndex}`]?.value || \"\"}\n                                        onChange={(e) => {\n                                          const key = `model-${editingProviderIndex}-${model}-transformer-${transformerIndex}`;\n                                          setModelParamInputs(prev => ({\n                                            ...prev,\n                                            [key]: {\n                                              ...prev[key] || {name: \"\", value: \"\"},\n                                              value: e.target.value\n                                            }\n                                          }));\n                                        }}\n                                      />\n                                      <Button \n                                        size=\"sm\"\n                                        onClick={() => {\n                                          if (editingProviderIndex !== null) {\n                                            const key = `model-${editingProviderIndex}-${model}-transformer-${transformerIndex}`;\n                                            const paramInput = modelParamInputs[key];\n                                            if (paramInput && paramInput.name && paramInput.value) {\n                                              addModelTransformerParameter(editingProviderIndex, model, transformerIndex, paramInput.name, paramInput.value);\n                                              setModelParamInputs(prev => ({\n                                                ...prev,\n                                                [key]: {name: \"\", value: \"\"}\n                                              }));\n                                            }\n                                          }\n                                        }}\n                                      >\n                                        <Plus className=\"h-4 w-4\" />\n                                      </Button>\n                                    </div>\n                                    \n                                    {/* Display existing parameters for this transformer */}\n                                    {(() => {\n                                      // Get parameters for this specific transformer\n                                      if (!editingProvider.transformer?.[model]?.use || editingProvider.transformer[model].use.length <= transformerIndex) {\n                                        return null;\n                                      }\n                                      \n                                      const targetTransformer = editingProvider.transformer[model].use[transformerIndex];\n                                      let params = {};\n                                      \n                                      if (Array.isArray(targetTransformer) && targetTransformer.length > 1) {\n                                        // Check if the second element is an object (parameters object)\n                                        if (typeof targetTransformer[1] === 'object' && targetTransformer[1] !== null) {\n                                          params = targetTransformer[1] as Record<string, unknown>;\n                                        }\n                                      }\n                                      \n                                      return Object.keys(params).length > 0 ? (\n                                        <div className=\"space-y-1\">\n                                          {Object.entries(params).map(([key, value]) => (\n                                            <div key={key} className=\"flex items-center justify-between bg-gray-50 rounded p-2\">\n                                              <div className=\"text-sm\">\n                                                <span className=\"font-medium\">{key}:</span> {String(value)}\n                                              </div>\n                                              <Button \n                                                variant=\"ghost\" \n                                                size=\"sm\"\n                                                className=\"h-6 w-6 p-0\"\n                                                onClick={() => {\n                                                  if (editingProviderIndex !== null) {\n                                                    // We need a function to remove parameters from a specific transformer\n                                                    removeModelTransformerParameterAtIndex(editingProviderIndex, model, transformerIndex, key);\n                                                  }\n                                                }}\n                                              >\n                                                <X className=\"h-3 w-3\" />\n                                              </Button>\n                                            </div>\n                                          ))}\n                                        </div>\n                                      ) : null;\n                                    })()}\n                                  </div>\n                                </div>\n                              </div>\n                            ))}\n                          </div>\n                        )}\n                      </div>\n                    ))}\n                  </div>\n                </div>\n              )}\n              \n            </div>\n          )}\n          <div className=\"space-y-3 mt-auto\">\n            <div className=\"flex justify-end gap-2\">\n              {/* <Button \n                variant=\"outline\" \n                onClick={() => editingProvider && testConnectivity(editingProvider)}\n                disabled={isTestingConnectivity || !editingProvider}\n              >\n                <Wifi className=\"mr-2 h-4 w-4\" />\n                {isTestingConnectivity ? t(\"providers.testing\") : t(\"providers.test_connectivity\")}\n              </Button> */}\n              <Button onClick={handleSaveProvider}>{t(\"app.save\")}</Button>\n            </div>\n          </div>\n        </DialogContent>\n      </Dialog>\n\n      {/* Delete Confirmation Dialog */}\n      <Dialog open={deletingProviderIndex !== null} onOpenChange={() => setDeletingProviderIndex(null)}>\n        <DialogContent>\n          <DialogHeader>\n            <DialogTitle>{t(\"providers.delete\")}</DialogTitle>\n            <DialogDescription>\n              {t(\"providers.delete_provider_confirm\")}\n            </DialogDescription>\n          </DialogHeader>\n          <DialogFooter>\n            <Button variant=\"outline\" onClick={() => setDeletingProviderIndex(null)}>{t(\"providers.cancel\")}</Button>\n            <Button variant=\"destructive\" onClick={() => deletingProviderIndex !== null && handleRemoveProvider(deletingProviderIndex)}>{t(\"providers.delete\")}</Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n    </Card>\n  );\n}\n"
  },
  {
    "path": "packages/ui/src/components/PublicRoute.tsx",
    "content": "const PublicRoute = ({ children }: { children: React.ReactNode }) => {\n  // Always show login page\n  // The login page will handle empty API keys appropriately\n  return children;\n};\n\nexport default PublicRoute;"
  },
  {
    "path": "packages/ui/src/components/RequestHistoryDrawer.tsx",
    "content": "import React, { useState, useEffect } from 'react';\nimport { Button } from '@/components/ui/button';\nimport { History, Trash2, Clock, X } from 'lucide-react';\nimport { requestHistoryDB, type RequestHistoryItem } from '@/lib/db';\n\ninterface RequestHistoryDrawerProps {\n  isOpen: boolean;\n  onClose: () => void;\n  onSelectRequest: (request: RequestHistoryItem) => void;\n}\n\nexport function RequestHistoryDrawer({ isOpen, onClose, onSelectRequest }: RequestHistoryDrawerProps) {\n  const [requests, setRequests] = useState<RequestHistoryItem[]>([]);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    if (isOpen) {\n      loadRequests();\n    }\n  }, [isOpen]);\n\n  const loadRequests = async () => {\n    try {\n      setLoading(true);\n      const history = await requestHistoryDB.getRequests();\n      setRequests(history);\n    } catch (error) {\n      console.error('Failed to load request history:', error);\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  const handleDelete = async (id: string, event: React.MouseEvent) => {\n    event.stopPropagation();\n    try {\n      await requestHistoryDB.deleteRequest(id);\n      setRequests(prev => prev.filter(req => req.id !== id));\n    } catch (error) {\n      console.error('Failed to delete request:', error);\n    }\n  };\n\n  const handleClearAll = async () => {\n    if (window.confirm('确定要清空所有请求历史吗？')) {\n      try {\n        await requestHistoryDB.clearAllRequests();\n        setRequests([]);\n      } catch (error) {\n        console.error('Failed to clear request history:', error);\n      }\n    }\n  };\n\n  const formatTime = (timestamp: string) => {\n    const date = new Date(timestamp);\n    const now = new Date();\n    const diff = now.getTime() - date.getTime();\n    const minutes = Math.floor(diff / 60000);\n    \n    if (minutes < 1) return '刚刚';\n    if (minutes < 60) return `${minutes}分钟前`;\n    if (minutes < 1440) return `${Math.floor(minutes / 60)}小时前`;\n    return date.toLocaleDateString();\n  };\n\n  if (!isOpen) return null;\n\n  return (\n    <div className=\"fixed inset-0 z-50\">\n      {/* 遮罩层 */}\n      <div \n        className=\"absolute inset-0 bg-black bg-opacity-50\"\n        onClick={onClose}\n      />\n      \n      {/* 抽屉 */}\n      <div className=\"absolute right-0 top-0 h-full w-96 bg-white shadow-xl flex flex-col\">\n        {/* 头部 */}\n        <div className=\"flex items-center justify-between p-4 border-b\">\n          <div className=\"flex items-center gap-2\">\n            <History className=\"h-5 w-5\" />\n            <h2 className=\"text-lg font-semibold\">请求历史</h2>\n          </div>\n          <div className=\"flex items-center gap-2\">\n            <Button \n              variant=\"outline\" \n              size=\"sm\" \n              onClick={handleClearAll}\n              disabled={requests.length === 0}\n            >\n              <Trash2 className=\"h-4 w-4 mr-1\" />\n              清空\n            </Button>\n            <Button variant=\"ghost\" size=\"sm\" onClick={onClose}>\n              <X className=\"h-4 w-4\" />\n            </Button>\n          </div>\n        </div>\n\n        {/* 内容 */}\n        <div className=\"flex-1 overflow-y-auto p-4\">\n          {loading ? (\n            <div className=\"flex items-center justify-center h-32 text-gray-500\">\n              加载中...\n            </div>\n          ) : requests.length > 0 ? (\n            <div className=\"space-y-2\">\n              {requests.map((item) => (\n                <div\n                  key={item.id}\n                  className=\"p-3 bg-gray-50 rounded-lg border cursor-pointer hover:bg-gray-100 transition-colors\"\n                  onClick={() => {\n                    onSelectRequest(item);\n                    onClose();\n                  }}\n                >\n                  <div className=\"flex items-center justify-between mb-2\">\n                    <div className=\"flex items-center gap-2\">\n                      <span className=\"font-mono text-xs bg-gray-200 px-2 py-1 rounded\">\n                        {item.method}\n                      </span>\n                      <span className=\"text-sm font-medium truncate flex-1\">\n                        {item.url}\n                      </span>\n                    </div>\n                    <Button\n                      variant=\"ghost\"\n                      size=\"sm\"\n                      onClick={(e) => handleDelete(item.id, e)}\n                      className=\"h-6 w-6 p-0\"\n                    >\n                      <Trash2 className=\"h-3 w-3\" />\n                    </Button>\n                  </div>\n                  \n                  <div className=\"flex items-center justify-between text-xs text-gray-500\">\n                    <div className=\"flex items-center gap-2\">\n                      <span className={`font-mono px-1 rounded ${\n                        item.status >= 200 && item.status < 300 \n                          ? 'bg-green-100 text-green-800' \n                          : item.status >= 400 \n                          ? 'bg-red-100 text-red-800' \n                          : 'bg-yellow-100 text-yellow-800'\n                      }`}>\n                        {item.status}\n                      </span>\n                      <span>{item.responseTime}ms</span>\n                    </div>\n                    <div className=\"flex items-center gap-1\">\n                      <Clock className=\"h-3 w-3\" />\n                      <span>{formatTime(item.timestamp)}</span>\n                    </div>\n                  </div>\n                </div>\n              ))}\n            </div>\n          ) : (\n            <div className=\"text-center text-gray-500 py-8\">\n              <History className=\"h-12 w-12 mx-auto mb-4 text-gray-300\" />\n              <p>暂无请求历史</p>\n              <p className=\"text-sm mt-2\">发送请求后会在此显示历史记录</p>\n            </div>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n}"
  },
  {
    "path": "packages/ui/src/components/Router.tsx",
    "content": "import { useTranslation } from \"react-i18next\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { Label } from \"@/components/ui/label\";\nimport { Input } from \"@/components/ui/input\";\nimport { useConfig } from \"./ConfigProvider\";\nimport { Combobox } from \"./ui/combobox\";\n\nexport function Router() {\n  const { t } = useTranslation();\n  const { config, setConfig } = useConfig();\n\n  // Handle case where config is null or undefined\n  if (!config) {\n    return (\n      <Card className=\"flex h-full flex-col rounded-lg border shadow-sm\">\n        <CardHeader className=\"border-b p-4\">\n          <CardTitle className=\"text-lg\">{t(\"router.title\")}</CardTitle>\n        </CardHeader>\n        <CardContent className=\"flex-grow flex items-center justify-center p-4\">\n          <div className=\"text-gray-500\">Loading router configuration...</div>\n        </CardContent>\n      </Card>\n    );\n  }\n\n  // Handle case where config.Router is null or undefined\n  const routerConfig = config.Router || {\n    default: \"\",\n    background: \"\",\n    think: \"\",\n    longContext: \"\",\n    longContextThreshold: 60000,\n    webSearch: \"\",\n    image: \"\"\n  };\n\n  const handleRouterChange = (field: string, value: string | number) => {\n    // Handle case where config.Router might be null or undefined\n    const currentRouter = config.Router || {};\n    const newRouter = { ...currentRouter, [field]: value };\n    setConfig({ ...config, Router: newRouter });\n  };\n\n  const handleForceUseImageAgentChange = (value: boolean) => {\n    setConfig({ ...config, forceUseImageAgent: value });\n  };\n\n  // Handle case where config.Providers might be null or undefined\n  const providers = Array.isArray(config.Providers) ? config.Providers : [];\n  \n  const modelOptions = providers.flatMap((provider) => {\n    // Handle case where individual provider might be null or undefined\n    if (!provider) return [];\n    \n    // Handle case where provider.models might be null or undefined\n    const models = Array.isArray(provider.models) ? provider.models : [];\n    \n    // Handle case where provider.name might be null or undefined\n    const providerName = provider.name || \"Unknown Provider\";\n    \n    return models.map((model) => ({\n      value: `${providerName},${model || \"Unknown Model\"}`,\n      label: `${providerName}, ${model || \"Unknown Model\"}`,\n    }));\n  });\n\n  return (\n    <Card className=\"flex h-full flex-col rounded-lg border shadow-sm\">\n      <CardHeader className=\"border-b p-4\">\n        <CardTitle className=\"text-lg\">{t(\"router.title\")}</CardTitle>\n      </CardHeader>\n      <CardContent className=\"flex-grow space-y-5 overflow-y-auto p-4\">\n        <div className=\"space-y-2\">\n          <Label>{t(\"router.default\")}</Label>\n          <Combobox\n            options={modelOptions}\n            value={routerConfig.default || \"\"}\n            onChange={(value) => handleRouterChange(\"default\", value)}\n            placeholder={t(\"router.selectModel\")}\n            searchPlaceholder={t(\"router.searchModel\")}\n            emptyPlaceholder={t(\"router.noModelFound\")}\n          />\n        </div>\n        <div className=\"space-y-2\">\n          <Label>{t(\"router.background\")}</Label>\n          <Combobox\n            options={modelOptions}\n            value={routerConfig.background || \"\"}\n            onChange={(value) => handleRouterChange(\"background\", value)}\n            placeholder={t(\"router.selectModel\")}\n            searchPlaceholder={t(\"router.searchModel\")}\n            emptyPlaceholder={t(\"router.noModelFound\")}\n          />\n        </div>\n        <div className=\"space-y-2\">\n          <Label>{t(\"router.think\")}</Label>\n          <Combobox\n            options={modelOptions}\n            value={routerConfig.think || \"\"}\n            onChange={(value) => handleRouterChange(\"think\", value)}\n            placeholder={t(\"router.selectModel\")}\n            searchPlaceholder={t(\"router.searchModel\")}\n            emptyPlaceholder={t(\"router.noModelFound\")}\n          />\n        </div>\n        <div className=\"space-y-2\">\n          <div className=\"flex items-center gap-4\">\n            <div className=\"flex-1\">\n              <Label>{t(\"router.longContext\")}</Label>\n              <Combobox\n                options={modelOptions}\n                value={routerConfig.longContext || \"\"}\n                onChange={(value) => handleRouterChange(\"longContext\", value)}\n                placeholder={t(\"router.selectModel\")}\n                searchPlaceholder={t(\"router.searchModel\")}\n                emptyPlaceholder={t(\"router.noModelFound\")}\n              />\n            </div>\n            <div className=\"w-48\">\n              <Label>{t(\"router.longContextThreshold\")}</Label>\n              <Input\n                type=\"number\"\n                value={routerConfig.longContextThreshold || 60000}\n                onChange={(e) => handleRouterChange(\"longContextThreshold\", parseInt(e.target.value) || 60000)}\n                placeholder=\"60000\"\n              />\n            </div>\n          </div>\n        </div>\n        <div className=\"space-y-2\">\n          <Label>{t(\"router.webSearch\")}</Label>\n          <Combobox\n            options={modelOptions}\n            value={routerConfig.webSearch || \"\"}\n            onChange={(value) => handleRouterChange(\"webSearch\", value)}\n            placeholder={t(\"router.selectModel\")}\n            searchPlaceholder={t(\"router.searchModel\")}\n            emptyPlaceholder={t(\"router.noModelFound\")}\n          />\n        </div>\n        <div className=\"space-y-2\">\n          <div className=\"flex items-center gap-4\">\n            <div className=\"flex-1\">\n              <Label>{t(\"router.image\")} (beta)</Label>\n              <Combobox\n                options={modelOptions}\n                value={routerConfig.image || \"\"}\n                onChange={(value) => handleRouterChange(\"image\", value)}\n                placeholder={t(\"router.selectModel\")}\n                searchPlaceholder={t(\"router.searchModel\")}\n                emptyPlaceholder={t(\"router.noModelFound\")}\n              />\n            </div>\n            <div className=\"w-48\">\n              <Label htmlFor=\"forceUseImageAgent\">{t(\"router.forceUseImageAgent\")}</Label>\n              <select\n                id=\"forceUseImageAgent\"\n                value={config.forceUseImageAgent ? \"true\" : \"false\"}\n                onChange={(e) => handleForceUseImageAgentChange(e.target.value === \"true\")}\n                className=\"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50\"\n              >\n                <option value=\"false\">{t(\"common.no\")}</option>\n                <option value=\"true\">{t(\"common.yes\")}</option>\n              </select>\n            </div>\n          </div>\n        </div>\n      </CardContent>\n    </Card>\n  );\n}\n"
  },
  {
    "path": "packages/ui/src/components/SettingsDialog.tsx",
    "content": "import { useTranslation } from \"react-i18next\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogHeader,\n  DialogTitle,\n  DialogFooter,\n} from \"@/components/ui/dialog\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { Switch } from \"@/components/ui/switch\";\nimport { Combobox } from \"@/components/ui/combobox\";\nimport { useConfig } from \"./ConfigProvider\";\nimport { StatusLineConfigDialog } from \"./StatusLineConfigDialog\";\nimport { useState } from \"react\";\nimport type { StatusLineConfig } from \"@/types\";\n\ninterface SettingsDialogProps {\n  isOpen: boolean;\n  onOpenChange: (isOpen: boolean) => void;\n}\n\nexport function SettingsDialog({ isOpen, onOpenChange }: SettingsDialogProps) {\n  const { t } = useTranslation();\n  const { config, setConfig } = useConfig();\n  const [isStatusLineConfigOpen, setIsStatusLineConfigOpen] = useState(false);\n\n  if (!config) {\n    return null;\n  }\n\n  const handleLogChange = (checked: boolean) => {\n    setConfig({ ...config, LOG: checked });\n  };\n\n  const handlePathChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    setConfig({ ...config, CLAUDE_PATH: e.target.value });\n  };\n\n  const handleStatusLineEnabledChange = (checked: boolean) => {\n    // Ensure we have a complete StatusLineConfig object\n    const newStatusLineConfig: StatusLineConfig = {\n      enabled: checked,\n      currentStyle: config.StatusLine?.currentStyle || \"default\",\n      default: config.StatusLine?.default || { modules: [] },\n      powerline: config.StatusLine?.powerline || { modules: [] },\n    };\n\n    setConfig({\n      ...config,\n      StatusLine: newStatusLineConfig,\n    });\n  };\n\n  const openStatusLineConfig = () => {\n    setIsStatusLineConfigOpen(true);\n  };\n\n  return (\n    <Dialog open={isOpen} onOpenChange={onOpenChange} >\n      <DialogContent data-testid=\"settings-dialog\" className=\"max-h-[80vh] flex flex-col p-0\">\n        <DialogHeader className=\"p-4 pb-0\">\n          <DialogTitle>{t(\"toplevel.title\")}</DialogTitle>\n        </DialogHeader>\n        <div className=\"space-y-4 p-4 px-8 overflow-y-auto flex-1\">\n          <div className=\"flex items-center space-x-2\">\n            <Switch\n              id=\"log\"\n              checked={config.LOG}\n              onCheckedChange={handleLogChange}\n            />\n            <Label\n              htmlFor=\"log\"\n              className=\"transition-all-ease hover:scale-[1.02] cursor-pointer\"\n            >\n              {t(\"toplevel.log\")}\n            </Label>\n          </div>\n          {/* StatusLine Configuration */}\n          <div className=\"space-y-2 border-t pt-4\">\n            <div className=\"flex items-center justify-between\">\n              <div className=\"flex items-center space-x-2\">\n                <Switch\n                  id=\"statusline\"\n                  checked={config.StatusLine?.enabled || false}\n                  onCheckedChange={handleStatusLineEnabledChange}\n                />\n                <Label\n                  htmlFor=\"statusline\"\n                  className=\"transition-all-ease hover:scale-[1.02] cursor-pointer\"\n                >\n                  {t(\"statusline.title\")}\n                </Label>\n              </div>\n              <Button\n                variant=\"outline\"\n                size=\"sm\"\n                onClick={openStatusLineConfig}\n                className=\"transition-all-ease hover:scale-[1.02] active:scale-[0.98]\"\n                data-testid=\"statusline-config-button\"\n              >\n                {t(\"app.settings\")}\n              </Button>\n            </div>\n          </div>\n          <div className=\"space-y-2\">\n            <Label htmlFor=\"log-level\" className=\"transition-all-ease hover:scale-[1.01] cursor-pointer\">{t(\"toplevel.log_level\")}</Label>\n            <Combobox\n              options={[\n                { label: \"fatal\", value: \"fatal\" },\n                { label: \"error\", value: \"error\" },\n                { label: \"warn\", value: \"warn\" },\n                { label: \"info\", value: \"info\" },\n                { label: \"debug\", value: \"debug\" },\n                { label: \"trace\", value: \"trace\" },\n              ]}\n              value={config.LOG_LEVEL}\n              onChange={(value) => setConfig({ ...config, LOG_LEVEL: value })}\n            />\n          </div>\n          <div className=\"space-y-2\">\n            <Label\n              htmlFor=\"claude-path\"\n              className=\"transition-all-ease hover:scale-[1.01] cursor-pointer\"\n            >\n              {t(\"toplevel.claude_path\")}\n            </Label>\n            <Input\n              id=\"claude-path\"\n              value={config.CLAUDE_PATH}\n              onChange={handlePathChange}\n              className=\"transition-all-ease focus:scale-[1.01]\"\n            />\n          </div>\n          <div className=\"space-y-2\">\n            <Label\n              htmlFor=\"host\"\n              className=\"transition-all-ease hover:scale-[1.01] cursor-pointer\"\n            >\n              {t(\"toplevel.host\")}\n            </Label>\n            <Input\n              id=\"host\"\n              value={config.HOST}\n              onChange={(e) => setConfig({ ...config, HOST: e.target.value })}\n              className=\"transition-all-ease focus:scale-[1.01]\"\n            />\n          </div>\n          <div className=\"space-y-2\">\n            <Label\n              htmlFor=\"port\"\n              className=\"transition-all-ease hover:scale-[1.01] cursor-pointer\"\n            >\n              {t(\"toplevel.port\")}\n            </Label>\n            <Input\n              id=\"port\"\n              type=\"number\"\n              value={config.PORT}\n              onChange={(e) =>\n                setConfig({ ...config, PORT: parseInt(e.target.value, 10) })\n              }\n              className=\"transition-all-ease focus:scale-[1.01]\"\n            />\n          </div>\n          <div className=\"space-y-2\">\n            <Label\n              htmlFor=\"timeout\"\n              className=\"transition-all-ease hover:scale-[1.01] cursor-pointer\"\n            >\n              {t(\"toplevel.timeout\")}\n            </Label>\n            <Input\n              id=\"timeout\"\n              value={config.API_TIMEOUT_MS}\n              onChange={(e) =>\n                setConfig({ ...config, API_TIMEOUT_MS: e.target.value })\n              }\n              className=\"transition-all-ease focus:scale-[1.01]\"\n            />\n          </div>\n          <div className=\"space-y-2\">\n            <Label\n              htmlFor=\"proxy-url\"\n              className=\"transition-all-ease hover:scale-[1.01] cursor-pointer\"\n            >\n              {t(\"toplevel.proxy_url\")}\n            </Label>\n            <Input\n              id=\"proxy-url\"\n              value={config.PROXY_URL}\n              onChange={(e) =>\n                setConfig({ ...config, PROXY_URL: e.target.value })\n              }\n              placeholder=\"http://127.0.0.1:7890\"\n              className=\"transition-all-ease focus:scale-[1.01]\"\n            />\n          </div>\n          <div className=\"space-y-2\">\n            <Label\n              htmlFor=\"apikey\"\n              className=\"transition-all-ease hover:scale-[1.01] cursor-pointer\"\n            >\n              {t(\"toplevel.apikey\")}\n            </Label>\n            <Input\n              id=\"apikey\"\n              type=\"password\"\n              value={config.APIKEY}\n              onChange={(e) => setConfig({ ...config, APIKEY: e.target.value })}\n              className=\"transition-all-ease focus:scale-[1.01]\"\n            />\n          </div>\n          <div className=\"space-y-2\">\n            <Label\n              htmlFor=\"custom-router-path\"\n              className=\"transition-all-ease hover:scale-[1.01] cursor-pointer\"\n            >\n              {t(\"toplevel.custom_router_path\")}\n            </Label>\n            <Input\n              id=\"custom-router-path\"\n              value={config.CUSTOM_ROUTER_PATH || \"\"}\n              onChange={(e) => setConfig({ ...config, CUSTOM_ROUTER_PATH: e.target.value })}\n              placeholder={t(\"toplevel.custom_router_path_placeholder\")}\n              className=\"transition-all-ease focus:scale-[1.01]\"\n            />\n          </div>\n        </div>\n        <DialogFooter className=\"p-4 pt-0\">\n          <Button\n            onClick={() => onOpenChange(false)}\n            className=\"transition-all-ease hover:scale-[1.02] active:scale-[0.98]\"\n          >\n            {t(\"app.save\")}\n          </Button>\n        </DialogFooter>\n      </DialogContent>\n\n      <StatusLineConfigDialog\n        isOpen={isStatusLineConfigOpen}\n        onOpenChange={setIsStatusLineConfigOpen}\n        data-testid=\"statusline-config-dialog\"\n      />\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "packages/ui/src/components/StatusLineConfigDialog.tsx",
    "content": "import { useTranslation } from \"react-i18next\";\nimport React, { useState, useEffect, useMemo, useCallback, useRef } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogHeader,\n  DialogTitle,\n  DialogFooter,\n} from \"@/components/ui/dialog\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { Combobox } from \"@/components/ui/combobox\";\nimport { ColorPicker } from \"@/components/ui/color-picker\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { useConfig } from \"./ConfigProvider\";\nimport {\n  validateStatusLineConfig,\n  formatValidationError,\n  createDefaultStatusLineConfig,\n} from \"@/utils/statusline\";\nimport type {\n  StatusLineConfig,\n  StatusLineModuleConfig,\n  StatusLineThemeConfig,\n} from \"@/types\";\n\nconst DEFAULT_MODULE: StatusLineModuleConfig = {\n  type: \"workDir\",\n  icon: \"󰉋\",\n  text: \"{{workDirName}}\",\n  color: \"bright_blue\",\n};\n\n// Nerd Font选项\nconst NERD_FONTS = [\n  { label: \"Hack Nerd Font Mono\", value: \"Hack Nerd Font Mono\" },\n  { label: \"FiraCode Nerd Font Mono\", value: \"FiraCode Nerd Font Mono\" },\n  {\n    label: \"JetBrainsMono Nerd Font Mono\",\n    value: \"JetBrainsMono Nerd Font Mono\",\n  },\n  { label: \"Monaspace Nerd Font Mono\", value: \"Monaspace Nerd Font Mono\" },\n  { label: \"UbuntuMono Nerd Font\", value: \"UbuntuMono Nerd Font\" },\n];\n\n// 模块类型选项\nconst MODULE_TYPES = [\n  { label: \"workDir\", value: \"workDir\" },\n  { label: \"gitBranch\", value: \"gitBranch\" },\n  { label: \"model\", value: \"model\" },\n  { label: \"usage\", value: \"usage\" },\n  { label: \"speed\", value: \"speed\" },\n  { label: \"script\", value: \"script\" },\n];\n\n// ANSI颜色代码映射\nconst ANSI_COLORS: Record<string, string> = {\n  // 标准颜色\n  black: \"text-black\",\n  red: \"text-red-600\",\n  green: \"text-green-600\",\n  yellow: \"text-yellow-500\",\n  blue: \"text-blue-500\",\n  magenta: \"text-purple-500\",\n  cyan: \"text-cyan-500\",\n  white: \"text-white\",\n  // 亮色\n  bright_black: \"text-gray-500\",\n  bright_red: \"text-red-400\",\n  bright_green: \"text-green-400\",\n  bright_yellow: \"text-yellow-300\",\n  bright_blue: \"text-blue-300\",\n  bright_magenta: \"text-purple-300\",\n  bright_cyan: \"text-cyan-300\",\n  bright_white: \"text-white\",\n  // 背景颜色\n  bg_black: \"bg-black\",\n  bg_red: \"bg-red-600\",\n  bg_green: \"bg-green-600\",\n  bg_yellow: \"bg-yellow-500\",\n  bg_blue: \"bg-blue-500\",\n  bg_magenta: \"bg-purple-500\",\n  bg_cyan: \"bg-cyan-500\",\n  bg_white: \"bg-white\",\n  // 亮背景色\n  bg_bright_black: \"bg-gray-800\",\n  bg_bright_red: \"bg-red-400\",\n  bg_bright_green: \"bg-green-400\",\n  bg_bright_yellow: \"bg-yellow-300\",\n  bg_bright_blue: \"bg-blue-300\",\n  bg_bright_magenta: \"bg-purple-300\",\n  bg_bright_cyan: \"bg-cyan-300\",\n  bg_bright_white: \"bg-gray-100\",\n  // Powerline样式需要的额外背景色\n  bg_bright_orange: \"bg-orange-400\",\n  bg_bright_purple: \"bg-purple-400\",\n};\n\n\n// 图标搜索输入组件\ninterface IconData {\n  className: string;\n  unicode: string;\n  char: string;\n}\n\ninterface IconSearchInputProps {\n  value: string;\n  onChange: (value: string) => void;\n  fontFamily: string;\n  t: (key: string) => string;\n}\n\nconst IconSearchInput = React.memo(({ value, onChange, fontFamily, t }: IconSearchInputProps) => {\n  const [isOpen, setIsOpen] = useState(false);\n  const [searchTerm, setSearchTerm] = useState(value);\n  const [icons, setIcons] = useState<IconData[]>([]);\n  const [filteredIcons, setFilteredIcons] = useState<IconData[]>([]);\n  const [isLoading, setIsLoading] = useState(false);\n  const inputRef = React.useRef<HTMLInputElement>(null);\n\n  // 加载Nerdfonts图标数据\n  const loadIcons = useCallback(async () => {\n    if (icons.length > 0) return; // 已经加载过了\n    \n    setIsLoading(true);\n    try {\n      const response = await fetch('https://www.nerdfonts.com/assets/css/combo.css');\n      const cssText = await response.text();\n      \n      // 解析CSS中的图标类名和Unicode\n      const iconRegex = /\\.nf-([a-zA-Z0-9_-]+):before\\s*\\{\\s*content:\\s*\"\\\\([0-9a-fA-F]+)\";?\\s*\\}/g;\n      const iconData: IconData[] = [];\n      let match;\n      \n      while ((match = iconRegex.exec(cssText)) !== null) {\n        const className = `nf-${match[1]}`;\n        const unicode = match[2];\n        const char = String.fromCharCode(parseInt(unicode, 16));\n        iconData.push({ className, unicode, char });\n      }\n      \n      setIcons(iconData);\n      setFilteredIcons(iconData.slice(0, 200));\n    } catch (error) {\n      console.error('Failed to load icons:', error);\n      setIcons([]);\n      setFilteredIcons([]);\n    } finally {\n      setIsLoading(false);\n    }\n  }, [icons.length]);\n\n  // 模糊搜索图标\n  useEffect(() => {\n    if (searchTerm.trim() === '') {\n      setFilteredIcons(icons.slice(0, 100)); // 显示前100个图标\n      return;\n    }\n    \n    const term = searchTerm.toLowerCase();\n    let filtered = icons;\n    \n    // 如果输入的是特殊字符（可能是粘贴的图标），则搜索对应图标\n    if (term.length === 1 || /[\\u{2000}-\\u{2FFFF}]/u.test(searchTerm)) {\n      const pastedIcon = icons.find(icon => icon.char === searchTerm);\n      if (pastedIcon) {\n        filtered = [pastedIcon];\n      } else {\n        // 搜索包含该字符的图标\n        filtered = icons.filter(icon => icon.char === searchTerm);\n      }\n    } else {\n      // 模糊搜索：类名、简化后的名称匹配\n      filtered = icons.filter(icon => {\n        const className = icon.className.toLowerCase();\n        const simpleClassName = className.replace(/[_-]/g, '');\n        const simpleTerm = term.replace(/[_-]/g, '');\n        \n        return (\n          className.includes(term) ||\n          simpleClassName.includes(simpleTerm) ||\n          // 关键词匹配\n          term.split(' ').every(keyword => \n            className.includes(keyword) || simpleClassName.includes(keyword)\n          )\n        );\n      });\n    }\n    \n    setFilteredIcons(filtered.slice(0, 120)); // 显示更多结果\n  }, [searchTerm, icons]);\n\n  // 处理输入变化\n  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    const newValue = e.target.value;\n    setSearchTerm(newValue);\n    onChange(newValue);\n    \n    // 始终打开下拉框，让用户搜索或确认粘贴的内容\n    setIsOpen(true);\n    if (icons.length === 0) {\n      loadIcons();\n    }\n  };\n\n  // 处理粘贴事件\n  const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {\n    const pastedText = e.clipboardData.getData('text');\n    \n    // 如果是单个字符（可能是图标），直接接受并打开下拉框显示相应图标\n    if (pastedText && pastedText.length === 1) {\n      setTimeout(() => {\n        setIsOpen(true);\n      }, 10);\n    }\n  };\n\n  // 选择图标\n  const handleIconSelect = (iconChar: string) => {\n    setSearchTerm(iconChar);\n    onChange(iconChar);\n    setIsOpen(false);\n    inputRef.current?.focus();\n  };\n\n  // 处理焦点事件\n  const handleFocus = () => {\n    setIsOpen(true);\n    if (icons.length === 0) {\n      loadIcons();\n    }\n  };\n\n\n  // 处理失去焦点（延迟关闭以便点击图标）\n  const handleBlur = () => {\n    setTimeout(() => setIsOpen(false), 200);\n  };\n\n  return (\n    <div className=\"relative\">\n      <div className=\"relative\">\n        <Input\n          ref={inputRef}\n          value={searchTerm}\n          onChange={handleInputChange}\n          onFocus={handleFocus}\n          onBlur={handleBlur}\n          onPaste={handlePaste}\n          placeholder={t(\"statusline.icon_placeholder\")}\n          style={{ fontFamily: fontFamily + ', monospace' }}\n          className=\"text-lg pr-2\"\n        />\n      </div>\n      {isOpen && (\n        <div className=\"absolute z-50 mt-1 w-full max-h-72 overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg\">\n            {isLoading ? (\n              <div className=\"flex items-center justify-center p-8\">\n                <svg className=\"animate-spin h-6 w-6 text-primary\" viewBox=\"0 0 24 24\">\n                  <circle cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" strokeWidth=\"4\" fill=\"none\" opacity=\"0.1\"/>\n                  <path fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"/>\n                </svg>\n              </div>\n            ) : (\n              <>\n                <div className=\"grid grid-cols-5 gap-2 p-2 max-h-72 overflow-y-auto\">\n                  {filteredIcons.map((icon) => (\n                    <div\n                      key={icon.className}\n                      className=\"flex items-center justify-center p-3 text-2xl cursor-pointer hover:bg-secondary rounded transition-colors\"\n                      onClick={() => handleIconSelect(icon.char)}\n                      onMouseDown={(e) => e.preventDefault()} // 防止失去焦点\n                      title={`${icon.char} - ${icon.className}`}\n                      style={{ fontFamily: fontFamily + ', monospace' }}\n                    >\n                      {icon.char}\n                    </div>\n                  ))}\n                  {filteredIcons.length === 0 && (\n                    <div className=\"col-span-5 flex flex-col items-center justify-center p-8 text-muted-foreground\">\n                      <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" className=\"mb-2\">\n                        <circle cx=\"11\" cy=\"11\" r=\"8\" />\n                        <path d=\"m21 21-4.35-4.35\" />\n                      </svg>\n                      <div className=\"text-sm\">\n                        {searchTerm ? `${t(\"statusline.no_icons_found\")} \"${searchTerm}\"` : t(\"statusline.no_icons_available\")}\n                      </div>\n                    </div>\n                  )}\n                </div>\n              </>\n            )}\n          </div>\n      )}\n    </div>\n  );\n});\n\n// 变量替换函数\nfunction replaceVariables(\n  text: string,\n  variables: Record<string, string>\n): string {\n  return text.replace(/\\{\\{(\\w+)\\}\\}/g, (match, varName) => {\n    return variables[varName] || match;\n  });\n}\n\n// 渲染单个模块预览\nfunction renderModulePreview(\n  module: StatusLineModuleConfig,\n  isPowerline: boolean = false\n): React.ReactNode {\n  // 模拟变量数据\n  const variables = {\n    workDirName: \"project\",\n    gitBranch: \"main\",\n    model: \"Claude Sonnet 4\",\n    inputTokens: \"1.2k\",\n    outputTokens: \"2.5k\",\n  };\n\n  const text = replaceVariables(module.text, variables);\n  const icon = module.icon || \"\";\n\n  // 如果text为空且不是usage类型，则跳过该模块\n  if (!text && module.type !== \"usage\") {\n    return null;\n  }\n\n  // 检查是否为十六进制颜色值\n  const isHexColor = (color: string) => /^#[0-9A-F]{6}$/i.test(color);\n\n  // 如果是Powerline样式，添加背景色和分隔符\n  if (isPowerline) {\n    // 处理背景色 - 支持ANSI颜色和十六进制颜色\n    let bgColorStyle = {};\n    let bgColorClass = \"\";\n    let separatorDataBg = \"\";\n    if (module.background) {\n      if (isHexColor(module.background)) {\n        bgColorStyle = { backgroundColor: module.background };\n        // 对于十六进制颜色，我们直接使用颜色值作为data属性\n        separatorDataBg = module.background;\n      } else {\n        bgColorClass = ANSI_COLORS[module.background] || \"\";\n        separatorDataBg = module.background;\n      }\n    }\n\n    // 处理文字颜色 - 支持ANSI颜色和十六进制颜色\n    let textColorStyle = {};\n    let textColorClass = \"\";\n    if (module.color) {\n      if (isHexColor(module.color)) {\n        textColorStyle = { color: module.color };\n      } else {\n        textColorClass = ANSI_COLORS[module.color] || \"text-white\";\n      }\n    } else {\n      textColorClass = \"text-white\";\n    }\n\n    return (\n      <div\n        className={`powerline-module px-4 ${bgColorClass} ${textColorClass}`}\n        style={{ ...bgColorStyle, ...textColorStyle }}\n      >\n        <div className=\"powerline-module-content\">\n          {icon && <span>{icon}</span>}\n          <span>{text}</span>\n        </div>\n        <div\n          className=\"powerline-separator\"\n          data-current-bg={separatorDataBg}\n        />\n      </div>\n    );\n  }\n\n  // 处理默认样式下的颜色\n  let textStyle = {};\n  let textClass = \"\";\n  if (module.color) {\n    if (isHexColor(module.color)) {\n      textStyle = { color: module.color };\n    } else {\n      textClass = ANSI_COLORS[module.color] || \"\";\n    }\n  }\n\n  return (\n    <>\n      {icon && (\n        <span style={textStyle} className={textClass}>\n          {icon}\n        </span>\n      )}\n      <span style={textStyle} className={textClass}>\n        {text}\n      </span>\n    </>\n  );\n}\n\ninterface StatusLineConfigDialogProps {\n  isOpen: boolean;\n  onOpenChange: (isOpen: boolean) => void;\n}\n\nexport function StatusLineConfigDialog({\n  isOpen,\n  onOpenChange,\n}: StatusLineConfigDialogProps) {\n  const { t } = useTranslation();\n  const { config, setConfig } = useConfig();\n\n  const [statusLineConfig, setStatusLineConfig] = useState<StatusLineConfig>(\n    config?.StatusLine || createDefaultStatusLineConfig()\n  );\n\n  // 字体状态\n  const [fontFamily, setFontFamily] = useState<string>(\n    config?.StatusLine?.fontFamily || \"Hack Nerd Font Mono\"\n  );\n\n  const [selectedModuleIndex, setSelectedModuleIndex] = useState<number | null>(\n    null\n  );\n  const [hexBackgroundColors, setHexBackgroundColors] = useState<Set<string>>(\n    new Set()\n  );\n\n  // 添加Powerline分隔符样式\n  useEffect(() => {\n    const styleElement = document.createElement(\"style\");\n    styleElement.innerHTML = `\n      .powerline-module {\n        display: inline-flex;\n        align-items: center;\n        height: 28px;\n        position: relative;\n        padding: 0 8px;\n        overflow: visible;\n      }\n      \n      .powerline-module-content {\n        display: flex;\n        align-items: center;\n        gap: 4px;\n        position: relative;\n      }\n      \n      .powerline-separator {\n        width: 0;\n        height: 0;\n        border-top: 14px solid transparent;\n        border-bottom: 14px solid transparent;\n        border-left: 8px solid;\n        position: absolute;\n        right: -8px;\n        top: 0;\n        display: block;\n      }\n      \n      /* 使用层级确保每个模块的三角形覆盖在下一个模块上方 */\n      .cursor-pointer:nth-child(1) .powerline-separator { z-index: 10; }\n      .cursor-pointer:nth-child(2) .powerline-separator { z-index: 9; }\n      .cursor-pointer:nth-child(3) .powerline-separator { z-index: 8; }\n      .cursor-pointer:nth-child(4) .powerline-separator { z-index: 7; }\n      .cursor-pointer:nth-child(5) .powerline-separator { z-index: 6; }\n      .cursor-pointer:nth-child(6) .powerline-separator { z-index: 5; }\n      .cursor-pointer:nth-child(7) .powerline-separator { z-index: 4; }\n      .cursor-pointer:nth-child(8) .powerline-separator { z-index: 3; }\n      .cursor-pointer:nth-child(9) .powerline-separator { z-index: 2; }\n      .cursor-pointer:nth-child(10) .powerline-separator { z-index: 1; }\n      \n      .cursor-pointer:last-child .powerline-separator {\n        display: none;\n      }\n      \n      /* 根据data属性动态设置颜色，确保与模块背景色一致 */\n      .powerline-separator[data-current-bg=\"bg_black\"] { border-left-color: #000000; }\n      .powerline-separator[data-current-bg=\"bg_red\"] { border-left-color: #dc2626; }\n      .powerline-separator[data-current-bg=\"bg_green\"] { border-left-color: #16a34a; }\n      .powerline-separator[data-current-bg=\"bg_yellow\"] { border-left-color: #eab308; }\n      .powerline-separator[data-current-bg=\"bg_blue\"] { border-left-color: #3b82f6; }\n      .powerline-separator[data-current-bg=\"bg_magenta\"] { border-left-color: #a855f7; }\n      .powerline-separator[data-current-bg=\"bg_cyan\"] { border-left-color: #06b6d4; }\n      .powerline-separator[data-current-bg=\"bg_white\"] { border-left-color: #ffffff; }\n      .powerline-separator[data-current-bg=\"bg_bright_black\"] { border-left-color: #1f2937; }\n      .powerline-separator[data-current-bg=\"bg_bright_red\"] { border-left-color: #f87171; }\n      .powerline-separator[data-current-bg=\"bg_bright_green\"] { border-left-color: #4ade80; }\n      .powerline-separator[data-current-bg=\"bg_bright_yellow\"] { border-left-color: #fde047; }\n      .powerline-separator[data-current-bg=\"bg_bright_blue\"] { border-left-color: #93c5fd; }\n      .powerline-separator[data-current-bg=\"bg_bright_magenta\"] { border-left-color: #c084fc; }\n      .powerline-separator[data-current-bg=\"bg_bright_cyan\"] { border-left-color: #22d3ee; }\n      .powerline-separator[data-current-bg=\"bg_bright_white\"] { border-left-color: #f3f4f6; }\n      .powerline-separator[data-current-bg=\"bg_bright_orange\"] { border-left-color: #fb923c; }\n      .powerline-separator[data-current-bg=\"bg_bright_purple\"] { border-left-color: #c084fc; }\n    `;\n    document.head.appendChild(styleElement);\n\n    // 清理函数\n    return () => {\n      document.head.removeChild(styleElement);\n    };\n  }, []);\n\n  // 动态更新十六进制背景颜色的样式\n  useEffect(() => {\n    // 收集所有模块中使用的十六进制背景颜色\n    const hexColors = new Set<string>();\n    Object.keys(statusLineConfig).forEach((key) => {\n      const themeConfig = statusLineConfig[key as keyof StatusLineConfig];\n      if (\n        themeConfig &&\n        typeof themeConfig === \"object\" &&\n        \"modules\" in themeConfig\n      ) {\n        const modules = (themeConfig as StatusLineThemeConfig).modules || [];\n        modules.forEach((module) => {\n          if (module.background && /^#[0-9A-F]{6}$/i.test(module.background)) {\n            hexColors.add(module.background);\n          }\n        });\n      }\n    });\n\n    setHexBackgroundColors(hexColors);\n\n    // 创建动态样式元素\n    const styleElement = document.createElement(\"style\");\n    styleElement.id = \"hex-powerline-styles\";\n\n    // 生成十六进制颜色的CSS规则\n    let cssRules = \"\";\n    hexColors.forEach((color) => {\n      // 将十六进制颜色转换为RGB值\n      const r = parseInt(color.slice(1, 3), 16);\n      const g = parseInt(color.slice(3, 5), 16);\n      const b = parseInt(color.slice(5, 7), 16);\n      cssRules += `.powerline-separator[data-current-bg=\"${color}\"] { border-left-color: rgb(${r}, ${g}, ${b}); }\\n`;\n    });\n\n    styleElement.innerHTML = cssRules;\n    document.head.appendChild(styleElement);\n\n    // 清理函数\n    return () => {\n      const existingStyle = document.getElementById(\"hex-powerline-styles\");\n      if (existingStyle) {\n        document.head.removeChild(existingStyle);\n      }\n    };\n  }, [statusLineConfig]);\n\n  // 模块类型选项\n  const MODULE_TYPES_OPTIONS = MODULE_TYPES.map((item) => ({\n    ...item,\n    label: t(`statusline.${item.label}`),\n  }));\n\n  const handleThemeChange = (value: string) => {\n    setStatusLineConfig((prev) => ({ ...prev, currentStyle: value }));\n  };\n\n  const handleModuleChange = (\n    index: number,\n    field: keyof StatusLineModuleConfig,\n    value: string\n  ) => {\n    const currentTheme =\n      statusLineConfig.currentStyle as keyof StatusLineConfig;\n    const themeConfig = statusLineConfig[currentTheme];\n    const modules =\n      themeConfig && typeof themeConfig === \"object\" && \"modules\" in themeConfig\n        ? [...((themeConfig as StatusLineThemeConfig).modules || [])]\n        : [];\n    if (modules[index]) {\n      modules[index] = { ...modules[index], [field]: value };\n    }\n\n    setStatusLineConfig((prev) => ({\n      ...prev,\n      [currentTheme]: { modules },\n    }));\n  };\n\n  const [validationErrors, setValidationErrors] = useState<string[]>([]);\n\n  const handleSave = () => {\n    // 验证配置\n    const validationResult = validateStatusLineConfig(statusLineConfig);\n\n    if (!validationResult.isValid) {\n      // 格式化错误信息\n      const errorMessages = validationResult.errors.map((error) =>\n        formatValidationError(error, t)\n      );\n      setValidationErrors(errorMessages);\n      return;\n    }\n\n    // 清除之前的错误\n    setValidationErrors([]);\n\n    if (config) {\n      setConfig({\n        ...config,\n        StatusLine: {\n          ...statusLineConfig,\n          fontFamily,\n        },\n      });\n      onOpenChange(false);\n    }\n  };\n\n  // 创建自定义Alert组件\n  const CustomAlert = ({\n    title,\n    description,\n    variant = \"default\",\n  }: {\n    title: string;\n    description: React.ReactNode;\n    variant?: \"default\" | \"destructive\";\n  }) => {\n    const isError = variant === \"destructive\";\n\n    return (\n      <div\n        className={`rounded-lg border p-4 ${\n          isError\n            ? \"bg-red-50 border-red-200 text-red-800\"\n            : \"bg-blue-50 border-blue-200 text-blue-800\"\n        }`}\n      >\n        <div className=\"flex\">\n          <div className=\"flex-shrink-0\">\n            {isError ? (\n              <svg\n                className=\"h-5 w-5 text-red-400\"\n                viewBox=\"0 0 20 20\"\n                fill=\"currentColor\"\n              >\n                <path\n                  fillRule=\"evenodd\"\n                  d=\"M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z\"\n                  clipRule=\"evenodd\"\n                />\n              </svg>\n            ) : (\n              <svg\n                className=\"h-5 w-5 text-blue-400\"\n                viewBox=\"0 0 20 20\"\n                fill=\"currentColor\"\n              >\n                <path\n                  fillRule=\"evenodd\"\n                  d=\"M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z\"\n                  clipRule=\"evenodd\"\n                />\n              </svg>\n            )}\n          </div>\n          <div className=\"ml-3\">\n            <h3\n              className={`text-sm font-medium ${\n                isError ? \"text-red-800\" : \"text-blue-800\"\n              }`}\n            >\n              {title}\n            </h3>\n            <div\n              className={`mt-2 text-sm ${\n                isError ? \"text-red-700\" : \"text-blue-700\"\n              }`}\n            >\n              {description}\n            </div>\n          </div>\n        </div>\n      </div>\n    );\n  };\n\n  const currentThemeKey =\n    statusLineConfig.currentStyle as keyof StatusLineConfig;\n  const currentThemeConfig = statusLineConfig[currentThemeKey];\n  const currentModules =\n    currentThemeConfig &&\n    typeof currentThemeConfig === \"object\" &&\n    \"modules\" in currentThemeConfig\n      ? (currentThemeConfig as StatusLineThemeConfig).modules || []\n      : [];\n  const selectedModule =\n    selectedModuleIndex !== null && currentModules.length > selectedModuleIndex\n      ? currentModules[selectedModuleIndex]\n      : null;\n\n  // 删除选中模块的函数\n  const deleteSelectedModule = useCallback(() => {\n    if (selectedModuleIndex === null) return;\n    \n    const currentTheme =\n      statusLineConfig.currentStyle as keyof StatusLineConfig;\n    const themeConfig = statusLineConfig[currentTheme];\n    const modules =\n      themeConfig &&\n      typeof themeConfig === \"object\" &&\n      \"modules\" in themeConfig\n        ? [...((themeConfig as StatusLineThemeConfig).modules || [])]\n        : [];\n        \n    if (selectedModuleIndex >= 0 && selectedModuleIndex < modules.length) {\n      modules.splice(selectedModuleIndex, 1);\n      \n      setStatusLineConfig((prev) => ({\n        ...prev,\n        [currentTheme]: { modules },\n      }));\n      \n      setSelectedModuleIndex(null);\n    }\n  }, [selectedModuleIndex, statusLineConfig]);\n\n  // 字体样式\n  const fontStyle = fontFamily ? { fontFamily } : {};\n\n  // 键盘事件监听器，支持删除选中的模块\n  useEffect(() => {\n    const handleKeyDown = (e: KeyboardEvent) => {\n      // 检查是否选中了模块\n      if (selectedModuleIndex === null) return;\n\n      // 检查是否按下了删除键 (Delete 或 Backspace)\n      if (e.key === 'Delete' || e.key === 'Backspace') {\n        // 检查当前焦点元素是否是预览区域的模块\n        const activeElement = document.activeElement as HTMLElement;\n\n        // 检查焦点元素是否是预览区域的模块（有 cursor-pointer 类和 tabIndex）\n        const isPreviewModule = activeElement?.classList.contains('cursor-pointer') &&\n                               activeElement?.hasAttribute('tabIndex');\n\n        // 只有当焦点在预览区域的组件上时，才执行删除操作\n        if (isPreviewModule) {\n          e.preventDefault();\n          deleteSelectedModule();\n        }\n      }\n    };\n\n    // 添加事件监听器\n    document.addEventListener('keydown', handleKeyDown);\n\n    // 清理函数\n    return () => {\n      document.removeEventListener('keydown', handleKeyDown);\n    };\n  }, [selectedModuleIndex, deleteSelectedModule]);\n\n  // 当字体或主题变化时强制重新渲染\n  const fontKey = `${fontFamily}-${statusLineConfig.currentStyle}`;\n\n  return (\n    <Dialog open={isOpen} onOpenChange={onOpenChange}>\n      <DialogContent className=\"max-w-4xl h-[90vh] overflow-hidden sm:max-w-5xl md:max-w-6xl lg:max-w-7xl animate-in fade-in-90 slide-in-from-bottom-10 duration-300 flex flex-col\">\n        <DialogHeader\n          data-testid=\"statusline-config-dialog-header\"\n          className=\"border-b pb-4\"\n        >\n          <DialogTitle className=\"flex items-center\">\n            <svg\n              xmlns=\"http://www.w3.org/2000/svg\"\n              width=\"24\"\n              height=\"24\"\n              viewBox=\"0 0 24 24\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              strokeWidth=\"2\"\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              className=\"mr-2\"\n            >\n              <path d=\"M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7\" />\n              <path d=\"M14 3v4a2 2 0 0 0 2 2h4\" />\n              <path d=\"M3 12h18\" />\n            </svg>\n            {t(\"statusline.title\")}\n          </DialogTitle>\n        </DialogHeader>\n\n        {/* 错误显示区域 */}\n        {validationErrors.length > 0 && (\n          <div className=\"px-6\">\n            <CustomAlert\n              variant=\"destructive\"\n              title=\"配置验证失败\"\n              description={\n                <ul className=\"list-disc pl-5 space-y-1\">\n                  {validationErrors.map((error, index) => (\n                    <li key={index}>{error}</li>\n                  ))}\n                </ul>\n              }\n            />\n          </div>\n        )}\n\n        <div className=\"flex flex-col gap-6 flex-1 overflow-hidden\">\n          {/* 配置面板 */}\n          <div className=\"space-y-6\">\n            {/* 主题样式和字体选择 */}\n            <div className=\"grid grid-cols-2 gap-4\">\n              <div className=\"space-y-2\">\n                <Label htmlFor=\"theme-style\" className=\"text-sm font-medium\">\n                  {t(\"statusline.theme\")}\n                </Label>\n                <Combobox\n                  options={[\n                    { label: t(\"statusline.theme_default\"), value: \"default\" },\n                    { label: t(\"statusline.theme_powerline\"), value: \"powerline\" },\n                  ]}\n                  value={statusLineConfig.currentStyle}\n                  onChange={handleThemeChange}\n                  data-testid=\"theme-selector\"\n                  placeholder={t(\"statusline.theme_placeholder\")}\n                />\n              </div>\n\n              <div className=\"space-y-2\">\n                <Label htmlFor=\"font-family\" className=\"text-sm font-medium\">\n                  {t(\"statusline.module_icon\")}\n                </Label>\n                <Combobox\n                  options={NERD_FONTS}\n                  value={fontFamily}\n                  onChange={(value) => setFontFamily(value)}\n                  data-testid=\"font-family-selector\"\n                  placeholder={t(\"statusline.font_placeholder\")}\n                />\n              </div>\n            </div>\n          </div>\n\n          {/* 三栏布局：组件列表 | 预览区域 | 属性配置 */}\n          <div className=\"grid grid-cols-5 gap-6 overflow-hidden flex-1\">\n            {/* 左侧：支持的组件 */}\n            <div className=\"border rounded-lg flex flex-col overflow-hidden col-span-1\">\n              <h3 className=\"text-sm font-medium p-4 pb-0 mb-3\">{t(\"statusline.components\")}</h3>\n              <div className=\"space-y-2 overflow-y-auto px-4 pb-4 flex-1\">\n                {MODULE_TYPES_OPTIONS.map((moduleType) => (\n                  <div\n                    key={moduleType.value}\n                    className=\"flex items-center gap-2 p-2 border rounded cursor-move hover:bg-secondary\"\n                    draggable\n                    onDragStart={(e) => {\n                      e.dataTransfer.setData(\"moduleType\", moduleType.value);\n                    }}\n                  >\n                    <span className=\"text-sm\">{moduleType.label}</span>\n                  </div>\n                ))}\n              </div>\n            </div>\n\n            {/* 中间：预览区域 */}\n            <div className=\"border rounded-lg p-4 flex flex-col col-span-3\">\n              <h3 className=\"text-sm font-medium mb-3\">{t(\"statusline.preview\")}</h3>\n              <div\n                key={fontKey}\n                className={`rounded bg-black/90 text-white font-mono text-sm overflow-x-auto flex items-center border border-border p-3 py-5 shadow-inner overflow-hidden ${\n                  statusLineConfig.currentStyle === \"powerline\"\n                    ? \"gap-0 h-8 p-0 items-center relative\"\n                    : \"h-5\"\n                }`}\n                data-testid=\"statusline-preview\"\n                style={fontStyle}\n                onDragOver={(e) => {\n                  e.preventDefault();\n                }}\n                onDrop={(e) => {\n                  e.preventDefault();\n                  const moduleType = e.dataTransfer.getData(\"moduleType\");\n                  if (moduleType) {\n                    // 添加新模块\n                    const currentTheme =\n                      statusLineConfig.currentStyle as keyof StatusLineConfig;\n                    const themeConfig = statusLineConfig[currentTheme];\n                    const modules =\n                      themeConfig &&\n                      typeof themeConfig === \"object\" &&\n                      \"modules\" in themeConfig\n                        ? [\n                            ...((themeConfig as StatusLineThemeConfig)\n                              .modules || []),\n                          ]\n                        : [];\n\n                    // 根据模块类型设置默认值\n                    let newModule: StatusLineModuleConfig;\n                    switch (moduleType) {\n                      case \"workDir\":\n                        newModule = {\n                          type: \"workDir\",\n                          icon: \"󰉋\",\n                          text: \"{{workDirName}}\",\n                          color: \"bright_blue\",\n                        };\n                        break;\n                      case \"gitBranch\":\n                        newModule = {\n                          type: \"gitBranch\",\n                          icon: \"🌿\",\n                          text: \"{{gitBranch}}\",\n                          color: \"bright_green\",\n                        };\n                        break;\n                      case \"model\":\n                        newModule = {\n                          type: \"model\",\n                          icon: \"🤖\",\n                          text: \"{{model}}\",\n                          color: \"bright_yellow\",\n                        };\n                        break;\n                      case \"usage\":\n                        newModule = {\n                          type: \"usage\",\n                          icon: \"📊\",\n                          text: \"{{inputTokens}} → {{outputTokens}}\",\n                          color: \"bright_magenta\",\n                        };\n                        break;\n                      case \"speed\":\n                        newModule = {\n                          type: \"speed\",\n                          icon: \"⚡\",\n                          text: \"{{tokenSpeed}}\",\n                          color: \"bright_green\",\n                        };\n                        break;\n                      case \"script\":\n                        newModule = {\n                          type: \"script\",\n                          icon: \"📜\",\n                          text: \"Script Module\",\n                          color: \"bright_cyan\",\n                          scriptPath: \"\",\n                        };\n                        break;\n                      default:\n                        newModule = { ...DEFAULT_MODULE, type: moduleType };\n                    }\n\n                    modules.push(newModule);\n\n                    setStatusLineConfig((prev) => ({\n                      ...prev,\n                      [currentTheme]: { modules },\n                    }));\n                  }\n                }}\n              >\n                {currentModules.length > 0 ? (\n                  <div className=\"flex items-center flex-wrap gap-0\">\n                    {currentModules.map((module, index) => (\n                      <div\n                        key={index}\n                        tabIndex={0}\n                        className={`cursor-pointer ${\n                          selectedModuleIndex === index\n                            ? \"bg-white/20\"\n                            : \"hover:bg-white/10\"\n                        } ${\n                          statusLineConfig.currentStyle === \"powerline\"\n                            ? \"p-0 rounded-none inline-flex overflow-visible relative\"\n                            : \"flex items-center gap-1 px-2 py-1 rounded\"\n                        }`}\n                        onClick={() => setSelectedModuleIndex(index)}\n                        draggable\n                        onDragStart={(e) => {\n                          e.dataTransfer.setData(\"dragIndex\", index.toString());\n                        }}\n                        onDragOver={(e) => {\n                          e.preventDefault();\n                        }}\n                        onDrop={(e) => {\n                          e.preventDefault();\n                          const dragIndex = parseInt(\n                            e.dataTransfer.getData(\"dragIndex\")\n                          );\n                          if (!isNaN(dragIndex) && dragIndex !== index) {\n                            // 重新排序模块\n                            const currentTheme =\n                              statusLineConfig.currentStyle as keyof StatusLineConfig;\n                            const themeConfig = statusLineConfig[currentTheme];\n                            const modules =\n                              themeConfig &&\n                              typeof themeConfig === \"object\" &&\n                              \"modules\" in themeConfig\n                                ? [\n                                    ...((themeConfig as StatusLineThemeConfig)\n                                      .modules || []),\n                                  ]\n                                : [];\n\n                            if (\n                              dragIndex >= 0 &&\n                              dragIndex < modules.length &&\n                              index >= 0 &&\n                              index <= modules.length\n                            ) {\n                              const [movedModule] = modules.splice(\n                                dragIndex,\n                                1\n                              );\n                              modules.splice(index, 0, movedModule);\n\n                              setStatusLineConfig((prev) => ({\n                                ...prev,\n                                [currentTheme]: { modules },\n                              }));\n\n                              // 更新选中项的索引\n                              if (selectedModuleIndex === dragIndex) {\n                                setSelectedModuleIndex(index);\n                              } else if (selectedModuleIndex === index) {\n                                setSelectedModuleIndex(dragIndex);\n                              }\n                            }\n                          }\n                        }}\n                      >\n                        {renderModulePreview(\n                          module,\n                          statusLineConfig.currentStyle === \"powerline\"\n                        )}\n                      </div>\n                    ))}\n                  </div>\n                ) : (\n                  <div className=\"flex flex-col items-center justify-center w-full py-4 text-center\">\n                    <svg\n                      xmlns=\"http://www.w3.org/2000/svg\"\n                      width=\"24\"\n                      height=\"24\"\n                      viewBox=\"0 0 24 24\"\n                      fill=\"none\"\n                      stroke=\"currentColor\"\n                      strokeWidth=\"2\"\n                      strokeLinecap=\"round\"\n                      strokeLinejoin=\"round\"\n                      className=\"text-gray-500 mb-2\"\n                    >\n                      <path d=\"M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18z\" />\n                      <path d=\"M12 8v8\" />\n                      <path d=\"M8 12h8\" />\n                    </svg>\n                    <span className=\"text-gray-500 text-sm\">\n                      {t(\"statusline.drag_hint\")}\n                    </span>\n                  </div>\n                )}\n              </div>\n            </div>\n\n            {/* 右侧：属性配置 */}\n            <div className=\"border rounded-lg flex flex-col overflow-hidden col-span-1\">\n              <h3 className=\"text-sm font-medium p-4 pb-0 mb-3\">{t(\"statusline.properties\")}</h3>\n              <div className=\"overflow-y-auto px-4 pb-4 flex-1\">\n                {selectedModule && selectedModuleIndex !== null ? (\n                  <div className=\"space-y-4\">\n                    \n                    <div className=\"space-y-2\">\n                      <Label htmlFor=\"module-icon\">\n                        {t(\"statusline.module_icon\")}\n                      </Label>\n                      <IconSearchInput\n                        key={fontKey}\n                        value={selectedModule.icon || \"\"}\n                        onChange={(value) =>\n                          handleModuleChange(\n                            selectedModuleIndex,\n                            \"icon\",\n                            value\n                          )\n                        }\n                        fontFamily={fontFamily}\n                        t={t}\n                      />\n                      <p className=\"text-xs text-muted-foreground\">\n                        {t(\"statusline.icon_description\")}\n                      </p>\n                    </div>\n\n                    <div className=\"space-y-2\">\n                      <Label htmlFor=\"module-text\">\n                        {t(\"statusline.module_text\")}\n                      </Label>\n                      <Input\n                        id=\"module-text\"\n                        value={selectedModule.text}\n                        onChange={(e) =>\n                          handleModuleChange(\n                            selectedModuleIndex,\n                            \"text\",\n                            e.target.value\n                          )\n                        }\n                        placeholder={t(\"statusline.text_placeholder\")}\n                      />\n                      <div className=\"text-xs text-muted-foreground\">\n                        <p>{t(\"statusline.module_text_description\")}</p>\n                        <div className=\"flex flex-wrap gap-1 mt-1\">\n                          <Badge\n                            variant=\"secondary\"\n                            className=\"text-xs py-0.5 px-1.5\"\n                          >\n                            {\"{{workDirName}}\"}\n                          </Badge>\n                          <Badge\n                            variant=\"secondary\"\n                            className=\"text-xs py-0.5 px-1.5\"\n                          >\n                            {\"{{gitBranch}}\"}\n                          </Badge>\n                          <Badge\n                            variant=\"secondary\"\n                            className=\"text-xs py-0.5 px-1.5\"\n                          >\n                            {\"{{model}}\"}\n                          </Badge>\n                          <Badge\n                            variant=\"secondary\"\n                            className=\"text-xs py-0.5 px-1.5\"\n                          >\n                            {\"{{inputTokens}}\"}\n                          </Badge>\n                          <Badge\n                            variant=\"secondary\"\n                            className=\"text-xs py-0.5 px-1.5\"\n                          >\n                            {\"{{outputTokens}}\"}\n                          </Badge>\n                        </div>\n                      </div>\n                    </div>\n\n                    <div className=\"space-y-2\">\n                      <Label>{t(\"statusline.module_color\")}</Label>\n                      <ColorPicker\n                        value={selectedModule.color || \"\"}\n                        onChange={(value) =>\n                          handleModuleChange(\n                            selectedModuleIndex,\n                            \"color\",\n                            value\n                          )\n                        }\n                      />\n                      <p className=\"text-xs text-muted-foreground\">\n                        {t(\"statusline.module_color_description\")}\n                      </p>\n                    </div>\n\n                    <div className=\"space-y-2\">\n                      <Label>{t(\"statusline.module_background\")}</Label>\n                      <ColorPicker\n                        value={selectedModule.background || \"\"}\n                        onChange={(value) =>\n                          handleModuleChange(\n                            selectedModuleIndex,\n                            \"background\",\n                            value\n                          )\n                        }\n                      />\n                      <p className=\"text-xs text-muted-foreground\">\n                        {t(\"statusline.module_background_description\")}\n                      </p>\n                    </div>\n\n                    {/* Script Path 输入框 - 仅在type为script时显示 */}\n                    {selectedModule.type === \"script\" && (\n                      <div className=\"space-y-2\">\n                        <Label htmlFor=\"module-script-path\">\n                          {t(\"statusline.module_script_path\")}\n                        </Label>\n                        <Input\n                          id=\"module-script-path\"\n                          value={selectedModule.scriptPath || \"\"}\n                          onChange={(e) =>\n                            handleModuleChange(\n                              selectedModuleIndex,\n                              \"scriptPath\",\n                              e.target.value\n                            )\n                          }\n                          placeholder={t(\"statusline.script_placeholder\")}\n                        />\n                        <p className=\"text-xs text-muted-foreground\">\n                          {t(\"statusline.module_script_path_description\")}\n                        </p>\n                      </div>\n                    )}\n\n                    \n                    <Button\n                      variant=\"destructive\"\n                      size=\"sm\"\n                      onClick={deleteSelectedModule}\n                    >\n                      {t(\"statusline.delete_module\")}\n                    </Button>\n                  </div>\n                ) : (\n                  <div className=\"flex items-center justify-center h-full min-h-[200px]\">\n                    <p className=\"text-muted-foreground text-sm\">\n                      {t(\"statusline.select_hint\")}\n                    </p>\n                  </div>\n                )}\n              </div>\n            </div>\n          </div>\n        </div>\n\n        <DialogFooter className=\"border-t pt-4 mt-4\">\n          <Button\n            variant=\"outline\"\n            onClick={() => onOpenChange(false)}\n            className=\"transition-all hover:scale-105\"\n          >\n            {t(\"app.cancel\")}\n          </Button>\n          <Button\n            onClick={handleSave}\n            data-testid=\"save-statusline-config\"\n            className=\"transition-all hover:scale-105\"\n          >\n            {t(\"app.save\")}\n          </Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "packages/ui/src/components/StatusLineImportExport.tsx",
    "content": "import { useTranslation } from \"react-i18next\";\nimport React, { useState, useRef } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { validateStatusLineConfig, backupConfig, restoreConfig, createDefaultStatusLineConfig } from \"@/utils/statusline\";\nimport type { StatusLineConfig } from \"@/types\";\n\ninterface StatusLineImportExportProps {\n  config: StatusLineConfig;\n  onImport: (config: StatusLineConfig) => void;\n  onShowToast: (message: string, type: 'success' | 'error' | 'warning') => void;\n}\n\nexport function StatusLineImportExport({ config, onImport, onShowToast }: StatusLineImportExportProps) {\n  const { t } = useTranslation();\n  const fileInputRef = useRef<HTMLInputElement>(null);\n  const [isImporting, setIsImporting] = useState(false);\n\n  // 导出配置为JSON文件\n  const handleExport = () => {\n    try {\n      // 在导出前验证配置\n      const validationResult = validateStatusLineConfig(config);\n      \n      if (!validationResult.isValid) {\n        onShowToast(t(\"statusline.export_validation_failed\"), 'error');\n        return;\n      }\n      \n      const dataStr = JSON.stringify(config, null, 2);\n      const dataUri = `data:application/json;charset=utf-8,${encodeURIComponent(dataStr)}`;\n      \n      const exportFileDefaultName = `statusline-config-${new Date().toISOString().slice(0, 10)}.json`;\n      \n      const linkElement = document.createElement('a');\n      linkElement.setAttribute('href', dataUri);\n      linkElement.setAttribute('download', exportFileDefaultName);\n      linkElement.click();\n      \n      onShowToast(t(\"statusline.export_success\"), 'success');\n    } catch (error) {\n      console.error(\"Export failed:\", error);\n      onShowToast(t(\"statusline.export_failed\"), 'error');\n    }\n  };\n\n  // 导入配置从JSON文件\n  const handleImport = (event: React.ChangeEvent<HTMLInputElement>) => {\n    const file = event.target.files?.[0];\n    if (!file) return;\n\n    setIsImporting(true);\n    \n    const reader = new FileReader();\n    reader.onload = (e) => {\n      try {\n        const content = e.target?.result as string;\n        const importedConfig = JSON.parse(content) as StatusLineConfig;\n        \n        // 验证导入的配置\n        const validationResult = validateStatusLineConfig(importedConfig);\n        \n        if (!validationResult.isValid) {\n          // 格式化错误信息\n          const errorMessages = validationResult.errors.map(error => \n            error.message\n          ).join('; ');\n          throw new Error(`${t(\"statusline.invalid_config\")}: ${errorMessages}`);\n        }\n        \n        onImport(importedConfig);\n        onShowToast(t(\"statusline.import_success\"), 'success');\n      } catch (error) {\n        console.error(\"Import failed:\", error);\n        onShowToast(t(\"statusline.import_failed\") + (error instanceof Error ? `: ${error.message}` : \"\"), 'error');\n      } finally {\n        setIsImporting(false);\n        // 重置文件输入，以便可以再次选择同一个文件\n        if (fileInputRef.current) {\n          fileInputRef.current.value = \"\";\n        }\n      }\n    };\n    \n    reader.onerror = () => {\n      onShowToast(t(\"statusline.import_failed\"), 'error');\n      setIsImporting(false);\n      if (fileInputRef.current) {\n        fileInputRef.current.value = \"\";\n      }\n    };\n    \n    reader.readAsText(file);\n  };\n\n  // 下载配置模板\n  const handleDownloadTemplate = () => {\n    try {\n      // 使用新的默认配置函数\n      const templateConfig = createDefaultStatusLineConfig();\n      \n      const dataStr = JSON.stringify(templateConfig, null, 2);\n      const dataUri = `data:application/json;charset=utf-8,${encodeURIComponent(dataStr)}`;\n      \n      const templateFileName = \"statusline-config-template.json\";\n      \n      const linkElement = document.createElement('a');\n      linkElement.setAttribute('href', dataUri);\n      linkElement.setAttribute('download', templateFileName);\n      linkElement.click();\n      \n      onShowToast(t(\"statusline.template_download_success\"), 'success');\n    } catch (error) {\n      console.error(\"Template download failed:\", error);\n      onShowToast(t(\"statusline.template_download_failed\"), 'error');\n    }\n  };\n\n  // 配置备份功能\n  const handleBackup = () => {\n    try {\n      const backupStr = backupConfig(config);\n      const dataUri = `data:application/json;charset=utf-8,${encodeURIComponent(backupStr)}`;\n      \n      const backupFileName = `statusline-backup-${new Date().toISOString().slice(0, 10)}.json`;\n      \n      const linkElement = document.createElement('a');\n      linkElement.setAttribute('href', dataUri);\n      linkElement.setAttribute('download', backupFileName);\n      linkElement.click();\n      \n      onShowToast(t(\"statusline.backup_success\"), 'success');\n    } catch (error) {\n      console.error(\"Backup failed:\", error);\n      onShowToast(t(\"statusline.backup_failed\"), 'error');\n    }\n  };\n\n  // 配置恢复功能\n  const handleRestore = (event: React.ChangeEvent<HTMLInputElement>) => {\n    const file = event.target.files?.[0];\n    if (!file) return;\n\n    const reader = new FileReader();\n    reader.onload = (e) => {\n      try {\n        const content = e.target?.result as string;\n        const restoredConfig = restoreConfig(content);\n        \n        if (!restoredConfig) {\n          throw new Error(t(\"statusline.invalid_backup_file\"));\n        }\n        \n        // 验证恢复的配置\n        const validationResult = validateStatusLineConfig(restoredConfig);\n        \n        if (!validationResult.isValid) {\n          // 格式化错误信息\n          const errorMessages = validationResult.errors.map(error => \n            error.message\n          ).join('; ');\n          throw new Error(`${t(\"statusline.invalid_config\")}: ${errorMessages}`);\n        }\n        \n        onImport(restoredConfig);\n        onShowToast(t(\"statusline.restore_success\"), 'success');\n      } catch (error) {\n        console.error(\"Restore failed:\", error);\n        onShowToast(t(\"statusline.restore_failed\") + (error instanceof Error ? `: ${error.message}` : \"\"), 'error');\n      } finally {\n        // 重置文件输入\n        if (fileInputRef.current) {\n          fileInputRef.current.value = \"\";\n        }\n      }\n    };\n    \n    reader.onerror = () => {\n      onShowToast(t(\"statusline.restore_failed\"), 'error');\n      if (fileInputRef.current) {\n        fileInputRef.current.value = \"\";\n      }\n    };\n    \n    reader.readAsText(file);\n  };\n\n  // 移除本地验证函数，因为我们现在使用utils中的验证函数\n\n  return (\n    <Card className=\"transition-all hover:shadow-md\">\n      <CardHeader className=\"p-4\">\n        <CardTitle className=\"text-lg flex items-center\">\n          <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className=\"mr-2\">\n            <path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\"/>\n            <polyline points=\"17 8 12 3 7 8\"/>\n            <line x1=\"12\" y1=\"3\" x2=\"12\" y2=\"15\"/>\n          </svg>\n          {t(\"statusline.import_export\")}\n        </CardTitle>\n      </CardHeader>\n      <CardContent className=\"space-y-4 px-4 pb-4\">\n        <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-3\">\n          <div className=\"grid grid-cols-2 gap-3\">\n            <Button \n              onClick={handleExport} \n              variant=\"outline\" \n              className=\"transition-all hover:scale-105\"\n            >\n              <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className=\"mr-2\">\n                <path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\"/>\n                <polyline points=\"7 10 12 15 17 10\"/>\n                <line x1=\"12\" y1=\"15\" x2=\"12\" y2=\"3\"/>\n              </svg>\n              {t(\"statusline.export\")}\n            </Button>\n            \n            <Button \n              onClick={() => fileInputRef.current?.click()} \n              variant=\"outline\" \n              disabled={isImporting}\n              className=\"transition-all hover:scale-105\"\n            >\n              <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className=\"mr-2\">\n                <path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\"/>\n                <polyline points=\"17 8 12 3 7 8\"/>\n                <line x1=\"12\" y1=\"3\" x2=\"12\" y2=\"15\"/>\n              </svg>\n              {t(\"statusline.import\")}\n            </Button>\n          </div>\n          \n          <div className=\"grid grid-cols-2 gap-3\">\n            <Button \n              onClick={handleBackup} \n              variant=\"outline\" \n              className=\"transition-all hover:scale-105\"\n            >\n              <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className=\"mr-2\">\n                <path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\"/>\n                <polyline points=\"14 2 14 8 20 8\"/>\n                <line x1=\"16\" y1=\"13\" x2=\"8\" y2=\"13\"/>\n                <line x1=\"16\" y1=\"17\" x2=\"8\" y2=\"17\"/>\n                <polyline points=\"10 9 9 9 8 9\"/>\n              </svg>\n              {t(\"statusline.backup\")}\n            </Button>\n            \n            <Button \n              onClick={() => {\n                // 创建一个隐藏的文件输入用于恢复\n                const restoreInput = document.createElement('input');\n                restoreInput.type = 'file';\n                restoreInput.accept = '.json';\n                restoreInput.onchange = (e) => handleRestore(e as any);\n                restoreInput.click();\n              }} \n              variant=\"outline\" \n              className=\"transition-all hover:scale-105\"\n            >\n              <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className=\"mr-2\">\n                <path d=\"M3 15v4c0 1.1.9 2 2 2h14a2 2 0 0 0 2-2v-4M17 9l-5 5-5-5M12 12.8V2.5\"/>\n              </svg>\n              {t(\"statusline.restore\")}\n            </Button>\n          </div>\n          \n          <Button \n            onClick={handleDownloadTemplate} \n            variant=\"outline\" \n            className=\"transition-all hover:scale-105 sm:col-span-2\"\n          >\n            <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className=\"mr-2\">\n              <path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\"/>\n              <polyline points=\"14 2 14 8 20 8\"/>\n              <line x1=\"16\" y1=\"13\" x2=\"8\" y2=\"13\"/>\n              <line x1=\"16\" y1=\"17\" x2=\"8\" y2=\"17\"/>\n              <polyline points=\"10 9 9 9 8 9\"/>\n            </svg>\n            {t(\"statusline.download_template\")}\n          </Button>\n        </div>\n        \n        <input\n          type=\"file\"\n          ref={fileInputRef}\n          onChange={handleImport}\n          accept=\".json\"\n          className=\"hidden\"\n        />\n        \n        <div className=\"p-3 bg-secondary/50 rounded-md\">\n          <div className=\"flex items-start gap-2\">\n            <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className=\"text-muted-foreground mt-0.5 flex-shrink-0\">\n              <circle cx=\"12\" cy=\"12\" r=\"10\"/>\n              <line x1=\"12\" y1=\"16\" x2=\"12\" y2=\"12\"/>\n              <line x1=\"12\" y1=\"8\" x2=\"12.01\" y2=\"8\"/>\n            </svg>\n            <div>\n              <p className=\"text-xs text-muted-foreground\">\n                {t(\"statusline.import_export_help\")}\n              </p>\n            </div>\n          </div>\n        </div>\n      </CardContent>\n    </Card>\n  );\n}\n"
  },
  {
    "path": "packages/ui/src/components/TransformerList.tsx",
    "content": "import { Pencil, Trash2 } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport type { Transformer } from \"@/types\";\n\ninterface TransformerListProps {\n  transformers: Transformer[];\n  onEdit: (index: number) => void;\n  onRemove: (index: number) => void;\n}\n\nexport function TransformerList({ transformers, onEdit, onRemove }: TransformerListProps) {\n  // Handle case where transformers might be null or undefined\n  if (!transformers || !Array.isArray(transformers)) {\n    return (\n      <div className=\"space-y-3\">\n        <div className=\"flex items-center justify-center rounded-md border bg-white p-8 text-gray-500\">\n          No transformers configured\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"space-y-3\">\n      {transformers.map((transformer, index) => {\n        // Handle case where individual transformer might be null or undefined\n        if (!transformer) {\n          return (\n            <div key={index} className=\"flex items-start justify-between rounded-md border bg-white p-4 transition-all hover:shadow-md animate-slide-in hover:scale-[1.01]\">\n              <div className=\"flex-1 space-y-1.5\">\n                <p className=\"text-md font-semibold text-gray-800\">Invalid Transformer</p>\n                <p className=\"text-sm text-gray-500\">Transformer data is missing</p>\n              </div>\n              <div className=\"ml-4 flex flex-shrink-0 items-center gap-2\">\n                <Button variant=\"ghost\" size=\"icon\" onClick={() => onEdit(index)} className=\"transition-all-ease hover:scale-110\" disabled>\n                  <Pencil className=\"h-4 w-4\" />\n                </Button>\n                <Button variant=\"destructive\" size=\"icon\" onClick={() => onRemove(index)} className=\"transition-all duration-200 hover:scale-110\">\n                  <Trash2 className=\"h-4 w-4 text-current transition-colors duration-200\" />\n                </Button>\n              </div>\n            </div>\n          );\n        }\n\n        // Handle case where transformer.path might be null or undefined\n        const transformerPath = transformer.path || \"Unnamed Transformer\";\n        \n        // Handle case where transformer.parameters might be null or undefined\n        const options = transformer.options || {};\n        \n        // Render parameters as tags in a single line\n        const renderParameters = () => {\n          if (!options || Object.keys(options).length === 0) {\n            return <p className=\"text-sm text-gray-500\">No parameters configured</p>;\n          }\n          \n          return (\n            <div className=\"flex flex-wrap gap-2 max-h-8 overflow-hidden\">\n              {Object.entries(options).map(([key, value]) => (\n                <span \n                  key={key} \n                  className=\"inline-flex items-center px-2 py-1 rounded-md bg-gray-100 text-xs font-medium text-gray-700 border\"\n                >\n                  <span className=\"text-gray-600\">{key}:</span>\n                  <span className=\"ml-1 text-gray-800\">{String(value)}</span>\n                </span>\n              ))}\n            </div>\n          );\n        };\n\n        return (\n          <div key={index} className=\"flex items-start justify-between rounded-md border bg-white p-4 transition-all hover:shadow-md animate-slide-in hover:scale-[1.01]\">\n            <div className=\"flex-1 space-y-1.5\">\n              <p className=\"text-md font-semibold text-gray-800\">{transformerPath}</p>\n              {renderParameters()}\n            </div>\n            <div className=\"ml-4 flex flex-shrink-0 items-center gap-2\">\n              <Button variant=\"ghost\" size=\"icon\" onClick={() => onEdit(index)} className=\"transition-all-ease hover:scale-110\">\n                <Pencil className=\"h-4 w-4\" />\n              </Button>\n              <Button variant=\"destructive\" size=\"icon\" onClick={() => onRemove(index)} className=\"transition-all duration-200 hover:scale-110\">\n                <Trash2 className=\"h-4 w-4 text-current transition-colors duration-200\" />\n              </Button>\n            </div>\n          </div>\n        );\n      })}\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/ui/src/components/Transformers.tsx",
    "content": "import { useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { Button } from \"@/components/ui/button\";\nimport { Plus, Trash2 } from \"lucide-react\";\nimport { useConfig } from \"./ConfigProvider\";\nimport { TransformerList } from \"./TransformerList\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from \"@/components/ui/dialog\";\n\nexport function Transformers() {\n  const { t } = useTranslation();\n  const { config, setConfig } = useConfig();\n  const [editingTransformerIndex, setEditingTransformerIndex] = useState<number | null>(null);\n  const [deletingTransformerIndex, setDeletingTransformerIndex] = useState<number | null>(null);\n  const [newTransformer, setNewTransformer] = useState<{ name?: string; path: string; options: { [key: string]: string } } | null>(null);\n\n  // Handle case where config is null or undefined\n  if (!config) {\n    return (\n      <Card className=\"flex h-full flex-col rounded-lg border shadow-sm\">\n        <CardHeader className=\"flex flex-row items-center justify-between border-b p-4\">\n          <CardTitle className=\"text-lg\">{t(\"transformers.title\")}</CardTitle>\n        </CardHeader>\n        <CardContent className=\"flex-grow flex items-center justify-center p-4\">\n          <div className=\"text-gray-500\">Loading transformers configuration...</div>\n        </CardContent>\n      </Card>\n    );\n  }\n\n  // Validate config.Transformers to ensure it's an array\n  const validTransformers = Array.isArray(config.transformers) ? config.transformers : [];\n\n  const handleAddTransformer = () => {\n    const newTransformer = { name: \"\", path: \"\",  options: {} };\n    setNewTransformer(newTransformer);\n    setEditingTransformerIndex(validTransformers.length); // Use the length as index for the new item\n  };\n\n  const handleRemoveTransformer = (index: number) => {\n    const newTransformers = [...validTransformers];\n    newTransformers.splice(index, 1);\n    setConfig({ ...config, transformers: newTransformers });\n    setDeletingTransformerIndex(null);\n  };\n\n  const handleTransformerChange = (index: number, field: string, value: string, parameterKey?: string) => {\n    if (index < validTransformers.length) {\n      // Editing an existing transformer\n      const newTransformers = [...validTransformers];\n      if (parameterKey !== undefined) {\n        newTransformers[index].options![parameterKey] = value;\n      } else {\n        (newTransformers[index] as unknown as Record<string, unknown>)[field] = value;\n      }\n      setConfig({ ...config, transformers: newTransformers });\n    } else {\n      // Editing the new transformer\n      if (newTransformer) {\n        const updatedTransformer = { ...newTransformer };\n        if (parameterKey !== undefined) {\n          updatedTransformer.options![parameterKey] = value;\n        } else {\n          (updatedTransformer as Record<string, unknown>)[field] = value;\n        }\n        setNewTransformer(updatedTransformer);\n      }\n    }\n  };\n\n  const editingTransformer = editingTransformerIndex !== null ? \n    (editingTransformerIndex < validTransformers.length ? \n      validTransformers[editingTransformerIndex] : \n      newTransformer) : \n    null;\n\n  const handleSaveTransformer = () => {\n    if (newTransformer && editingTransformerIndex === validTransformers.length) {\n      // Saving a new transformer\n      const newTransformers = [...validTransformers, newTransformer];\n      setConfig({ ...config, transformers: newTransformers });\n    }\n    // Close the dialog\n    setEditingTransformerIndex(null);\n    setNewTransformer(null);\n  };\n\n  const handleCancelTransformer = () => {\n    // Close the dialog without saving\n    setEditingTransformerIndex(null);\n    setNewTransformer(null);\n  };\n\n  return (\n    <Card className=\"flex h-full flex-col rounded-lg border shadow-sm\">\n      <CardHeader className=\"flex flex-row items-center justify-between border-b p-4\">\n        <CardTitle className=\"text-lg\">{t(\"transformers.title\")} <span className=\"text-sm font-normal text-gray-500\">({validTransformers.length})</span></CardTitle>\n        <Button onClick={handleAddTransformer}>{t(\"transformers.add\")}</Button>\n      </CardHeader>\n      <CardContent className=\"flex-grow overflow-y-auto p-4\">\n        <TransformerList\n          transformers={validTransformers}\n          onEdit={setEditingTransformerIndex}\n          onRemove={setDeletingTransformerIndex}\n        />\n      </CardContent>\n\n      {/* Edit Dialog */}\n      <Dialog open={editingTransformerIndex !== null} onOpenChange={handleCancelTransformer}>\n        <DialogContent>\n          <DialogHeader>\n            <DialogTitle>{t(\"transformers.edit\")}</DialogTitle>\n          </DialogHeader>\n          {editingTransformer && editingTransformerIndex !== null && (\n            <div className=\"space-y-4 py-4 px-6 max-h-96 overflow-y-auto\">\n              <div className=\"space-y-2\">\n                <Label htmlFor=\"transformer-path\">{t(\"transformers.path\")}</Label>\n                <Input \n                  id=\"transformer-path\" \n                  value={editingTransformer.path || ''} \n                  onChange={(e) => handleTransformerChange(editingTransformerIndex, \"path\", e.target.value)} \n                />\n              </div>\n              <div className=\"space-y-2\">\n                <div className=\"flex items-center justify-between\">\n                  <Label>{t(\"transformers.parameters\")}</Label>\n                  <Button \n                    variant=\"outline\" \n                    size=\"sm\"\n                    onClick={() => {\n                      const parameters = editingTransformer.options || {};\n                      const newKey = `param${Object.keys(parameters).length + 1}`;\n                      if (editingTransformerIndex !== null) {\n                        const newParameters = { ...parameters, [newKey]: \"\" };\n                        if (editingTransformerIndex < validTransformers.length) {\n                          const newTransformers = [...validTransformers];\n                          newTransformers[editingTransformerIndex].options = newParameters;\n                          setConfig({ ...config, transformers: newTransformers });\n                        } else if (newTransformer) {\n                          setNewTransformer({ ...newTransformer, options: newParameters });\n                        }\n                      }\n                    }}\n                  >\n                    <Plus className=\"h-4 w-4\" />\n                  </Button>\n                </div>\n                {Object.entries(editingTransformer.options || {}).map(([key, value]) => (\n                  <div key={key} className=\"flex items-center gap-2\">\n                    <Input \n                      value={key} \n                      onChange={(e) => {\n                        const parameters = editingTransformer.options || {};\n                        const newParameters = { ...parameters };\n                        delete newParameters[key];\n                        newParameters[e.target.value] = value;\n                        if (editingTransformerIndex !== null) {\n                          if (editingTransformerIndex < validTransformers.length) {\n                            const newTransformers = [...validTransformers];\n                            newTransformers[editingTransformerIndex].options = newParameters;\n                            setConfig({ ...config, transformers: newTransformers });\n                          } else if (newTransformer) {\n                            setNewTransformer({ ...newTransformer, options: newParameters });\n                          }\n                        }\n                      }}\n                      className=\"flex-1\"\n                    />\n                    <Input \n                      value={value} \n                      onChange={(e) => {\n                        if (editingTransformerIndex !== null) {\n                          handleTransformerChange(editingTransformerIndex, \"parameters\", e.target.value, key);\n                        }\n                      }}\n                      className=\"flex-1\"\n                    />\n                    <Button \n                      variant=\"outline\"\n                      size=\"icon\"\n                      onClick={() => {\n                        if (editingTransformerIndex !== null) {\n                          const parameters = editingTransformer.options || {};\n                          const newParameters = { ...parameters };\n                          delete newParameters[key];\n                          if (editingTransformerIndex < validTransformers.length) {\n                            const newTransformers = [...validTransformers];\n                            newTransformers[editingTransformerIndex].options = newParameters;\n                            setConfig({ ...config, transformers: newTransformers });\n                          } else if (newTransformer) {\n                            setNewTransformer({ ...newTransformer, options: newParameters });\n                          }\n                        }\n                      }}\n                    >\n                      <Trash2 className=\"h-4 w-4\" />\n                    </Button>\n                  </div>\n                ))}\n              </div>\n            </div>\n          )}\n          <DialogFooter>\n            <Button variant=\"outline\" onClick={handleCancelTransformer}>{t(\"app.cancel\")}</Button>\n            <Button onClick={handleSaveTransformer}>{t(\"app.save\")}</Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n\n      {/* Delete Confirmation Dialog */}\n      <Dialog open={deletingTransformerIndex !== null} onOpenChange={() => setDeletingTransformerIndex(null)}>\n        <DialogContent>\n          <DialogHeader>\n            <DialogTitle>{t(\"transformers.delete\")}</DialogTitle>\n            <DialogDescription>\n              {t(\"transformers.delete_transformer_confirm\")}\n            </DialogDescription>\n          </DialogHeader>\n          <DialogFooter>\n            <Button variant=\"outline\" onClick={() => setDeletingTransformerIndex(null)}>{t(\"app.cancel\")}</Button>\n            <Button variant=\"destructive\" onClick={() => deletingTransformerIndex !== null && handleRemoveTransformer(deletingTransformerIndex)}>{t(\"app.delete\")}</Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n    </Card>\n  );\n}\n"
  },
  {
    "path": "packages/ui/src/components/preset/DynamicConfigForm.tsx",
    "content": "import { useState, useEffect } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { Input } from '@/components/ui/input';\nimport { Label } from '@/components/ui/label';\nimport { Button } from '@/components/ui/button';\nimport { Checkbox } from '@/components/ui/checkbox';\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from '@/components/ui/select';\nimport { Textarea } from '@/components/ui/textarea';\nimport { Loader2 } from 'lucide-react';\n\n// Type definitions\ninterface InputOption {\n  label: string;\n  value: string | number | boolean;\n  description?: string;\n  disabled?: boolean;\n}\n\ninterface DynamicOptions {\n  type: 'static' | 'providers' | 'models' | 'custom';\n  options?: InputOption[];\n  providerField?: string;\n}\n\ninterface Condition {\n  field: string;\n  operator?: 'eq' | 'ne' | 'in' | 'nin' | 'gt' | 'lt' | 'gte' | 'lte' | 'exists';\n  value?: any;\n}\n\ninterface RequiredInput {\n  id: string;\n  type?: 'password' | 'input' | 'select' | 'multiselect' | 'confirm' | 'editor' | 'number';\n  label?: string;\n  prompt?: string;\n  placeholder?: string;\n  options?: InputOption[] | DynamicOptions;\n  when?: Condition | Condition[];\n  defaultValue?: any;\n  required?: boolean;\n  validator?: RegExp | string | ((value: any) => boolean | string);\n  min?: number;\n  max?: number;\n  rows?: number;\n  dependsOn?: string[];\n}\n\ninterface PresetConfigSection {\n  Providers?: Array<{\n    name: string;\n    api_base_url?: string;\n    models?: string[];\n    [key: string]: any;\n  }>;\n  [key: string]: any;\n}\n\ninterface DynamicConfigFormProps {\n  schema: RequiredInput[];\n  presetConfig: PresetConfigSection;\n  onSubmit: (values: Record<string, any>) => void;\n  onCancel: () => void;\n  isSubmitting?: boolean;\n  initialValues?: Record<string, any>;\n}\n\nexport function DynamicConfigForm({\n  schema,\n  presetConfig,\n  onSubmit,\n  onCancel,\n  isSubmitting = false,\n  initialValues = {},\n}: DynamicConfigFormProps) {\n  const { t } = useTranslation();\n  const [values, setValues] = useState<Record<string, any>>(initialValues);\n  const [errors, setErrors] = useState<Record<string, string>>({});\n  const [visibleFields, setVisibleFields] = useState<Set<string>>(new Set());\n\n  // Calculate visible fields\n  useEffect(() => {\n    const updateVisibility = () => {\n      const visible = new Set<string>();\n\n      for (const field of schema) {\n        if (shouldShowField(field)) {\n          visible.add(field.id);\n        }\n      }\n\n      setVisibleFields(visible);\n    };\n\n    updateVisibility();\n  }, [values, schema]);\n\n  // Evaluate condition\n  const evaluateCondition = (condition: Condition): boolean => {\n    const actualValue = values[condition.field];\n\n    if (condition.operator === 'exists') {\n      return actualValue !== undefined && actualValue !== null;\n    }\n\n    if (condition.operator === 'in') {\n      return Array.isArray(condition.value) && condition.value.includes(actualValue);\n    }\n\n    if (condition.operator === 'nin') {\n      return Array.isArray(condition.value) && !condition.value.includes(actualValue);\n    }\n\n    switch (condition.operator) {\n      case 'eq':\n        return actualValue === condition.value;\n      case 'ne':\n        return actualValue !== condition.value;\n      case 'gt':\n        return actualValue > condition.value;\n      case 'lt':\n        return actualValue < condition.value;\n      case 'gte':\n        return actualValue >= condition.value;\n      case 'lte':\n        return actualValue <= condition.value;\n      default:\n        return actualValue === condition.value;\n    }\n  };\n\n  // Determine if field should be displayed\n  const shouldShowField = (field: RequiredInput): boolean => {\n    if (!field.when) {\n      return true;\n    }\n\n    const conditions = Array.isArray(field.when) ? field.when : [field.when];\n    return conditions.every(condition => evaluateCondition(condition));\n  };\n\n  // Get options list\n  const getOptions = (field: RequiredInput): InputOption[] => {\n    if (!field.options) {\n      return [];\n    }\n\n    const options = field.options as any;\n\n    if (Array.isArray(options)) {\n      return options as InputOption[];\n    }\n\n    if (options.type === 'static') {\n      return options.options || [];\n    }\n\n    if (options.type === 'providers') {\n      const providers = presetConfig.Providers || [];\n      return providers.map((p) => ({\n        label: p.name || p.id || String(p),\n        value: p.name || p.id || String(p),\n        description: p.api_base_url,\n      }));\n    }\n\n    if (options.type === 'models') {\n      const providerField = options.providerField;\n      if (!providerField) {\n        return [];\n      }\n\n      const providerId = String(providerField).replace(/^{{(.+)}}$/, '$1');\n      const selectedProvider = values[providerId];\n\n      if (!selectedProvider || !presetConfig.Providers) {\n        return [];\n      }\n\n      const provider = presetConfig.Providers.find(\n        (p) => p.name === selectedProvider || p.id === selectedProvider\n      );\n\n      if (!provider || !provider.models) {\n        return [];\n      }\n\n      return provider.models.map((model: string) => ({\n        label: model,\n        value: model,\n      }));\n    }\n\n    return [];\n  };\n\n  // Update field value\n  const updateValue = (fieldId: string, value: any) => {\n    setValues((prev) => ({\n      ...prev,\n      [fieldId]: value,\n    }));\n    // Clear errors for this field\n    setErrors((prev) => {\n      const newErrors = { ...prev };\n      delete newErrors[fieldId];\n      return newErrors;\n    });\n  };\n\n  // Validate single field\n  const validateField = (field: RequiredInput): string | null => {\n    const value = values[field.id];\n    const fieldName = field.label || field.id;\n\n    // Check required (for confirm type, false is a valid value)\n    const isEmpty = value === undefined || value === null || value === '' ||\n      (Array.isArray(value) && value.length === 0);\n\n    if (field.required !== false && isEmpty) {\n      return t('presets.form.field_required', { field: fieldName });\n    }\n\n    // Type check\n    if (field.type === 'number' && value !== '' && isNaN(Number(value))) {\n      return t('presets.form.must_be_number', { field: fieldName });\n    }\n\n    if (field.type === 'number') {\n      const numValue = Number(value);\n      if (field.min !== undefined && numValue < field.min) {\n        return t('presets.form.must_be_at_least', { field: fieldName, min: field.min });\n      }\n      if (field.max !== undefined && numValue > field.max) {\n        return t('presets.form.must_be_at_most', { field: fieldName, max: field.max });\n      }\n    }\n\n    // Custom validator\n    if (field.validator && value !== '') {\n      if (field.validator instanceof RegExp) {\n        if (!field.validator.test(String(value))) {\n          return t('presets.form.format_invalid', { field: fieldName });\n        }\n      } else if (typeof field.validator === 'string') {\n        const regex = new RegExp(field.validator);\n        if (!regex.test(String(value))) {\n          return t('presets.form.format_invalid', { field: fieldName });\n        }\n      }\n    }\n\n    return null;\n  };\n\n  // Submit form\n  const handleSubmit = (e: React.FormEvent) => {\n    e.preventDefault();\n\n    // Validate all visible fields\n    const newErrors: Record<string, string> = {};\n\n    for (const field of schema) {\n      if (!visibleFields.has(field.id)) {\n        continue;\n      }\n\n      const error = validateField(field);\n      if (error) {\n        newErrors[field.id] = error;\n      }\n    }\n\n    if (Object.keys(newErrors).length > 0) {\n      setErrors(newErrors);\n      return;\n    }\n\n    onSubmit(values);\n  };\n\n  return (\n    <form onSubmit={handleSubmit} className=\"space-y-4\">\n      {schema.map((field) => {\n        if (!visibleFields.has(field.id)) {\n          return null;\n        }\n\n        const label = field.label || field.id;\n        const prompt = field.prompt;\n        const error = errors[field.id];\n\n        return (\n          <div key={field.id} className=\"space-y-2\">\n            <Label htmlFor={`field-${field.id}`}>\n              {label}\n              {field.required !== false && <span className=\"text-red-500 ml-1\">*</span>}\n            </Label>\n\n            {prompt && (\n              <p className=\"text-sm text-gray-600\">{prompt}</p>\n            )}\n\n            {/* Password / Input */}\n            {(field.type === 'password' || field.type === 'input' || !field.type) && (\n              <Input\n                id={`field-${field.id}`}\n                type={field.type === 'password' ? 'password' : 'text'}\n                placeholder={field.placeholder}\n                value={values[field.id] || ''}\n                onChange={(e) => updateValue(field.id, e.target.value)}\n                disabled={isSubmitting}\n              />\n            )}\n\n            {/* Number */}\n            {field.type === 'number' && (\n              <Input\n                id={`field-${field.id}`}\n                type=\"number\"\n                placeholder={field.placeholder}\n                value={values[field.id] || ''}\n                onChange={(e) => updateValue(field.id, Number(e.target.value))}\n                min={field.min}\n                max={field.max}\n                disabled={isSubmitting}\n              />\n            )}\n\n            {/* Select */}\n            {field.type === 'select' && (\n              <Select\n                value={values[field.id] || ''}\n                onValueChange={(value: string) => updateValue(field.id, value)}\n                disabled={isSubmitting}\n              >\n                <SelectTrigger id={`field-${field.id}`}>\n                  <SelectValue placeholder={field.placeholder || t('presets.form.select', { label })} />\n                </SelectTrigger>\n                <SelectContent>\n                  {getOptions(field).map((option) => (\n                    <SelectItem\n                      key={String(option.value)}\n                      value={String(option.value)}\n                      disabled={option.disabled}\n                    >\n                      <div>\n                        <div>{option.label}</div>\n                        {option.description && (\n                          <div className=\"text-xs text-gray-500\">{option.description}</div>\n                        )}\n                      </div>\n                    </SelectItem>\n                  ))}\n                </SelectContent>\n              </Select>\n            )}\n\n            {/* Multiselect */}\n            {field.type === 'multiselect' && (\n              <div className=\"space-y-2\">\n                {getOptions(field).map((option) => (\n                  <div key={String(option.value)} className=\"flex items-center space-x-2\">\n                    <Checkbox\n                      id={`field-${field.id}-${option.value}`}\n                      checked={Array.isArray(values[field.id]) && values[field.id].includes(option.value)}\n                      onCheckedChange={(checked: boolean | 'indeterminate') => {\n                        const current = Array.isArray(values[field.id]) ? values[field.id] : [];\n                        if (checked === true) {\n                          updateValue(field.id, [...current, option.value]);\n                        } else {\n                          updateValue(field.id, current.filter((v: any) => v !== option.value));\n                        }\n                      }}\n                      disabled={isSubmitting || option.disabled}\n                    />\n                    <Label\n                      htmlFor={`field-${field.id}-${option.value}`}\n                      className=\"text-sm font-normal cursor-pointer\"\n                    >\n                      {option.label}\n                      {option.description && (\n                        <span className=\"text-gray-500 ml-2\">{option.description}</span>\n                      )}\n                    </Label>\n                  </div>\n                ))}\n              </div>\n            )}\n\n            {/* Confirm */}\n            {field.type === 'confirm' && (\n              <div className=\"flex items-center space-x-2\">\n                <Checkbox\n                  id={`field-${field.id}`}\n                  checked={values[field.id] || false}\n                  onCheckedChange={(checked: boolean | 'indeterminate') => updateValue(field.id, checked)}\n                  disabled={isSubmitting}\n                />\n                <Label htmlFor={`field-${field.id}`} className=\"text-sm font-normal cursor-pointer\">\n                  {field.prompt || label}\n                </Label>\n              </div>\n            )}\n\n            {/* Editor */}\n            {field.type === 'editor' && (\n              <Textarea\n                id={`field-${field.id}`}\n                placeholder={field.placeholder}\n                value={values[field.id] || ''}\n                onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => updateValue(field.id, e.target.value)}\n                rows={field.rows || 5}\n                disabled={isSubmitting}\n              />\n            )}\n\n            {error && (\n              <p className=\"text-sm text-red-500\">{error}</p>\n            )}\n          </div>\n        );\n      })}\n\n      <div className=\"flex justify-end gap-2 pt-4\">\n        <Button\n          type=\"button\"\n          variant=\"outline\"\n          onClick={onCancel}\n          disabled={isSubmitting}\n        >\n          {t('app.cancel')}\n        </Button>\n        <Button type=\"submit\" disabled={isSubmitting}>\n          {isSubmitting ? (\n            <>\n              <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n              {t('presets.form.saving')}\n            </>\n          ) : (\n            t('app.save')\n          )}\n        </Button>\n      </div>\n    </form>\n  );\n}\n"
  },
  {
    "path": "packages/ui/src/components/ui/badge.tsx",
    "content": "\nimport * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst badgeVariants = cva(\n  \"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2\",\n  {\n    variants: {\n      variant: {\n        default:\n          \"border-transparent bg-primary text-primary-foreground hover:bg-primary/80\",\n        secondary:\n          \"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n        destructive:\n          \"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80\",\n        outline: \"text-foreground\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  }\n)\n\nexport interface BadgeProps\n  extends React.HTMLAttributes<HTMLDivElement>,\n    VariantProps<typeof badgeVariants> {}\n\nfunction Badge({ className, variant, ...props }: BadgeProps) {\n  return (\n    <div className={cn(badgeVariants({ variant }), className)} {...props} />\n  )\n}\n\n// eslint-disable-next-line react-refresh/only-export-components\nexport { Badge, badgeVariants }\n"
  },
  {
    "path": "packages/ui/src/components/ui/button.tsx",
    "content": "import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst buttonVariants = cva(\n  \"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n        destructive:\n          \"border border-destructive/50 text-destructive hover:bg-destructive hover:text-destructive-foreground\",\n        outline:\n          \"border border-input bg-background hover:bg-accent hover:text-accent-foreground\",\n        secondary:\n          \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n        ghost: \"hover:bg-accent hover:text-accent-foreground\",\n        link: \"text-primary underline-offset-4 hover:underline\",\n      },\n      size: {\n        default: \"h-10 px-4 py-2\",\n        sm: \"h-9 rounded-md px-3\",\n        lg: \"h-11 rounded-md px-8\",\n        icon: \"h-10 w-10\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  }\n)\n\nexport interface ButtonProps\n  extends React.ButtonHTMLAttributes<HTMLButtonElement>,\n    VariantProps<typeof buttonVariants> {\n  asChild?: boolean\n}\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n  ({ className, variant, size, asChild = false, ...props }, ref) => {\n    const Comp = asChild ? Slot : \"button\"\n    return (\n      <Comp\n        className={cn(buttonVariants({ variant, size, className }))}\n        ref={ref}\n        {...props}\n      />\n    )\n  }\n)\nButton.displayName = \"Button\"\n\n// eslint-disable-next-line react-refresh/only-export-components\nexport { Button, buttonVariants }\n"
  },
  {
    "path": "packages/ui/src/components/ui/card.tsx",
    "content": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Card = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\n      \"rounded-lg border bg-card text-card-foreground shadow-sm\",\n      className\n    )}\n    {...props}\n  />\n))\nCard.displayName = \"Card\"\n\nconst CardHeader = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\"flex flex-col space-y-1.5 p-6\", className)}\n    {...props}\n  />\n))\nCardHeader.displayName = \"CardHeader\"\n\nconst CardTitle = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\n      \"text-2xl font-semibold leading-none tracking-tight\",\n      className\n    )}\n    {...props}\n  />\n))\nCardTitle.displayName = \"CardTitle\"\n\nconst CardDescription = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\"text-sm text-muted-foreground\", className)}\n    {...props}\n  />\n))\nCardDescription.displayName = \"CardDescription\"\n\nconst CardContent = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div ref={ref} className={cn(\"p-6 pt-0\", className)} {...props} />\n))\nCardContent.displayName = \"CardContent\"\n\nconst CardFooter = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\"flex items-center p-6 pt-0\", className)}\n    {...props}\n  />\n))\nCardFooter.displayName = \"CardFooter\"\n\nexport { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }\n"
  },
  {
    "path": "packages/ui/src/components/ui/checkbox.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as CheckboxPrimitives from \"@radix-ui/react-checkbox\"\nimport { Check } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Checkbox = React.forwardRef<\n  React.ElementRef<typeof CheckboxPrimitives.Root>,\n  React.ComponentPropsWithoutRef<typeof CheckboxPrimitives.Root>\n>(({ className, ...props }, ref) => (\n  <CheckboxPrimitives.Root\n    ref={ref}\n    className={cn(\n      \"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground\",\n      className\n    )}\n    {...props}\n  >\n    <CheckboxPrimitives.Indicator\n      className={cn(\"flex items-center justify-center text-current\")}\n    >\n      <Check className=\"h-4 w-4\" />\n    </CheckboxPrimitives.Indicator>\n  </CheckboxPrimitives.Root>\n))\nCheckbox.displayName = CheckboxPrimitives.Root.displayName\n\nexport { Checkbox }\n"
  },
  {
    "path": "packages/ui/src/components/ui/color-picker.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { useTranslation } from \"react-i18next\"\nimport { HexColorPicker } from \"react-colorful\"\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport { Input } from \"@/components/ui/input\"\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\"\n\ninterface ColorPickerProps {\n  value?: string;\n  onChange: (value: string) => void;\n  placeholder?: string;\n  showPreview?: boolean;\n}\n\n// 获取颜色值的函数\nconst getColorValue = (color: string): string => {\n  // 如果是十六进制颜色\n  if (color.startsWith(\"#\")) {\n    return color\n  }\n  \n  // 默认返回黑色\n  return \"#000000\"\n}\n\nexport function ColorPicker({\n  value = \"\",\n  onChange,\n  placeholder,\n  showPreview = true\n}: ColorPickerProps) {\n  const { t } = useTranslation()\n  const [open, setOpen] = React.useState(false)\n  const [customColor, setCustomColor] = React.useState(\"\")\n  \n  // 当value变化时更新customColor\n  React.useEffect(() => {\n    if (value.startsWith(\"#\")) {\n      setCustomColor(value)\n    } else {\n      setCustomColor(\"\")\n    }\n  }, [value])\n\n  const handleColorChange = (color: string) => {\n    onChange(color)\n  }\n\n  const handleCustomColorChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    const color = e.target.value\n    setCustomColor(color)\n    // 验证十六进制颜色格式\n    if (/^#[0-9A-F]{6}$/i.test(color)) {\n      handleColorChange(color)\n    }\n  }\n\n  \n  const selectedColorValue = getColorValue(value)\n\n  return (\n    <div className=\"space-y-2\">\n      <Popover open={open} onOpenChange={setOpen}>\n        <PopoverTrigger asChild>\n          <Button\n            variant=\"outline\"\n            className={cn(\n              \"w-full justify-start text-left font-normal h-10 transition-all hover:scale-[1.02] active:scale-[0.98]\",\n              !value && \"text-muted-foreground\"\n            )}\n          >\n            <div className=\"flex items-center gap-2 w-full\">\n              {showPreview && (\n                <div \n                  className=\"h-5 w-5 rounded border shadow-sm\" \n                  style={{ backgroundColor: selectedColorValue }}\n                />\n              )}\n              <span className=\"truncate flex-1\">\n                {value || placeholder || t('color_picker.placeholder')}\n              </span>\n              <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n                <path d=\"m7 15 5 5 5-5\"/>\n                <path d=\"m7 9 5-5 5 5\"/>\n              </svg>\n            </div>\n          </Button>\n        </PopoverTrigger>\n        <PopoverContent className=\"w-72 p-3\" align=\"start\">\n          <div className=\"space-y-4\">\n            {/* 颜色选择器标题 */}\n            <div className=\"flex items-center justify-between\">\n              <h4 className=\"text-sm font-semibold\">{t('color_picker.title')}</h4>\n              <Button\n                variant=\"ghost\"\n                size=\"sm\"\n                className=\"h-6 px-2 text-xs\"\n                onClick={() => handleColorChange(\"\")}\n              >\n                {t('color_picker.clear')}\n              </Button>\n            </div>\n            \n            {/* 颜色预览 */}\n            <div className=\"flex items-center gap-2 p-2 rounded-md bg-secondary\">\n              <div \n                className=\"h-8 w-8 rounded border shadow-sm\" \n                style={{ backgroundColor: selectedColorValue }}\n              />\n              <div className=\"flex-1 min-w-0\">\n                <div className=\"text-sm font-medium truncate\">\n                  {value || t('color_picker.no_color_selected')}\n                </div>\n                {value && value.startsWith(\"#\") && (\n                  <div className=\"text-xs text-muted-foreground font-mono\">\n                    {value.toUpperCase()}\n                  </div>\n                )}\n              </div>\n            </div>\n            \n            {/* 颜色选择器 */}\n            <div className=\"rounded-md overflow-hidden border\">\n              <HexColorPicker \n                color={selectedColorValue} \n                onChange={handleColorChange} \n                className=\"w-full\" \n              />\n            </div>\n            \n            {/* 自定义颜色输入 */}\n            <div className=\"space-y-2\">\n              <label className=\"text-sm font-medium\">{t('color_picker.custom_color')}</label>\n              <div className=\"flex gap-2\">\n                <Input\n                  type=\"text\"\n                  value={customColor}\n                  onChange={handleCustomColorChange}\n                  placeholder=\"#RRGGBB\"\n                  className=\"font-mono flex-1\"\n                />\n                <Button\n                  size=\"sm\"\n                  onClick={() => {\n                    if (customColor && /^#[0-9A-F]{6}$/i.test(customColor)) {\n                      handleColorChange(customColor)\n                      setOpen(false)\n                    }\n                  }}\n                  disabled={!customColor || !/^#[0-9A-F]{6}$/i.test(customColor)}\n                >\n                  {t('color_picker.apply')}\n                </Button>\n              </div>\n              <p className=\"text-xs text-muted-foreground\">\n                {t('color_picker.hex_input_help')}\n              </p>\n            </div>\n          </div>\n        </PopoverContent>\n      </Popover>\n    </div>\n  )\n}\n"
  },
  {
    "path": "packages/ui/src/components/ui/combo-input.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Check, ChevronsUpDown } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport { Input } from \"@/components/ui/input\"\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from \"@/components/ui/command\"\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\"\n\ninterface ComboInputProps {\n  options: { label: string; value: string }[];\n  value?: string;\n  onChange: (value: string) => void;\n  onEnter?: (value: string) => void;\n  searchPlaceholder?: string;\n  emptyPlaceholder?: string;\n  inputPlaceholder?: string;\n}\n\nexport const ComboInput = React.forwardRef<HTMLInputElement, ComboInputProps>(({\n  options,\n  value,\n  onChange,\n  onEnter,\n  searchPlaceholder = \"Search...\",\n  emptyPlaceholder = \"No options found.\",\n  inputPlaceholder = \"Type or select...\",\n}, ref) => {\n  const [open, setOpen] = React.useState(false)\n  const [inputValue, setInputValue] = React.useState(value || \"\")\n  const internalInputRef = React.useRef<HTMLInputElement>(null)\n\n  // Forward ref to the internal input\n  React.useImperativeHandle(ref, () => internalInputRef.current as HTMLInputElement)\n\n  React.useEffect(() => {\n    setInputValue(value || \"\")\n  }, [value])\n\n  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    const newValue = e.target.value\n    setInputValue(newValue)\n    onChange(newValue)\n  }\n\n  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {\n    if (e.key === 'Enter' && inputValue.trim() && onEnter) {\n      onEnter(inputValue.trim())\n      setInputValue(\"\")\n    }\n  }\n\n  const handleSelect = (selectedValue: string) => {\n    setInputValue(selectedValue)\n    onChange(selectedValue)\n    if (onEnter) {\n      onEnter(selectedValue)\n      setInputValue(\"\")\n    }\n    setOpen(false)\n  }\n\n  // Function to get current value for external access\n  const getCurrentValue = () => inputValue\n\n  // Expose methods through the ref\n  React.useImperativeHandle(ref, () => ({\n    ...internalInputRef.current!,\n    value: inputValue,\n    getCurrentValue,\n    clearInput: () => {\n      setInputValue(\"\")\n      onChange(\"\")\n    }\n  }))\n\n  return (\n    <div className=\"relative\">\n      <Input\n        ref={internalInputRef}\n        value={inputValue}\n        onChange={handleInputChange}\n        onKeyDown={handleKeyDown}\n        placeholder={inputPlaceholder}\n        className=\"pr-10\"\n      />\n      <Popover open={open} onOpenChange={setOpen}>\n        <PopoverTrigger asChild>\n          <Button\n            variant=\"ghost\"\n            size=\"sm\"\n            className=\"absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent\"\n          >\n            <ChevronsUpDown className=\"h-4 w-4 opacity-50\" />\n          </Button>\n        </PopoverTrigger>\n        <PopoverContent className=\"w-[--radix-popover-trigger-width] p-0 animate-fade-in\">\n          <Command>\n            <CommandInput placeholder={searchPlaceholder} />\n            <CommandList>\n              <CommandEmpty>{emptyPlaceholder}</CommandEmpty>\n              <CommandGroup>\n                {options.map((option) => (\n                  <CommandItem\n                    key={option.value}\n                    value={option.value}\n                    onSelect={() => handleSelect(option.value)}\n                    className=\"transition-all-ease hover:bg-accent hover:text-accent-foreground\"\n                  >\n                    <Check\n                      className={cn(\n                        \"mr-2 h-4 w-4 transition-opacity\",\n                        value === option.value ? \"opacity-100\" : \"opacity-0\"\n                      )}\n                    />\n                    {option.label}\n                  </CommandItem>\n                ))}\n              </CommandGroup>\n            </CommandList>\n          </Command>\n        </PopoverContent>\n      </Popover>\n    </div>\n  )\n})"
  },
  {
    "path": "packages/ui/src/components/ui/combobox.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Check, ChevronsUpDown } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from \"@/components/ui/command\"\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\"\n\ninterface ComboboxProps {\n  options: { label: string; value: string }[];\n  value?: string;\n  onChange: (value: string) => void;\n  placeholder?: string;\n  searchPlaceholder?: string;\n  emptyPlaceholder?: string;\n}\n\nexport function Combobox({\n  options,\n  value,\n  onChange,\n  placeholder = \"Select an option...\",\n  searchPlaceholder = \"Search...\",\n  emptyPlaceholder = \"No options found.\",\n}: ComboboxProps) {\n  const [open, setOpen] = React.useState(false)\n\n  const selectedOption = options.find((option) => option.value === value)\n\n  return (\n    <Popover open={open} onOpenChange={setOpen}>\n      <PopoverTrigger asChild>\n        <Button\n          variant=\"outline\"\n          role=\"combobox\"\n          aria-expanded={open}\n          className=\"w-full justify-between transition-all-ease hover:scale-[1.02] active:scale-[0.98]\"\n        >\n          {selectedOption ? selectedOption.label : placeholder}\n          <ChevronsUpDown className=\"ml-2 h-4 w-4 shrink-0 opacity-50 transition-transform duration-200 group-data-[state=open]:rotate-180\" />\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent className=\"w-[--radix-popover-trigger-width] p-0 animate-fade-in\">\n        <Command>\n          <CommandInput placeholder={searchPlaceholder} />\n          <CommandList>\n            <CommandEmpty>{emptyPlaceholder}</CommandEmpty>\n            <CommandGroup>\n              {options.map((option) => (\n                <CommandItem\n                  key={option.value}\n                  value={option.value}\n                  onSelect={(currentValue) => {\n                    onChange(currentValue === value ? \"\" : currentValue)\n                    setOpen(false)\n                  }}\n                  className=\"transition-all-ease hover:bg-accent hover:text-accent-foreground\"\n                >\n                  <Check\n                    className={cn(\n                      \"mr-2 h-4 w-4 transition-opacity\",\n                      value === option.value ? \"opacity-100\" : \"opacity-0\"\n                    )}\n                  />\n                  {option.label}\n                </CommandItem>\n              ))}\n            </CommandGroup>\n          </CommandList>\n        </Command>\n      </PopoverContent>\n    </Popover>\n  )\n}"
  },
  {
    "path": "packages/ui/src/components/ui/command.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Command as CommandPrimitive } from \"cmdk\"\nimport { SearchIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogHeader,\n  DialogTitle,\n} from \"@/components/ui/dialog\"\n\nfunction Command({\n  className,\n  ...props\n}: React.ComponentProps<typeof CommandPrimitive>) {\n  return (\n    <CommandPrimitive\n      data-slot=\"command\"\n      className={cn(\n        \"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction CommandDialog({\n  title = \"Command Palette\",\n  description = \"Search for a command to run...\",\n  children,\n  className,\n  ...props\n}: React.ComponentProps<typeof Dialog> & {\n  title?: string\n  description?: string\n  className?: string\n}) {\n  return (\n    <Dialog {...props}>\n      <DialogHeader className=\"sr-only\">\n        <DialogTitle>{title}</DialogTitle>\n        <DialogDescription>{description}</DialogDescription>\n      </DialogHeader>\n      <DialogContent\n        className={cn(\"overflow-hidden p-0\", className)}\n      >\n        <Command className=\"[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5\">\n          {children}\n        </Command>\n      </DialogContent>\n    </Dialog>\n  )\n}\n\nfunction CommandInput({\n  className,\n  ...props\n}: React.ComponentProps<typeof CommandPrimitive.Input>) {\n  return (\n    <div\n      data-slot=\"command-input-wrapper\"\n      className=\"flex h-9 items-center gap-2 border-b px-3\"\n    >\n      <SearchIcon className=\"size-4 shrink-0 opacity-50\" />\n      <CommandPrimitive.Input\n        data-slot=\"command-input\"\n        className={cn(\n          \"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50\",\n          className\n        )}\n        {...props}\n      />\n    </div>\n  )\n}\n\nfunction CommandList({\n  className,\n  ...props\n}: React.ComponentProps<typeof CommandPrimitive.List>) {\n  return (\n    <CommandPrimitive.List\n      data-slot=\"command-list\"\n      className={cn(\n        \"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction CommandEmpty({\n  ...props\n}: React.ComponentProps<typeof CommandPrimitive.Empty>) {\n  return (\n    <CommandPrimitive.Empty\n      data-slot=\"command-empty\"\n      className=\"py-6 text-center text-sm\"\n      {...props}\n    />\n  )\n}\n\nfunction CommandGroup({\n  className,\n  ...props\n}: React.ComponentProps<typeof CommandPrimitive.Group>) {\n  return (\n    <CommandPrimitive.Group\n      data-slot=\"command-group\"\n      className={cn(\n        \"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction CommandSeparator({\n  className,\n  ...props\n}: React.ComponentProps<typeof CommandPrimitive.Separator>) {\n  return (\n    <CommandPrimitive.Separator\n      data-slot=\"command-separator\"\n      className={cn(\"bg-border -mx-1 h-px\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction CommandItem({\n  className,\n  ...props\n}: React.ComponentProps<typeof CommandPrimitive.Item>) {\n  return (\n    <CommandPrimitive.Item\n      data-slot=\"command-item\"\n      className={cn(\n        \"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction CommandShortcut({\n  className,\n  ...props\n}: React.ComponentProps<\"span\">) {\n  return (\n    <span\n      data-slot=\"command-shortcut\"\n      className={cn(\n        \"text-muted-foreground ml-auto text-xs tracking-widest\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nexport {\n  Command,\n  CommandDialog,\n  CommandInput,\n  CommandList,\n  CommandEmpty,\n  CommandGroup,\n  CommandItem,\n  CommandShortcut,\n  CommandSeparator,\n}\n"
  },
  {
    "path": "packages/ui/src/components/ui/dialog.tsx",
    "content": "\nimport * as React from \"react\"\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\"\nimport { X } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Dialog = DialogPrimitive.Root\n\nconst DialogTrigger = DialogPrimitive.Trigger\n\nconst DialogPortal = DialogPrimitive.Portal\n\nconst DialogClose = DialogPrimitive.Close\n\nconst DialogOverlay = React.forwardRef<(\n  React.ElementRef<typeof DialogPrimitive.Overlay>\n), (\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>\n)>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Overlay\n    ref={ref}\n    className={cn(\n      \"fixed inset-0 z-50 bg-black/80  data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n      className\n    )}\n    {...props}\n  />\n))\nDialogOverlay.displayName = DialogPrimitive.Overlay.displayName\n\nconst DialogContent = React.forwardRef<(\n  React.ElementRef<typeof DialogPrimitive.Content>\n), (\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>\n)>(({ className, children, ...props }, ref) => (\n  <DialogPortal>\n    <DialogOverlay />\n    <DialogPrimitive.Content\n      ref={ref}\n      className={cn(\n        \"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-300 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg animate-scale-in\",\n        className\n      )}\n      {...props}\n    >\n      {children}\n      <DialogPrimitive.Close className=\"absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground transition-all-ease hover:scale-110\">\n        <X className=\"h-4 w-4\" />\n        <span className=\"sr-only\">Close</span>\n      </DialogPrimitive.Close>\n    </DialogPrimitive.Content>\n  </DialogPortal>\n))\nDialogContent.displayName = DialogPrimitive.Content.displayName\n\nconst DialogHeader = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      \"flex flex-col space-y-1.5 text-center sm:text-left\",\n      className\n    )}\n    {...props}\n  />\n)\nDialogHeader.displayName = \"DialogHeader\"\n\nconst DialogFooter = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      \"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2\",\n      className\n    )}\n    {...props}\n  />\n)\nDialogFooter.displayName = \"DialogFooter\"\n\nconst DialogTitle = React.forwardRef<(\n  React.ElementRef<typeof DialogPrimitive.Title>\n), (\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>\n)>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Title\n    ref={ref}\n    className={cn(\n      \"text-lg font-semibold leading-none tracking-tight\",\n      className\n    )}\n    {...props}\n  />\n))\nDialogTitle.displayName = DialogPrimitive.Title.displayName\n\nconst DialogDescription = React.forwardRef<(\n  React.ElementRef<typeof DialogPrimitive.Description>\n), (\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>\n)>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Description\n    ref={ref}\n    className={cn(\"text-sm text-muted-foreground\", className)}\n    {...props}\n  />\n))\nDialogDescription.displayName = DialogPrimitive.Description.displayName\n\nexport {\n  Dialog,\n  DialogPortal,\n  DialogOverlay,\n  DialogTrigger,\n  DialogClose,\n  DialogContent,\n  DialogHeader,\n  DialogFooter,\n  DialogTitle,\n  DialogDescription,\n}\n"
  },
  {
    "path": "packages/ui/src/components/ui/input.tsx",
    "content": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Input = React.forwardRef<HTMLInputElement, React.ComponentProps<\"input\">>(\n  ({ className, type, ...props }, ref) => {\n    const isNumeric = type === \"number\";\n    const [tempValue, setTempValue] = React.useState(props.value?.toString() || '');\n    \n    React.useEffect(() => {\n      if (props.value !== undefined) {\n        setTempValue(props.value.toString());\n      }\n    }, [props.value]);\n\n    const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n      const newValue = e.target.value;\n      \n      if (isNumeric) {\n        // Only allow empty string or numbers for numeric input\n        if (newValue === '' || /^\\d+$/.test(newValue)) {\n          setTempValue(newValue);\n          // Only call onChange if the value is not empty\n          if (props.onChange && newValue !== '') {\n            props.onChange(e);\n          }\n        }\n      } else {\n        setTempValue(newValue);\n        if (props.onChange) {\n          props.onChange(e);\n        }\n      }\n    };\n\n    const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {\n      if (isNumeric && tempValue === '') {\n        const defaultValue = props.placeholder || \"1\";\n        setTempValue(defaultValue);\n        \n        // Create a synthetic event for the corrected value\n        if (props.onChange) {\n          const syntheticEvent = {\n            ...e,\n            target: { ...e.target, value: defaultValue }\n          } as React.ChangeEvent<HTMLInputElement>;\n          \n          props.onChange(syntheticEvent);\n        }\n      }\n      \n      if (props.onBlur) {\n        props.onBlur(e);\n      }\n    };\n\n    // For numeric inputs, use text type and manage value internally\n    const inputType = isNumeric ? \"text\" : type;\n    const inputValue = isNumeric ? tempValue : props.value;\n\n    return (\n      <input\n        {...props}\n        type={inputType}\n        value={inputValue}\n        onChange={handleChange}\n        onBlur={handleBlur}\n        className={cn(\n          \"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm\",\n          className\n        )}\n        ref={ref}\n      />\n    )\n  }\n)\nInput.displayName = \"Input\"\n\nexport { Input }\n"
  },
  {
    "path": "packages/ui/src/components/ui/label.tsx",
    "content": "import * as React from \"react\"\nimport * as LabelPrimitive from \"@radix-ui/react-label\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst labelVariants = cva(\n  \"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\"\n)\n\nconst Label = React.forwardRef<\n  React.ElementRef<typeof LabelPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &\n    VariantProps<typeof labelVariants>\n>(({ className, ...props }, ref) => (\n  <LabelPrimitive.Root\n    ref={ref}\n    className={cn(labelVariants(), className)}\n    {...props}\n  />\n))\nLabel.displayName = LabelPrimitive.Root.displayName\n\nexport { Label }\n"
  },
  {
    "path": "packages/ui/src/components/ui/multi-combobox.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Check, ChevronsUpDown, X } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from \"@/components/ui/command\"\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\"\nimport { Badge } from \"@/components/ui/badge\"\n\ninterface MultiComboboxProps {\n  options: { label: string; value: string }[];\n  value?: string[];\n  onChange: (value: string[]) => void;\n  placeholder?: string;\n  searchPlaceholder?: string;\n  emptyPlaceholder?: string;\n}\n\nexport function MultiCombobox({\n  options,\n  value = [],\n  onChange,\n  placeholder = \"Select options...\",\n  searchPlaceholder = \"Search...\",\n  emptyPlaceholder = \"No options found.\",\n}: MultiComboboxProps) {\n  const [open, setOpen] = React.useState(false)\n  \n  const handleSelect = (currentValue: string) => {\n    if (value.includes(currentValue)) {\n      onChange(value.filter(v => v !== currentValue))\n    } else {\n      onChange([...value, currentValue])\n    }\n  }\n  \n  const removeValue = (val: string, e: React.MouseEvent) => {\n    e.stopPropagation()\n    onChange(value.filter(v => v !== val))\n  }\n\n  return (\n    <div className=\"space-y-2\">\n      <div className=\"flex flex-wrap gap-1\">\n        {value.map((val) => {\n          const option = options.find(opt => opt.value === val)\n          return (\n            <Badge key={val} variant=\"outline\" className=\"font-normal\">\n              {option?.label || val}\n              <button\n                onClick={(e) => removeValue(val, e)}\n                className=\"ml-1 rounded-full hover:bg-gray-200\"\n              >\n                <X className=\"h-3 w-3\" />\n              </button>\n            </Badge>\n          )\n        })}\n      </div>\n      <Popover open={open} onOpenChange={setOpen}>\n        <PopoverTrigger asChild>\n          <Button\n            variant=\"outline\"\n            role=\"combobox\"\n            aria-expanded={open}\n            className=\"w-full justify-between transition-all-ease hover:scale-[1.02] active:scale-[0.98]\"\n          >\n            {value.length > 0 ? `${value.length} selected` : placeholder}\n            <ChevronsUpDown className=\"ml-2 h-4 w-4 shrink-0 opacity-50 transition-transform duration-200 group-data-[state=open]:rotate-180\" />\n          </Button>\n        </PopoverTrigger>\n        <PopoverContent className=\"w-[--radix-popover-trigger-width] p-0 animate-fade-in\">\n          <Command>\n            <CommandInput placeholder={searchPlaceholder} />\n            <CommandList>\n              <CommandEmpty>{emptyPlaceholder}</CommandEmpty>\n              <CommandGroup>\n                {options.map((option) => (\n                  <CommandItem\n                    key={option.value}\n                    value={option.value}\n                    onSelect={() => handleSelect(option.value)}\n                    className=\"transition-all-ease hover:bg-accent hover:text-accent-foreground\"\n                  >\n                    <Check\n                      className={cn(\n                        \"mr-2 h-4 w-4 transition-opacity\",\n                        value.includes(option.value) ? \"opacity-100\" : \"opacity-0\"\n                      )}\n                    />\n                    {option.label}\n                  </CommandItem>\n                ))}\n              </CommandGroup>\n            </CommandList>\n          </Command>\n        </PopoverContent>\n      </Popover>\n    </div>\n  )\n}"
  },
  {
    "path": "packages/ui/src/components/ui/popover.tsx",
    "content": "import * as React from \"react\"\nimport * as PopoverPrimitive from \"@radix-ui/react-popover\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Popover({\n  ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Root>) {\n  return <PopoverPrimitive.Root data-slot=\"popover\" {...props} />\n}\n\nfunction PopoverTrigger({\n  ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {\n  return <PopoverPrimitive.Trigger data-slot=\"popover-trigger\" {...props} />\n}\n\nfunction PopoverContent({\n  className,\n  align = \"center\",\n  sideOffset = 4,\n  ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Content>) {\n  return (\n    <PopoverPrimitive.Portal>\n      <PopoverPrimitive.Content\n        data-slot=\"popover-content\"\n        align={align}\n        sideOffset={sideOffset}\n        className={cn(\n          \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden animate-fade-in\",\n          className\n        )}\n        {...props}\n      />\n    </PopoverPrimitive.Portal>\n  )\n}\n\nfunction PopoverAnchor({\n  ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {\n  return <PopoverPrimitive.Anchor data-slot=\"popover-anchor\" {...props} />\n}\n\nexport { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }\n"
  },
  {
    "path": "packages/ui/src/components/ui/select.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as SelectPrimitive from \"@radix-ui/react-select\"\nimport { Check, ChevronDown, ChevronUp } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Select = SelectPrimitive.Root\n\nconst SelectGroup = SelectPrimitive.Group\n\nconst SelectValue = SelectPrimitive.Value\n\nconst SelectTrigger = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>\n>(({ className, children, ...props }, ref) => (\n  <SelectPrimitive.Trigger\n    ref={ref}\n    className={cn(\n      \"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1\",\n      className\n    )}\n    {...props}\n  >\n    {children}\n    <SelectPrimitive.Icon asChild>\n      <ChevronDown className=\"h-4 w-4 opacity-50\" />\n    </SelectPrimitive.Icon>\n  </SelectPrimitive.Trigger>\n))\nSelectTrigger.displayName = SelectPrimitive.Trigger.displayName\n\nconst SelectScrollUpButton = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.ScrollUpButton\n    ref={ref}\n    className={cn(\n      \"flex cursor-default items-center justify-center py-1\",\n      className\n    )}\n    {...props}\n  >\n    <ChevronUp className=\"h-4 w-4\" />\n  </SelectPrimitive.ScrollUpButton>\n))\nSelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName\n\nconst SelectScrollDownButton = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.ScrollDownButton\n    ref={ref}\n    className={cn(\n      \"flex cursor-default items-center justify-center py-1\",\n      className\n    )}\n    {...props}\n  >\n    <ChevronDown className=\"h-4 w-4\" />\n  </SelectPrimitive.ScrollDownButton>\n))\nSelectScrollDownButton.displayName =\n  SelectPrimitive.ScrollDownButton.displayName\n\nconst SelectContent = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>\n>(({ className, children, position = \"popper\", ...props }, ref) => (\n  <SelectPrimitive.Portal>\n    <SelectPrimitive.Content\n      ref={ref}\n      className={cn(\n        \"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n        position === \"popper\" &&\n          \"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1\",\n        className\n      )}\n      position={position}\n      {...props}\n    >\n      <SelectScrollUpButton />\n      <SelectPrimitive.Viewport\n        className={cn(\n          \"p-1\",\n          position === \"popper\" &&\n            \"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]\"\n        )}\n      >\n        {children}\n      </SelectPrimitive.Viewport>\n      <SelectScrollDownButton />\n    </SelectPrimitive.Content>\n  </SelectPrimitive.Portal>\n))\nSelectContent.displayName = SelectPrimitive.Content.displayName\n\nconst SelectLabel = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.Label\n    ref={ref}\n    className={cn(\"py-1.5 pl-8 pr-2 text-sm font-semibold\", className)}\n    {...props}\n  />\n))\nSelectLabel.displayName = SelectPrimitive.Label.displayName\n\nconst SelectItem = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>\n>(({ className, children, ...props }, ref) => (\n  <SelectPrimitive.Item\n    ref={ref}\n    className={cn(\n      \"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      className\n    )}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <SelectPrimitive.ItemIndicator>\n        <Check className=\"h-4 w-4\" />\n      </SelectPrimitive.ItemIndicator>\n    </span>\n\n    <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n  </SelectPrimitive.Item>\n))\nSelectItem.displayName = SelectPrimitive.Item.displayName\n\nconst SelectSeparator = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.Separator\n    ref={ref}\n    className={cn(\"-mx-1 my-1 h-px bg-muted\", className)}\n    {...props}\n  />\n))\nSelectSeparator.displayName = SelectPrimitive.Separator.displayName\n\nexport {\n  Select,\n  SelectGroup,\n  SelectValue,\n  SelectTrigger,\n  SelectContent,\n  SelectLabel,\n  SelectItem,\n  SelectSeparator,\n  SelectScrollUpButton,\n  SelectScrollDownButton,\n}\n"
  },
  {
    "path": "packages/ui/src/components/ui/switch.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as SwitchPrimitives from \"@radix-ui/react-switch\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Switch = React.forwardRef<\n  React.ElementRef<typeof SwitchPrimitives.Root>,\n  React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>\n>(({ className, ...props }, ref) => (\n  <SwitchPrimitives.Root\n    className={cn(\n      \"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-all-ease focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input\",\n      className\n    )}\n    {...props}\n    ref={ref}\n  >\n    <SwitchPrimitives.Thumb\n      className={cn(\n        \"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0 transition-all-ease\"\n      )}\n    />\n  </SwitchPrimitives.Root>\n))\nSwitch.displayName = SwitchPrimitives.Root.displayName\n\nexport { Switch }\n"
  },
  {
    "path": "packages/ui/src/components/ui/tabs.tsx",
    "content": "import * as React from \"react\"\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Tabs = TabsPrimitive.Root\n\nconst TabsList = React.forwardRef<\n  React.ElementRef<typeof TabsPrimitive.List>,\n  React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>\n>(({ className, ...props }, ref) => (\n  <TabsPrimitive.List\n    ref={ref}\n    className={cn(\n      \"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground\",\n      className\n    )}\n    {...props}\n  />\n))\nTabsList.displayName = TabsPrimitive.List.displayName\n\nconst TabsTrigger = React.forwardRef<\n  React.ElementRef<typeof TabsPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>\n>(({ className, ...props }, ref) => (\n  <TabsPrimitive.Trigger\n    ref={ref}\n    className={cn(\n      \"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm\",\n      className\n    )}\n    {...props}\n  />\n))\nTabsTrigger.displayName = TabsPrimitive.Trigger.displayName\n\nconst TabsContent = React.forwardRef<\n  React.ElementRef<typeof TabsPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>\n>(({ className, ...props }, ref) => (\n  <TabsPrimitive.Content\n    ref={ref}\n    className={cn(\n      \"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n      className\n    )}\n    {...props}\n  />\n))\nTabsContent.displayName = TabsPrimitive.Content.displayName\n\nexport { Tabs, TabsList, TabsTrigger, TabsContent }"
  },
  {
    "path": "packages/ui/src/components/ui/textarea.tsx",
    "content": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport interface TextareaProps\n  extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}\n\nconst Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(\n  ({ className, ...props }, ref) => {\n    return (\n      <textarea\n        className={cn(\n          \"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50\",\n          className\n        )}\n        ref={ref}\n        {...props}\n      />\n    )\n  }\n)\nTextarea.displayName = \"Textarea\"\n\nexport { Textarea }\n"
  },
  {
    "path": "packages/ui/src/components/ui/toast.tsx",
    "content": "import { useEffect } from 'react';\nimport { CheckCircle, XCircle, AlertCircle, X } from 'lucide-react';\n\ninterface ToastProps {\n  message: string;\n  type: 'success' | 'error' | 'warning';\n  onClose: () => void;\n}\n\nexport function Toast({ message, type, onClose }: ToastProps) {\n  useEffect(() => {\n    const timer = setTimeout(() => {\n      onClose();\n    }, 3000);\n\n    return () => clearTimeout(timer);\n  }, [onClose]);\n\n  const getIcon = () => {\n    switch (type) {\n      case 'success':\n        return <CheckCircle className=\"h-5 w-5 text-green-500\" />;\n      case 'error':\n        return <XCircle className=\"h-5 w-5 text-red-500\" />;\n      case 'warning':\n        return <AlertCircle className=\"h-5 w-5 text-yellow-500\" />;\n      default:\n        return null;\n    }\n  };\n\n  const getBackgroundColor = () => {\n    switch (type) {\n      case 'success':\n        return 'bg-green-100 border-green-200';\n      case 'error':\n        return 'bg-red-100 border-red-200';\n      case 'warning':\n        return 'bg-yellow-100 border-yellow-200';\n      default:\n        return 'bg-gray-100 border-gray-200';\n    }\n  };\n\n  return (\n    <div className={`fixed top-4 right-4 z-[100] flex items-center justify-between p-4 rounded-lg border shadow-lg ${getBackgroundColor()} transition-all duration-300 ease-in-out`}>\n      <div className=\"flex items-center space-x-2\">\n        {getIcon()}\n        <span className=\"text-sm font-medium\">{message}</span>\n      </div>\n      <button\n        onClick={onClose}\n        className=\"ml-4 text-gray-500 hover:text-gray-700 focus:outline-none\"\n      >\n        <X className=\"h-4 w-4\" />\n      </button>\n    </div>\n  );\n}"
  },
  {
    "path": "packages/ui/src/components/ui/tooltip.tsx",
    "content": "import * as React from \"react\"\nimport * as TooltipPrimitive from \"@radix-ui/react-tooltip\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst TooltipProvider = TooltipPrimitive.Provider\n\nconst Tooltip = TooltipPrimitive.Root\n\nconst TooltipTrigger = TooltipPrimitive.Trigger\n\nconst TooltipContent = React.forwardRef<\n  React.ElementRef<typeof TooltipPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>\n>(({ className, sideOffset = 4, ...props }, ref) => (\n  <TooltipPrimitive.Content\n    ref={ref}\n    sideOffset={sideOffset}\n    className={cn(\n      \"z-50 overflow-hidden rounded-md bg-gray-900 px-3 py-1.5 text-xs text-white animate-in fade-in-0 zoom-in-95\",\n      \"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95\",\n      \"data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2\",\n      \"data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n      className\n    )}\n    {...props}\n  />\n))\nTooltipContent.displayName = TooltipPrimitive.Content.displayName\n\nexport { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }\n"
  },
  {
    "path": "packages/ui/src/i18n.ts",
    "content": "import i18n from \"i18next\";\nimport { initReactI18next } from \"react-i18next\";\nimport LanguageDetector from \"i18next-browser-languagedetector\";\n\nimport en from \"./locales/en.json\";\nimport zh from \"./locales/zh.json\";\n\nconst resources = {\n  en: {\n    translation: en,\n  },\n  zh: {\n    translation: zh,\n  },\n};\n\ni18n\n  .use(LanguageDetector)\n  .use(initReactI18next)\n  .init({\n    resources,\n    fallbackLng: \"en\",\n    interpolation: {\n      escapeValue: false,\n    },\n  });\n\nexport default i18n;\n"
  },
  {
    "path": "packages/ui/src/index.css",
    "content": "@import \"tailwindcss\";\n\n@custom-variant dark (&:is(.dark *));\n\n@theme inline {\n  --radius-sm: calc(var(--radius) - 4px);\n  --radius-md: calc(var(--radius) - 2px);\n  --radius-lg: var(--radius);\n  --radius-xl: calc(var(--radius) + 4px);\n  --color-background: var(--background);\n  --color-foreground: var(--foreground);\n  --color-card: var(--card);\n  --color-card-foreground: var(--card-foreground);\n  --color-popover: var(--popover);\n  --color-popover-foreground: var(--popover-foreground);\n  --color-primary: var(--primary);\n  --color-primary-foreground: var(--primary-foreground);\n  --color-secondary: var(--secondary);\n  --color-secondary-foreground: var(--secondary-foreground);\n  --color-muted: var(--muted);\n  --color-muted-foreground: var(--muted-foreground);\n  --color-accent: var(--accent);\n  --color-accent-foreground: var(--accent-foreground);\n  --color-destructive: var(--destructive);\n  --color-destructive-foreground: var(--destructive-foreground);\n  --color-border: var(--border);\n  --color-input: var(--input);\n  --color-ring: var(--ring);\n  --color-chart-1: var(--chart-1);\n  --color-chart-2: var(--chart-2);\n  --color-chart-3: var(--chart-3);\n  --color-chart-4: var(--chart-4);\n  --color-chart-5: var(--chart-5);\n  --color-sidebar: var(--sidebar);\n  --color-sidebar-foreground: var(--sidebar-foreground);\n  --color-sidebar-primary: var(--sidebar-primary);\n  --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);\n  --color-sidebar-accent: var(--sidebar-accent);\n  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);\n  --color-sidebar-border: var(--sidebar-border);\n  --color-sidebar-ring: var(--sidebar-ring);\n}\n\n:root {\n  --radius: 0.625rem;\n  --background: oklch(1 0 0);\n  --foreground: oklch(0.145 0 0);\n  --card: oklch(1 0 0);\n  --card-foreground: oklch(0.145 0 0);\n  --popover: oklch(1 0 0);\n  --popover-foreground: oklch(0.145 0 0);\n  --primary: oklch(0.205 0 0);\n  --primary-foreground: oklch(0.985 0 0);\n  --secondary: oklch(0.97 0 0);\n  --secondary-foreground: oklch(0.205 0 0);\n  --muted: oklch(0.97 0 0);\n  --muted-foreground: oklch(0.556 0 0);\n  --accent: oklch(0.97 0 0);\n  --accent-foreground: oklch(0.205 0 0);\n  --destructive: oklch(0.577 0.245 27.325);\n  --destructive-foreground: oklch(0.985 0 0);\n  --border: oklch(0.922 0 0);\n  --input: oklch(0.922 0 0);\n  --ring: oklch(0.708 0 0);\n  --chart-1: oklch(0.646 0.222 41.116);\n  --chart-2: oklch(0.6 0.118 184.704);\n  --chart-3: oklch(0.398 0.07 227.392);\n  --chart-4: oklch(0.828 0.189 84.429);\n  --chart-5: oklch(0.769 0.188 70.08);\n  --sidebar: oklch(0.985 0 0);\n  --sidebar-foreground: oklch(0.145 0 0);\n  --sidebar-primary: oklch(0.205 0 0);\n  --sidebar-primary-foreground: oklch(0.985 0 0);\n  --sidebar-accent: oklch(0.97 0 0);\n  --sidebar-accent-foreground: oklch(0.205 0 0);\n  --sidebar-border: oklch(0.922 0 0);\n  --sidebar-ring: oklch(0.708 0 0);\n}\n\n.dark {\n  --background: oklch(0.145 0 0);\n  --foreground: oklch(0.985 0 0);\n  --card: oklch(0.205 0 0);\n  --card-foreground: oklch(0.985 0 0);\n  --popover: oklch(0.205 0 0);\n  --popover-foreground: oklch(0.985 0 0);\n  --primary: oklch(0.922 0 0);\n  --primary-foreground: oklch(0.205 0 0);\n  --secondary: oklch(0.269 0 0);\n  --secondary-foreground: oklch(0.985 0 0);\n  --muted: oklch(0.269 0 0);\n  --muted-foreground: oklch(0.708 0 0);\n  --accent: oklch(0.269 0 0);\n  --accent-foreground: oklch(0.985 0 0);\n  --destructive: oklch(0.704 0.191 22.216);\n  --destructive-foreground: oklch(0.985 0 0);\n  --border: oklch(1 0 0 / 10%);\n  --input: oklch(1 0 0 / 15%);\n  --ring: oklch(0.556 0 0);\n  --chart-1: oklch(0.488 0.243 264.376);\n  --chart-2: oklch(0.696 0.17 162.48);\n  --chart-3: oklch(0.769 0.188 70.08);\n  --chart-4: oklch(0.627 0.265 303.9);\n  --chart-5: oklch(0.645 0.246 16.439);\n  --sidebar: oklch(0.205 0 0);\n  --sidebar-foreground: oklch(0.985 0 0);\n  --sidebar-primary: oklch(0.488 0.243 264.376);\n  --sidebar-primary-foreground: oklch(0.985 0 0);\n  --sidebar-accent: oklch(0.269 0 0);\n  --sidebar-accent-foreground: oklch(0.985 0 0);\n  --sidebar-border: oklch(1 0 0 / 10%);\n  --sidebar-ring: oklch(0.556 0 0);\n}\n\n@layer base {\n  * {\n    @apply border-border outline-ring/50;\n  }\n  body {\n    @apply bg-background text-foreground;\n  }\n\n  /* 美化滚动条 - WebKit浏览器 (Chrome, Safari, Edge) */\n  ::-webkit-scrollbar {\n    width: 8px;\n    height: 8px;\n  }\n\n  ::-webkit-scrollbar-track {\n    @apply bg-transparent;\n    border-radius: 4px;\n  }\n\n  ::-webkit-scrollbar-thumb {\n    @apply bg-muted-foreground/30;\n    border-radius: 4px;\n    transition: background-color 0.2s ease;\n  }\n\n  ::-webkit-scrollbar-thumb:hover {\n    @apply bg-muted-foreground/50;\n  }\n\n  ::-webkit-scrollbar-corner {\n    @apply bg-transparent;\n  }\n\n  * {\n    scrollbar-width: thin;\n    scrollbar-color: oklch(0.556 0 0) oklch(0.97 0 0);\n  }\n\n  .dark * {\n    scrollbar-color: oklch(0.708 0 0) oklch(0.269 0 0);\n  }\n}\n\n/* Monaco Editor 调试按钮样式 */\n.debug-button-glyph {\n  background: url('data:image/svg+xml;utf8,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"%23056bfe\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 20v-9\"/><path d=\"M14 7a4 4 0 0 1 4 4v3a6 6 0 0 1-12 0v-3a4 4 0 0 1 4-4z\"/><path d=\"M14.12 3.88 16 2\"/><path d=\"M21 21a4 4 0 0 0-3.81-4\"/><path d=\"M21 5a4 4 0 0 1-3.55 3.97\"/><path d=\"M22 13h-4\"/><path d=\"M3 21a4 4 0 0 1 3.81-4\"/><path d=\"M3 5a4 4 0 0 0 3.55 3.97\"/><path d=\"M6 13H2\"/><path d=\"m8 2 1.88 1.88\"/><path d=\"M9 7.13V6a3 3 0 1 1 6 0v1.13\"/></svg>') center center no-repeat;\n  background-size: 14px 14px;\n  cursor: pointer;\n  opacity: 0.8;\n  transition: opacity 0.2s ease;\n}\n\n.debug-button-glyph:hover {\n  opacity: 1;\n}\n\n/* 确保调试按钮在glyph margin中可见 */\n.monaco-editor .margin-view-overlays .debug-button-glyph {\n  display: block !important;\n  width: 16px !important;\n  height: 16px !important;\n  margin: 2px 0;\n}\n"
  },
  {
    "path": "packages/ui/src/lib/api.ts",
    "content": "import type { Config, Provider, Transformer } from '@/types';\n\n// 日志聚合响应类型\ninterface GroupedLogsResponse {\n  grouped: boolean;\n  groups: { [reqId: string]: Array<{ timestamp: string; level: string; message: string; source?: string; reqId?: string }> };\n  summary: {\n    totalRequests: number;\n    totalLogs: number;\n    requests: Array<{\n      reqId: string;\n      logCount: number;\n      firstLog: string;\n      lastLog: string;\n    }>;\n  };\n}\n\n// API Client Class for handling requests with baseUrl and apikey authentication\nclass ApiClient {\n  private baseUrl: string;\n  private apiKey: string;\n  private tempApiKey: string | null;\n\n  constructor(baseUrl: string = '/api', apiKey: string = '') {\n    this.baseUrl = baseUrl;\n    // Load API key from localStorage if available\n    this.apiKey = apiKey || localStorage.getItem('apiKey') || '';\n    // Load temp API key from URL if available\n    this.tempApiKey = new URLSearchParams(window.location.search).get('tempApiKey');\n  }\n\n  // Update base URL\n  setBaseUrl(url: string) {\n    this.baseUrl = url;\n  }\n\n  // Update API key\n  setApiKey(apiKey: string) {\n    this.apiKey = apiKey;\n    // Save API key to localStorage\n    if (apiKey) {\n      localStorage.setItem('apiKey', apiKey);\n    } else {\n      localStorage.removeItem('apiKey');\n    }\n  }\n\n  // Update temp API key\n  setTempApiKey(tempApiKey: string | null) {\n    this.tempApiKey = tempApiKey;\n  }\n\n  // Create headers with API key authentication\n  private createHeaders(contentType: string = 'application/json'): HeadersInit {\n    const headers: Record<string, string> = {\n      'Accept': 'application/json',\n    };\n\n    // Use temp API key if available, otherwise use regular API key\n    if (this.tempApiKey) {\n      headers['X-Temp-API-Key'] = this.tempApiKey;\n    } else if (this.apiKey) {\n      headers['X-API-Key'] = this.apiKey;\n    }\n\n    if (contentType) {\n      headers['Content-Type'] = contentType;\n    }\n\n    return headers;\n  }\n\n  // Generic fetch wrapper with base URL and authentication\n  private async apiFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n    const url = `${this.baseUrl}${endpoint}`;\n\n    const config: RequestInit = {\n      ...options,\n      headers: {\n        ...this.createHeaders(),\n        ...options.headers,\n      },\n    };\n\n    try {\n      const response = await fetch(url, config);\n\n      // Handle 401 Unauthorized responses\n      if (response.status === 401) {\n        // Remove API key when it's invalid\n        localStorage.removeItem('apiKey');\n        // Redirect to login page if not already there\n        // For memory router, we need to use the router instance\n        // We'll dispatch a custom event that the app can listen to\n        window.dispatchEvent(new CustomEvent('unauthorized'));\n        // Return a promise that never resolves to prevent further execution\n        return new Promise(() => {}) as Promise<T>;\n      }\n\n      if (!response.ok) {\n        // Try to get detailed error message from response body\n        let errorMessage = `API request failed: ${response.status} ${response.statusText}`;\n        try {\n          const errorData = await response.json();\n          if (errorData.error || errorData.message) {\n            errorMessage = errorData.message || errorData.error || errorMessage;\n          }\n        } catch {\n          // If parsing fails, use default error message\n        }\n        throw new Error(errorMessage);\n      }\n\n      if (response.status === 204) {\n        return {} as T;\n      }\n\n      const text = await response.text();\n      return text ? JSON.parse(text) : ({} as T);\n\n    } catch (error) {\n      console.error('API request error:', error);\n      throw error;\n    }\n  }\n\n  // GET request\n  async get<T>(endpoint: string): Promise<T> {\n    return this.apiFetch<T>(endpoint, {\n      method: 'GET',\n    });\n  }\n\n  // POST request\n  async post<T>(endpoint: string, data: unknown): Promise<T> {\n    return this.apiFetch<T>(endpoint, {\n      method: 'POST',\n      body: JSON.stringify(data),\n    });\n  }\n\n  // PUT request\n  async put<T>(endpoint: string, data: unknown): Promise<T> {\n    return this.apiFetch<T>(endpoint, {\n      method: 'PUT',\n      body: JSON.stringify(data),\n    });\n  }\n\n  // DELETE request\n  async delete<T>(endpoint: string, body?: any): Promise<T> {\n    return this.apiFetch<T>(endpoint, {\n      method: 'DELETE',\n      body: JSON.stringify(body || {}),\n    });\n  }\n\n  // API methods for configuration\n  // Get current configuration\n  async getConfig(): Promise<Config> {\n    return this.get<Config>('/config');\n  }\n\n  // Update entire configuration\n  async updateConfig(config: Config): Promise<Config> {\n    return this.post<Config>('/config', config);\n  }\n\n  // Get providers\n  async getProviders(): Promise<Provider[]> {\n    return this.get<Provider[]>('/api/providers');\n  }\n\n  // Add a new provider\n  async addProvider(provider: Provider): Promise<Provider> {\n    return this.post<Provider>('/api/providers', provider);\n  }\n\n  // Update a provider\n  async updateProvider(index: number, provider: Provider): Promise<Provider> {\n    return this.post<Provider>(`/api/providers/${index}`, provider);\n  }\n\n  // Delete a provider\n  async deleteProvider(index: number): Promise<void> {\n    return this.delete<void>(`/api/providers/${index}`);\n  }\n\n  // Get transformers\n  async getTransformers(): Promise<Transformer[]> {\n    return this.get<Transformer[]>('/api/transformers');\n  }\n\n  // Add a new transformer\n  async addTransformer(transformer: Transformer): Promise<Transformer> {\n    return this.post<Transformer>('/api/transformers', transformer);\n  }\n\n  // Update a transformer\n  async updateTransformer(index: number, transformer: Transformer): Promise<Transformer> {\n    return this.post<Transformer>(`/api/transformers/${index}`, transformer);\n  }\n\n  // Delete a transformer\n  async deleteTransformer(index: number): Promise<void> {\n    return this.delete<void>(`/api/transformers/${index}`);\n  }\n\n  // Get configuration (new endpoint)\n  async getConfigNew(): Promise<Config> {\n    return this.get<Config>('/config');\n  }\n\n  // Save configuration (new endpoint)\n  async saveConfig(config: Config): Promise<unknown> {\n    return this.post<Config>('/config', config);\n  }\n\n  // Restart service\n  async restartService(): Promise<unknown> {\n    return this.post<void>('/restart', {});\n  }\n\n  // Check for updates\n  async checkForUpdates(): Promise<{ hasUpdate: boolean; latestVersion?: string; changelog?: string }> {\n    return this.get<{ hasUpdate: boolean; latestVersion?: string; changelog?: string }>('/update/check');\n  }\n\n  // Perform update\n  async performUpdate(): Promise<{ success: boolean; message: string }> {\n    return this.post<{ success: boolean; message: string }>('/api/update/perform', {});\n  }\n\n  // Get log files list\n  async getLogFiles(): Promise<Array<{ name: string; path: string; size: number; lastModified: string }>> {\n    return this.get<Array<{ name: string; path: string; size: number; lastModified: string }>>('/logs/files');\n  }\n\n  // Get logs from specific file\n  async getLogs(filePath: string): Promise<string[]> {\n    return this.get<string[]>(`/logs?file=${encodeURIComponent(filePath)}`);\n  }\n\n  // Clear logs from specific file\n  async clearLogs(filePath: string): Promise<void> {\n    return this.delete<void>(`/logs?file=${encodeURIComponent(filePath)}`);\n  }\n\n  // ========== Preset API methods ==========\n\n  // Get presets list\n  async getPresets(): Promise<{ presets: Array<any> }> {\n    return this.get<{ presets: Array<any> }>('/presets');\n  }\n\n  // Get preset details\n  async getPreset(name: string): Promise<any> {\n    return this.get<any>(`/presets/${encodeURIComponent(name)}`);\n  }\n\n  // Install preset from URL\n  async installPresetFromUrl(url: string, name?: string): Promise<any> {\n    return this.post<any>('/presets/install', { url, name });\n  }\n\n  // Upload preset file\n  async uploadPresetFile(file: File, name?: string): Promise<any> {\n    const formData = new FormData();\n    formData.append('file', file);\n    if (name) {\n      formData.append('name', name);\n    }\n\n    const url = `${this.baseUrl}/presets/upload`;\n\n    const headers: Record<string, string> = {\n      'Accept': 'application/json',\n    };\n\n    // Use temp API key if available, otherwise use regular API key\n    if (this.tempApiKey) {\n      headers['X-Temp-API-Key'] = this.tempApiKey;\n    } else if (this.apiKey) {\n      headers['X-API-Key'] = this.apiKey;\n    }\n\n    const response = await fetch(url, {\n      method: 'POST',\n      headers,\n      body: formData,\n    });\n\n    if (response.status === 401) {\n      localStorage.removeItem('apiKey');\n      window.dispatchEvent(new CustomEvent('unauthorized'));\n      return new Promise(() => {}) as any;\n    }\n\n    if (!response.ok) {\n      throw new Error(`Failed to upload preset: ${response.status} ${response.statusText}`);\n    }\n\n    return response.json();\n  }\n\n  // Apply preset (configure sensitive fields)\n  async applyPreset(name: string, secrets: Record<string, string>): Promise<any> {\n    return this.post<any>(`/presets/${encodeURIComponent(name)}/apply`, { secrets });\n  }\n\n  // Delete preset\n  async deletePreset(name: string): Promise<any> {\n    return this.delete<any>(`/presets/${encodeURIComponent(name)}`, {});\n  }\n\n  // Get market presets\n  async getMarketPresets(): Promise<{ presets: Array<any> }> {\n    return this.get<{ presets: Array<any> }>('/presets/market');\n  }\n\n  // Install preset from GitHub repository\n  async installPresetFromGitHub(repo: string, name?: string): Promise<any> {\n    return this.post<any>('/presets/install/github', { repo, name });\n  }\n}\n\n// Create a default instance of the API client\nexport const api = new ApiClient();\n\n// Export the class for creating custom instances\nexport default ApiClient;\n"
  },
  {
    "path": "packages/ui/src/lib/db.ts",
    "content": "export interface RequestHistoryItem {\n  id: string;\n  url: string;\n  method: string;\n  headers: string;\n  body: string;\n  timestamp: string;\n  status: number;\n  responseTime: number;\n  responseBody: string;\n  responseHeaders: string;\n}\n\nclass RequestHistoryDB {\n  private readonly DB_NAME = 'RequestHistoryDB';\n  private readonly STORE_NAME = 'requests';\n  private readonly VERSION = 1;\n\n  async openDB(): Promise<IDBDatabase> {\n    return new Promise((resolve, reject) => {\n      const request = indexedDB.open(this.DB_NAME, this.VERSION);\n      \n      request.onerror = () => reject(request.error);\n      request.onsuccess = () => resolve(request.result);\n      \n      request.onupgradeneeded = (event) => {\n        const db = (event.target as IDBOpenDBRequest).result;\n        \n        if (!db.objectStoreNames.contains(this.STORE_NAME)) {\n          const store = db.createObjectStore(this.STORE_NAME, { keyPath: 'id' });\n          store.createIndex('timestamp', 'timestamp', { unique: false });\n          store.createIndex('url', 'url', { unique: false });\n          store.createIndex('method', 'method', { unique: false });\n        }\n      };\n    });\n  }\n\n  async saveRequest(request: Omit<RequestHistoryItem, 'id' | 'timestamp'>): Promise<void> {\n    const db = await this.openDB();\n    const item: RequestHistoryItem = {\n      ...request,\n      id: Date.now().toString(),\n      timestamp: new Date().toISOString(),\n    };\n    \n    return new Promise((resolve, reject) => {\n      const transaction = db.transaction([this.STORE_NAME], 'readwrite');\n      const store = transaction.objectStore(this.STORE_NAME);\n      const request = store.add(item);\n      \n      request.onsuccess = () => resolve();\n      request.onerror = () => reject(request.error);\n    });\n  }\n\n  async getRequests(limit: number = 50): Promise<RequestHistoryItem[]> {\n    const db = await this.openDB();\n    return new Promise((resolve, reject) => {\n      const transaction = db.transaction([this.STORE_NAME], 'readonly');\n      const store = transaction.objectStore(this.STORE_NAME);\n      const index = store.index('timestamp');\n      const request = index.openCursor(null, 'prev');\n      \n      const results: RequestHistoryItem[] = [];\n      \n      request.onsuccess = (event) => {\n        const cursor = (event.target as IDBRequest).result;\n        if (cursor && results.length < limit) {\n          results.push(cursor.value);\n          cursor.continue();\n        } else {\n          resolve(results);\n        }\n      };\n      \n      request.onerror = () => reject(request.error);\n    });\n  }\n\n  async deleteRequest(id: string): Promise<void> {\n    const db = await this.openDB();\n    return new Promise((resolve, reject) => {\n      const transaction = db.transaction([this.STORE_NAME], 'readwrite');\n      const store = transaction.objectStore(this.STORE_NAME);\n      const request = store.delete(id);\n      \n      request.onsuccess = () => resolve();\n      request.onerror = () => reject(request.error);\n    });\n  }\n\n  async clearAllRequests(): Promise<void> {\n    const db = await this.openDB();\n    return new Promise((resolve, reject) => {\n      const transaction = db.transaction([this.STORE_NAME], 'readwrite');\n      const store = transaction.objectStore(this.STORE_NAME);\n      const request = store.clear();\n      \n      request.onsuccess = () => resolve();\n      request.onerror = () => reject(request.error);\n    });\n  }\n}\n\nexport const requestHistoryDB = new RequestHistoryDB();"
  },
  {
    "path": "packages/ui/src/lib/utils.ts",
    "content": "import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"
  },
  {
    "path": "packages/ui/src/locales/en.json",
    "content": "{\n  \"common\": {\n    \"yes\": \"Yes\",\n    \"no\": \"No\"\n  },\n  \"app\": {\n    \"title\": \"Claude Code Router\",\n    \"save\": \"Save\",\n    \"save_and_restart\": \"Save and Restart\",\n    \"cancel\": \"Cancel\",\n    \"edit\": \"Edit\",\n    \"remove\": \"Remove\",\n    \"delete\": \"Delete\",\n    \"settings\": \"Settings\",\n    \"selectFile\": \"Select File\",\n    \"config_saved_success\": \"Config saved successfully\",\n    \"config_saved_failed\": \"Failed to save config\",\n    \"config_saved_restart_success\": \"Config saved and service restarted successfully\",\n    \"config_saved_restart_failed\": \"Failed to save config and restart service\",\n    \"new_version_available\": \"New Version Available\",\n    \"update_description\": \"A new version is available. Please review the changelog and update to get the latest features and improvements.\",\n    \"no_changelog_available\": \"No changelog available\",\n    \"later\": \"Later\",\n    \"update_now\": \"Update Now\",\n    \"no_updates_available\": \"No updates available\",\n    \"update_check_failed\": \"Failed to check for updates\",\n    \"update_successful\": \"Update successful\",\n    \"update_failed\": \"Update failed\",\n    \"json_editor\": \"JSON Editor\",\n    \"log_viewer\": \"Log Viewer\",\n    \"presets\": \"Presets\",\n    \"language\": \"Language\",\n    \"check_updates\": \"Check for Updates\"\n  },\n  \"login\": {\n    \"title\": \"Sign in to your account\",\n    \"description\": \"Enter your API key to access the configuration panel\",\n    \"apiKey\": \"API Key\",\n    \"apiKeyPlaceholder\": \"Enter your API key\",\n    \"signIn\": \"Sign In\",\n    \"invalidApiKey\": \"Invalid API key\",\n    \"configError\": \"Configuration not loaded\",\n    \"validating\": \"Validating API key...\"\n  },\n  \"toplevel\": {\n    \"title\": \"General Settings\",\n    \"log\": \"Enable Logging\",\n    \"log_level\": \"Log Level\",\n    \"claude_path\": \"Claude Path\",\n    \"host\": \"Host\",\n    \"port\": \"Port\",\n    \"apikey\": \"API Key\",\n    \"timeout\": \"API Timeout (ms)\",\n    \"proxy_url\": \"Proxy URL\",\n    \"custom_router_path\": \"Custom Router Script Path\",\n    \"custom_router_path_placeholder\": \"Enter absolute path to custom router script file\"\n  },\n  \"transformers\": {\n    \"title\": \"Custom Transformers\",\n    \"path\": \"Path\",\n    \"project\": \"Project\",\n    \"remove\": \"Remove\",\n    \"add\": \"Add Custom Transformer\",\n    \"edit\": \"Edit Custom Transformer\",\n    \"delete\": \"Delete Custom Transformer\",\n    \"delete_transformer_confirm\": \"Are you sure you want to delete this custom transformer?\",\n    \"parameters\": \"Parameters\"\n  },\n  \"providers\": {\n    \"title\": \"Providers\",\n    \"name\": \"Name\",\n    \"api_base_url\": \"API Full URL\",\n    \"api_key\": \"API Key\",\n    \"models\": \"Models\",\n    \"models_placeholder\": \"Enter model name and press Enter to add\",\n    \"add_model\": \"Add Model\",\n    \"select_models\": \"Select Models\",\n    \"remove\": \"Remove\",\n    \"add\": \"Add Provider\",\n    \"edit\": \"Edit Provider\",\n    \"delete\": \"Delete\",\n    \"cancel\": \"Cancel\",\n    \"delete_provider_confirm\": \"Are you sure you want to delete this provider?\",\n    \"test_connectivity\": \"Test Connectivity\",\n    \"testing\": \"Testing...\",\n    \"connection_successful\": \"Connection successful!\",\n    \"connection_failed\": \"Connection failed!\",\n    \"missing_credentials\": \"Missing API base URL or API key\",\n    \"fetch_available_models\": \"Fetch available models\",\n    \"fetching_models\": \"Fetching models...\",\n    \"fetch_models_failed\": \"Failed to fetch models\",\n    \"transformers\": \"Transformers\",\n    \"select_transformer\": \"Select Transformer\",\n    \"no_transformers\": \"No transformers available\",\n    \"provider_transformer\": \"Provider Transformer\",\n    \"model_transformers\": \"Model Transformers\",\n    \"transformer_parameters\": \"Transformer Parameters\",\n    \"add_parameter\": \"Add Parameter\",\n    \"parameter_name\": \"Parameter Name\",\n    \"parameter_value\": \"Parameter Value\",\n    \"selected_transformers\": \"Selected Transformers\",\n    \"import_from_template\": \"Import from template\",\n    \"no_templates_found\": \"No templates found\",\n    \"select_template\": \"Select a template...\",\n    \"api_key_required\": \"API Key is required\",\n    \"name_required\": \"Name is required\",\n    \"name_duplicate\": \"A provider with this name already exists\",\n    \"search\": \"Search providers...\"\n\n  },\n  \"router\": {\n    \"title\": \"Router\",\n    \"default\": \"Default\",\n    \"background\": \"Background\",\n    \"think\": \"Think\",\n    \"longContext\": \"Long Context\",\n    \"longContextThreshold\": \"Context Threshold\",\n    \"webSearch\": \"Web Search\",\n    \"image\": \"Image\",\n    \"forceUseImageAgent\": \"Force Use Image Agent\",\n    \"selectModel\": \"Select a model...\",\n    \"searchModel\": \"Search model...\",\n    \"noModelFound\": \"No model found.\"\n  },\n  \"json_editor\": {\n    \"title\": \"JSON Editor\",\n    \"save\": \"Save\",\n    \"saving\": \"Saving...\",\n    \"cancel\": \"Cancel\",\n    \"save_failed\": \"Failed to save config\",\n    \"save_and_restart\": \"Save & Restart\"\n  },\n  \"statusline\": {\n    \"title\": \"Status Line Configuration\",\n    \"enable\": \"Enable Status Line\",\n    \"theme\": \"Theme Style\",\n    \"theme_default\": \"Default\",\n    \"theme_powerline\": \"Powerline\",\n    \"modules\": \"Modules\",\n    \"module_type\": \"Type\",\n    \"module_icon\": \"Icon\",\n    \"module_text\": \"Text\",\n    \"module_color\": \"Color\",\n    \"module_background\": \"Background\",\n    \"module_text_description\": \"Enter display text, variables can be used:\",\n    \"module_color_description\": \"Select text color\",\n    \"module_background_description\": \"Select background color (optional)\",\n    \"module_script_path\": \"Script Path\",\n    \"module_script_path_description\": \"Enter the absolute path of the Node.js script file\",\n    \"add_module\": \"Add Module\",\n    \"remove_module\": \"Remove Module\",\n    \"delete_module\": \"Delete Module\",\n    \"preview\": \"Preview\",\n    \"components\": \"Components\",\n    \"properties\": \"Properties\",\n    \"workDir\": \"Working Directory\",\n    \"gitBranch\": \"Git Branch\",\n    \"model\": \"Model\",\n    \"usage\": \"Usage\",\n    \"script\": \"Script\",\n    \"background_none\": \"None\",\n    \"color_black\": \"Black\",\n    \"color_red\": \"Red\",\n    \"color_green\": \"Green\",\n    \"color_yellow\": \"Yellow\",\n    \"color_blue\": \"Blue\",\n    \"color_magenta\": \"Magenta\",\n    \"color_cyan\": \"Cyan\",\n    \"color_white\": \"White\",\n    \"color_bright_black\": \"Bright Black\",\n    \"color_bright_red\": \"Bright Red\",\n    \"color_bright_green\": \"Bright Green\",\n    \"color_bright_yellow\": \"Bright Yellow\",\n    \"color_bright_blue\": \"Bright Blue\",\n    \"color_bright_magenta\": \"Bright Magenta\",\n    \"color_bright_cyan\": \"Bright Cyan\",\n    \"color_bright_white\": \"Bright White\",\n    \"font_placeholder\": \"Select Font\",\n    \"theme_placeholder\": \"Select Theme Style\",\n    \"icon_placeholder\": \"Paste icon or search by name...\",\n    \"icon_description\": \"Enter icon character, paste icon, or search icons (optional)\",\n    \"text_placeholder\": \"e.g.: {{workDirName}}\",\n    \"script_placeholder\": \"e.g.: /path/to/your/script.js\",\n    \"drag_hint\": \"Drag components here to configure\",\n    \"select_hint\": \"Select a component to configure\",\n    \"no_icons_found\": \"No icons found\",\n    \"no_icons_available\": \"No icons available\",\n    \"import_export\": \"Import/Export\",\n    \"import\": \"Import Config\",\n    \"export\": \"Export Config\",\n    \"download_template\": \"Download Template\",\n    \"import_export_help\": \"Export current configuration as a JSON file, or import configuration from a JSON file. You can also download a configuration template for reference.\",\n    \"export_success\": \"Configuration exported successfully\",\n    \"export_failed\": \"Failed to export configuration\",\n    \"import_success\": \"Configuration imported successfully\",\n    \"import_failed\": \"Failed to import configuration\",\n    \"invalid_config\": \"Invalid configuration file\",\n    \"template_download_success\": \"Template downloaded successfully\",\n    \"template_download_success_desc\": \"Configuration template has been downloaded to your device\",\n    \"template_download_failed\": \"Failed to download template\"\n  },\n  \"color_picker\": {\n    \"placeholder\": \"Select color...\",\n    \"title\": \"Color Picker\",\n    \"clear\": \"Clear\",\n    \"no_color_selected\": \"No color selected\",\n    \"custom_color\": \"Custom Color\",\n    \"apply\": \"Apply\",\n    \"hex_input_help\": \"Enter hex color value (e.g.: #FF0000)\"\n  },\n  \"log_viewer\": {\n    \"title\": \"Log Viewer\",\n    \"close\": \"Close\",\n    \"download\": \"Download\",\n    \"clear\": \"Clear\",\n    \"auto_refresh_on\": \"Auto Refresh On\",\n    \"auto_refresh_off\": \"Auto Refresh Off\",\n    \"load_failed\": \"Failed to load logs\",\n    \"no_logs_available\": \"No logs available\",\n    \"logs_cleared\": \"Logs cleared successfully\",\n    \"clear_failed\": \"Failed to clear logs\",\n    \"logs_downloaded\": \"Logs downloaded successfully\",\n    \"back_to_files\": \"Back to Files\",\n    \"select_file\": \"Select a log file to view\",\n    \"no_log_files_available\": \"No log files available\",\n    \"load_files_failed\": \"Failed to load log files\",\n    \"group_by_req_id\": \"Group by Request ID\",\n    \"grouped_on\": \"Grouped\",\n    \"request_groups\": \"Request Groups\",\n    \"total_requests\": \"Total Requests\",\n    \"total_logs\": \"Total Logs\",\n    \"request\": \"Request\",\n    \"logs\": \"logs\",\n    \"first_log\": \"First Log\",\n    \"last_log\": \"Last Log\",\n    \"back_to_all_logs\": \"Back to All Logs\",\n    \"worker_error\": \"Worker error\",\n    \"worker_init_failed\": \"Failed to initialize worker\",\n    \"grouping_not_supported\": \"Log grouping not supported by server\",\n    \"back\": \"Back\"\n  },\n  \"presets\": {\n    \"title\": \"Presets\",\n    \"market_title\": \"Preset Market\",\n    \"market_description\": \"Browse and install presets from the community marketplace\",\n    \"no_presets\": \"No presets installed\",\n    \"no_presets_hint\": \"Install a preset to get started\",\n    \"search_placeholder\": \"Search presets by name, description, author, or keywords...\",\n    \"no_presets_found\": \"No presets found\",\n    \"no_presets_found_hint\": \"Try adjusting your search terms\",\n    \"loading\": \"Loading...\",\n    \"by\": \"by {{author}}\",\n    \"downloads\": \"{{count}} downloads\",\n    \"github_repository\": \"GitHub Repository\",\n    \"view_details\": \"View Details\",\n    \"install\": \"Install\",\n    \"installing\": \"Installing...\",\n    \"installed_label\": \"Installed\",\n    \"apply\": \"Apply Preset\",\n    \"applying\": \"Applying...\",\n    \"close\": \"Close\",\n    \"delete\": \"Delete\",\n    \"install_dialog_title\": \"Install Preset\",\n    \"install_dialog_description\": \"Install a preset from a GitHub repository\",\n    \"from_url\": \"From GitHub\",\n    \"github_repository\": \"GitHub Repository\",\n    \"preset_url\": \"Repository URL\",\n    \"preset_url_placeholder\": \"https://github.com/owner/repo\",\n    \"preset_name\": \"Preset Name (Optional)\",\n    \"preset_name_placeholder\": \"Auto-generated from repository\",\n    \"github_url_hint\": \"Enter GitHub repository URL (e.g., https://github.com/owner/repo)\",\n    \"detail_dialog_title\": \"Preset Details\",\n    \"required_information\": \"Required Information\",\n    \"delete_dialog_title\": \"Delete Preset\",\n    \"delete_dialog_description\": \"Are you sure you want to delete preset \\\"{{name}}\\\"? This action cannot be undone.\",\n    \"preset_installed\": \"Preset installed successfully\",\n    \"preset_install_failed\": \"Failed to install preset: {{error}}\",\n    \"preset_already_installed\": \"Preset already installed. Please delete it first if you want to reinstall.\",\n    \"preset_applied\": \"Preset applied successfully\",\n    \"preset_apply_failed\": \"Failed to apply preset: {{error}}\",\n    \"preset_deleted\": \"Preset deleted successfully\",\n    \"preset_delete_failed\": \"Failed to delete preset: {{error}}\",\n    \"load_presets_failed\": \"Failed to load presets\",\n    \"load_preset_details_failed\": \"Failed to load preset details\",\n    \"please_fill_field\": \"Please fill in {{field}}\",\n    \"load_market_failed\": \"Failed to load market presets\",\n    \"preset_installed_config_required\": \"Preset installed, please complete configuration\",\n    \"please_provide_file\": \"Please provide a preset directory\",\n    \"please_provide_url\": \"Please provide a valid GitHub repository URL\",\n    \"form\": {\n      \"field_required\": \"{{field}} is required\",\n      \"must_be_number\": \"{{field}} must be a number\",\n      \"must_be_at_least\": \"{{field}} must be at least {{min}}\",\n      \"must_be_at_most\": \"{{field}} must be at most {{max}}\",\n      \"format_invalid\": \"{{field}} format is invalid\",\n      \"select\": \"Select {{label}}\",\n      \"applying\": \"Applying...\",\n      \"apply\": \"Apply\",\n      \"cancel\": \"Cancel\"\n    }\n  }\n}\n"
  },
  {
    "path": "packages/ui/src/locales/zh.json",
    "content": "{\n  \"common\": {\n    \"yes\": \"是\",\n    \"no\": \"否\"\n  },\n  \"app\": {\n    \"title\": \"Claude Code Router\",\n    \"save\": \"保存\",\n    \"save_and_restart\": \"保存并重启\",\n    \"cancel\": \"取消\",\n    \"edit\": \"编辑\",\n    \"remove\": \"移除\",\n    \"delete\": \"删除\",\n    \"settings\": \"设置\",\n    \"selectFile\": \"选择文件\",\n    \"config_saved_success\": \"配置保存成功\",\n    \"config_saved_failed\": \"配置保存失败\",\n    \"config_saved_restart_success\": \"配置保存并服务重启成功\",\n    \"config_saved_restart_failed\": \"配置保存并服务重启失败\",\n    \"new_version_available\": \"有新版本可用\",\n    \"update_description\": \"发现新版本。请查看更新日志并更新以获取最新功能和改进。\",\n    \"no_changelog_available\": \"暂无更新日志\",\n    \"later\": \"稍后再说\",\n    \"update_now\": \"立即更新\",\n    \"no_updates_available\": \"当前已是最新版本\",\n    \"update_check_failed\": \"检查更新失败\",\n    \"update_successful\": \"更新成功\",\n    \"update_failed\": \"更新失败\",\n    \"json_editor\": \"JSON 编辑器\",\n    \"log_viewer\": \"日志查看器\",\n    \"presets\": \"预设\",\n    \"language\": \"语言\",\n    \"check_updates\": \"检查更新\"\n  },\n  \"login\": {\n    \"title\": \"登录到您的账户\",\n    \"description\": \"请输入您的API密钥以访问配置面板\",\n    \"apiKey\": \"API密钥\",\n    \"apiKeyPlaceholder\": \"请输入您的API密钥\",\n    \"signIn\": \"登录\",\n    \"invalidApiKey\": \"API密钥无效\",\n    \"configError\": \"配置未加载\",\n    \"validating\": \"正在验证API密钥...\"\n  },\n  \"toplevel\": {\n    \"title\": \"通用设置\",\n    \"log\": \"启用日志\",\n    \"log_level\": \"日志级别\",\n    \"claude_path\": \"Claude 路径\",\n    \"host\": \"主机\",\n    \"port\": \"端口\",\n    \"apikey\": \"API 密钥\",\n    \"timeout\": \"API 超时时间 (毫秒)\",\n    \"proxy_url\": \"代理地址\",\n    \"custom_router_path\": \"自定义路由脚本路径\",\n    \"custom_router_path_placeholder\": \"输入自定义路由脚本文件的绝对路径\"\n  },\n  \"transformers\": {\n    \"title\": \"自定义转换器\",\n    \"path\": \"路径\",\n    \"project\": \"项目\",\n    \"remove\": \"移除\",\n    \"add\": \"添加自定义转换器\",\n    \"edit\": \"编辑自定义转换器\",\n    \"delete\": \"删除自定义转换器\",\n    \"delete_transformer_confirm\": \"您确定要删除此自定义转换器吗？\",\n    \"parameters\": \"参数\"\n  },\n  \"providers\": {\n    \"title\": \"供应商\",\n    \"name\": \"名称\",\n    \"api_base_url\": \"API 完整地址\",\n    \"api_key\": \"API 密钥\",\n    \"models\": \"模型\",\n    \"models_placeholder\": \"输入模型名称并按回车键添加\",\n    \"add_model\": \"添加模型\",\n    \"select_models\": \"选择模型\",\n    \"remove\": \"移除\",\n    \"add\": \"添加供应商\",\n    \"edit\": \"编辑供应商\",\n    \"delete\": \"删除\",\n    \"cancel\": \"取消\",\n    \"delete_provider_confirm\": \"您确定要删除此供应商吗？\",\n    \"test_connectivity\": \"测试连通性\",\n    \"testing\": \"测试中...\",\n    \"connection_successful\": \"连接成功！\",\n    \"connection_failed\": \"连接失败！\",\n    \"missing_credentials\": \"缺少 API 基础地址或 API 密钥\",\n    \"fetch_available_models\": \"获取可用模型\",\n    \"fetching_models\": \"获取模型中...\",\n    \"fetch_models_failed\": \"获取模型失败\",\n    \"transformers\": \"转换器\",\n    \"select_transformer\": \"选择转换器\",\n    \"no_transformers\": \"无可用转换器\",\n    \"provider_transformer\": \"供应商转换器\",\n    \"model_transformers\": \"模型转换器\",\n    \"transformer_parameters\": \"转换器参数\",\n    \"add_parameter\": \"添加参数\",\n    \"parameter_name\": \"参数名称\",\n    \"parameter_value\": \"参数值\",\n    \"selected_transformers\": \"已选转换器\",\n    \"import_from_template\": \"从模板导入\",\n    \"no_templates_found\": \"未找到模板\",\n    \"select_template\": \"选择一个模板...\",\n    \"api_key_required\": \"API 密钥为必填项\",\n    \"name_required\": \"名称为必填项\",\n    \"name_duplicate\": \"已存在同名供应商\",\n    \"search\": \"搜索供应商...\"\n\n  },\n  \"router\": {\n    \"title\": \"路由\",\n    \"default\": \"默认\",\n    \"background\": \"后台\",\n    \"think\": \"思考\",\n    \"longContext\": \"长上下文\",\n    \"longContextThreshold\": \"上下文阈值\",\n    \"webSearch\": \"网络搜索\",\n    \"image\": \"图像\",\n    \"forceUseImageAgent\": \"强制使用图像代理\",\n    \"selectModel\": \"选择一个模型...\",\n    \"searchModel\": \"搜索模型...\",\n    \"noModelFound\": \"未找到模型.\"\n  },\n  \"json_editor\": {\n    \"title\": \"JSON 编辑器\",\n    \"save\": \"保存\",\n    \"saving\": \"保存中...\",\n    \"cancel\": \"取消\",\n    \"save_failed\": \"配置保存失败\",\n    \"save_and_restart\": \"保存并重启\"\n  },\n  \"statusline\": {\n    \"title\": \"状态栏配置\",\n    \"enable\": \"启用状态栏\",\n    \"theme\": \"主题样式\",\n    \"theme_default\": \"默认\",\n    \"theme_powerline\": \"Powerline\",\n    \"modules\": \"模块\",\n    \"module_type\": \"类型\",\n    \"module_icon\": \"图标\",\n    \"module_text\": \"文本\",\n    \"module_color\": \"颜色\",\n    \"module_background\": \"背景\",\n    \"module_text_description\": \"输入显示文本，可使用变量:\",\n    \"module_color_description\": \"选择文字颜色\",\n    \"module_background_description\": \"选择背景颜色（可选）\",\n    \"module_script_path\": \"脚本路径\",\n    \"module_script_path_description\": \"输入Node.js脚本文件的绝对路径\",\n    \"add_module\": \"添加模块\",\n    \"remove_module\": \"移除模块\",\n    \"delete_module\": \"删除组件\",\n    \"preview\": \"预览\",\n    \"components\": \"组件\",\n    \"properties\": \"属性\",\n    \"workDir\": \"工作目录\",\n    \"gitBranch\": \"Git分支\",\n    \"model\": \"模型\",\n    \"usage\": \"使用情况\",\n    \"script\": \"脚本\",\n    \"background_none\": \"无\",\n    \"color_black\": \"黑色\",\n    \"color_red\": \"红色\",\n    \"color_green\": \"绿色\",\n    \"color_yellow\": \"黄色\",\n    \"color_blue\": \"蓝色\",\n    \"color_magenta\": \"品红\",\n    \"color_cyan\": \"青色\",\n    \"color_white\": \"白色\",\n    \"color_bright_black\": \"亮黑色\",\n    \"color_bright_red\": \"亮红色\",\n    \"color_bright_green\": \"亮绿色\",\n    \"color_bright_yellow\": \"亮黄色\",\n    \"color_bright_blue\": \"亮蓝色\",\n    \"color_bright_magenta\": \"亮品红\",\n    \"color_bright_cyan\": \"亮青色\",\n    \"color_bright_white\": \"亮白色\",\n    \"font_placeholder\": \"选择字体\",\n    \"theme_placeholder\": \"选择主题样式\",\n    \"icon_placeholder\": \"粘贴图标或输入名称搜索...\",\n    \"icon_description\": \"输入图标字符、粘贴图标或搜索图标（可选）\",\n    \"text_placeholder\": \"例如: {{workDirName}}\",\n    \"script_placeholder\": \"例如: /path/to/your/script.js\",\n    \"drag_hint\": \"拖拽组件到此处进行配置\",\n    \"select_hint\": \"选择一个组件进行配置\",\n    \"no_icons_found\": \"未找到图标\",\n    \"no_icons_available\": \"暂无可用图标\",\n    \"import_export\": \"导入/导出\",\n    \"import\": \"导入配置\",\n    \"export\": \"导出配置\",\n    \"download_template\": \"下载模板\",\n    \"import_export_help\": \"导出当前配置为JSON文件，或从JSON文件导入配置。您也可以下载配置模板作为参考。\",\n    \"export_success\": \"配置导出成功\",\n    \"export_failed\": \"配置导出失败\",\n    \"import_success\": \"配置导入成功\",\n    \"import_failed\": \"配置导入失败\",\n    \"invalid_config\": \"无效的配置文件\",\n    \"template_download_success\": \"模板下载成功\",\n    \"template_download_success_desc\": \"配置模板已下载到您的设备\",\n    \"template_download_failed\": \"模板下载失败\"\n  },\n  \"color_picker\": {\n    \"placeholder\": \"选择颜色...\",\n    \"title\": \"颜色选择器\",\n    \"clear\": \"清除\",\n    \"no_color_selected\": \"未选择颜色\",\n    \"custom_color\": \"自定义颜色\",\n    \"apply\": \"应用\",\n    \"hex_input_help\": \"输入十六进制颜色值 (例如: #FF0000)\"\n  },\n  \"log_viewer\": {\n    \"title\": \"日志查看器\",\n    \"close\": \"关闭\",\n    \"download\": \"下载\",\n    \"clear\": \"清除\",\n    \"auto_refresh_on\": \"自动刷新开启\",\n    \"auto_refresh_off\": \"自动刷新关闭\",\n    \"load_failed\": \"加载日志失败\",\n    \"no_logs_available\": \"暂无日志\",\n    \"logs_cleared\": \"日志清除成功\",\n    \"clear_failed\": \"清除日志失败\",\n    \"logs_downloaded\": \"日志下载成功\",\n    \"back_to_files\": \"返回文件列表\",\n    \"select_file\": \"选择要查看的日志文件\",\n    \"no_log_files_available\": \"暂无日志文件\",\n    \"load_files_failed\": \"加载日志文件失败\",\n    \"group_by_req_id\": \"按请求ID分组\",\n    \"grouped_on\": \"已分组\",\n    \"request_groups\": \"请求组\",\n    \"total_requests\": \"总请求数\",\n    \"total_logs\": \"总日志数\",\n    \"request\": \"请求\",\n    \"logs\": \"条日志\",\n    \"first_log\": \"首条日志\",\n    \"last_log\": \"末条日志\",\n    \"back_to_all_logs\": \"返回所有日志\",\n    \"worker_error\": \"Worker错误\",\n    \"worker_init_failed\": \"Worker初始化失败\",\n    \"grouping_not_supported\": \"服务器不支持日志分组\",\n    \"back\": \"返回\"\n  },\n  \"presets\": {\n    \"title\": \"预设\",\n    \"market_title\": \"预设市场\",\n    \"market_description\": \"浏览并从社区市场安装预设\",\n    \"no_presets\": \"暂无已安装的预设\",\n    \"no_presets_hint\": \"安装一个预设以开始使用\",\n    \"search_placeholder\": \"按名称、描述、作者或关键词搜索预设...\",\n    \"no_presets_found\": \"未找到预设\",\n    \"no_presets_found_hint\": \"请尝试调整搜索条件\",\n    \"loading\": \"加载中...\",\n    \"by\": \"{{author}} 创作\",\n    \"downloads\": \"{{count}} 次下载\",\n    \"github_repository\": \"GitHub 仓库\",\n    \"view_details\": \"查看详情\",\n    \"install\": \"安装\",\n    \"installing\": \"安装中...\",\n    \"installed_label\": \"已安装\",\n    \"apply\": \"应用预设\",\n    \"applying\": \"应用中...\",\n    \"close\": \"关闭\",\n    \"delete\": \"删除\",\n    \"install_dialog_title\": \"安装预设\",\n    \"install_dialog_description\": \"从 GitHub 仓库安装预设\",\n    \"from_url\": \"从 GitHub\",\n    \"preset_url\": \"仓库 URL\",\n    \"preset_url_placeholder\": \"https://github.com/owner/repo\",\n    \"preset_name\": \"预设名称 (可选)\",\n    \"preset_name_placeholder\": \"从仓库自动生成\",\n    \"github_url_hint\": \"输入 GitHub 仓库 URL（例如：https://github.com/owner/repo）\",\n    \"detail_dialog_title\": \"预设详情\",\n    \"required_information\": \"必需信息\",\n    \"delete_dialog_title\": \"删除预设\",\n    \"delete_dialog_description\": \"您确定要删除预设 \\\"{{name}}\\\" 吗？此操作无法撤销。\",\n    \"preset_installed\": \"预设安装成功\",\n    \"preset_install_failed\": \"预设安装失败：{{error}}\",\n    \"preset_already_installed\": \"预设已经安装。如需重新安装，请先删除现有预设。\",\n    \"preset_applied\": \"预设应用成功\",\n    \"preset_apply_failed\": \"预设应用失败：{{error}}\",\n    \"preset_deleted\": \"预设删除成功\",\n    \"preset_delete_failed\": \"预设删除失败：{{error}}\",\n    \"load_presets_failed\": \"加载预设失败\",\n    \"load_preset_details_failed\": \"加载预设详情失败\",\n    \"please_fill_field\": \"请填写 {{field}}\",\n    \"load_market_failed\": \"加载市场预设失败\",\n    \"preset_installed_config_required\": \"预设已安装，请完成配置\",\n    \"please_provide_file\": \"请提供预设目录\",\n    \"please_provide_url\": \"请提供有效的 GitHub 仓库 URL\",\n    \"form\": {\n      \"field_required\": \"{{field}} 为必填项\",\n      \"must_be_number\": \"{{field}} 必须是数字\",\n      \"must_be_at_least\": \"{{field}} 至少为 {{min}}\",\n      \"must_be_at_most\": \"{{field}} 最多为 {{max}}\",\n      \"format_invalid\": \"{{field}} 格式无效\",\n      \"select\": \"选择 {{label}}\",\n      \"applying\": \"应用中...\",\n      \"apply\": \"应用\",\n      \"cancel\": \"取消\"\n    }\n  }\n}\n"
  },
  {
    "path": "packages/ui/src/main.tsx",
    "content": "import './i18n';\nimport { StrictMode } from 'react'\nimport { createRoot } from 'react-dom/client'\nimport './index.css'\nimport 'remixicon/fonts/remixicon.css'\nimport { RouterProvider } from 'react-router-dom';\nimport { router } from './routes';\nimport { ConfigProvider } from '@/components/ConfigProvider';\n\ncreateRoot(document.getElementById('root')!).render(\n  <StrictMode>\n    <ConfigProvider>\n      <RouterProvider router={router} />\n    </ConfigProvider>\n  </StrictMode>,\n)\n"
  },
  {
    "path": "packages/ui/src/routes.tsx",
    "content": "import { createMemoryRouter, Navigate } from 'react-router-dom';\nimport App from './App';\nimport { Login } from '@/components/Login';\nimport { DebugPage } from '@/components/DebugPage';\nimport { Presets } from '@/components/Presets';\nimport ProtectedRoute from '@/components/ProtectedRoute';\nimport PublicRoute from '@/components/PublicRoute';\n\nexport const router = createMemoryRouter([\n  {\n    path: '/',\n    element: <Navigate to=\"/dashboard\" replace />,\n  },\n  {\n    path: '/login',\n    element: <PublicRoute><Login /></PublicRoute>,\n  },\n  {\n    path: '/dashboard',\n    element: <ProtectedRoute><App /></ProtectedRoute>,\n  },\n  {\n    path: '/presets',\n    element: <ProtectedRoute><Presets /></ProtectedRoute>,\n  },\n  {\n    path: '/debug',\n    element: <ProtectedRoute><DebugPage /></ProtectedRoute>,\n  },\n], {\n  initialEntries: ['/dashboard']\n});"
  },
  {
    "path": "packages/ui/src/styles/animations.css",
    "content": "@keyframes fadeIn {\n  from {\n    opacity: 0;\n    transform: translateY(10px);\n  }\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n\n@keyframes scaleIn {\n  from {\n    opacity: 0;\n    transform: scale(0.95);\n  }\n  to {\n    opacity: 1;\n    transform: scale(1);\n  }\n}\n\n@keyframes slideIn {\n  from {\n    opacity: 0;\n    transform: translateX(20px);\n  }\n  to {\n    opacity: 1;\n    transform: translateX(0);\n  }\n}\n\n.animate-fade-in {\n  animation: fadeIn 0.2s ease-out forwards;\n}\n\n.animate-scale-in {\n  animation: scaleIn 0.2s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;\n}\n\n.animate-slide-in {\n  animation: slideIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;\n}\n\n.transition-all-ease {\n  transition: all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);\n}"
  },
  {
    "path": "packages/ui/src/types.ts",
    "content": "export interface ProviderTransformer {\n  use: (string | (string | Record<string, unknown> | { max_tokens: number })[])[];\n  [key: string]: any; // Allow for model-specific transformers\n}\n\nexport interface Provider {\n  name: string;\n  api_base_url: string;\n  api_key: string;\n  models: string[];\n  transformer?: ProviderTransformer;\n}\n\nexport interface RouterConfig {\n    default: string;\n    background: string;\n    think: string;\n    longContext: string;\n    longContextThreshold: number;\n    webSearch: string;\n    image: string;\n    custom?: any;\n}\n\nexport interface Transformer {\n    name?: string;\n    path: string;\n    options?: Record<string, any>;\n}\n\nexport interface StatusLineModuleConfig {\n  type: string;\n  icon?: string;\n  text: string;\n  color?: string;\n  background?: string;\n  scriptPath?: string; // 用于script类型的模块，指定要执行的Node.js脚本文件路径\n}\n\nexport interface StatusLineThemeConfig {\n  modules: StatusLineModuleConfig[];\n}\n\nexport interface StatusLineConfig {\n  enabled: boolean;\n  currentStyle: string;\n  default: StatusLineThemeConfig;\n  powerline: StatusLineThemeConfig;\n  fontFamily?: string;\n}\n\nexport interface Config {\n  Providers: Provider[];\n  Router: RouterConfig;\n  transformers: Transformer[];\n  StatusLine?: StatusLineConfig;\n  forceUseImageAgent?: boolean;\n  // Top-level settings\n  LOG: boolean;\n  LOG_LEVEL: string;\n  CLAUDE_PATH: string;\n  HOST: string;\n  PORT: number;\n  APIKEY: string;\n  API_TIMEOUT_MS: string;\n  PROXY_URL: string;\n  CUSTOM_ROUTER_PATH?: string;\n}\n\nexport type AccessLevel = 'restricted' | 'full';\n"
  },
  {
    "path": "packages/ui/src/utils/statusline.ts",
    "content": "import type { StatusLineConfig, StatusLineModuleConfig } from \"@/types\";\n\n// 验证结果（保留接口但不使用）\nexport interface ValidationResult {\n  isValid: boolean;\n  errors: any[];\n}\n\n/**\n * 验证StatusLine配置 - 已移除所有验证\n * @param config 要验证的配置对象\n * @returns 始终返回验证通过\n */\nexport function validateStatusLineConfig(config: unknown): ValidationResult {\n  // 不再执行任何验证\n  return { isValid: true, errors: [] };\n}\n\n\n/**\n * 格式化错误信息（支持国际化）- 不再使用\n */\nexport function formatValidationError(error: unknown, t: (key: string, options?: Record<string, unknown>) => string): string {\n  return t(\"statusline.validation.unknown_error\");\n}\n\n/**\n * 解析颜色值，支持十六进制和内置颜色名称\n * @param color 颜色值（可以是颜色名称或十六进制值）\n * @param defaultColor 默认颜色（十六进制）\n * @returns 十六进制颜色值\n */\nexport function parseColorValue(color: string | undefined, defaultColor: string = \"#ffffff\"): string {\n  if (!color) {\n    return defaultColor;\n  }\n  \n  // 如果是十六进制颜色值（以#开头）\n  if (color.startsWith('#')) {\n    return color;\n  }\n  \n  // 如果是已知的颜色名称，返回对应的十六进制值\n  return COLOR_HEX_MAP[color] || defaultColor;\n}\n\n/**\n * 判断是否为有效的十六进制颜色值\n * @param color 要检查的颜色值\n * @returns 是否为有效的十六进制颜色值\n */\nexport function isHexColor(color: string): boolean {\n  return /^#([0-9A-F]{3}){1,2}$/i.test(color);\n}\n\n// 颜色枚举到十六进制的映射\nexport const COLOR_HEX_MAP: Record<string, string> = {\n  black: \"#000000\",\n  red: \"#cd0000\",\n  green: \"#00cd00\",\n  yellow: \"#cdcd00\",\n  blue: \"#0000ee\",\n  magenta: \"#cd00cd\",\n  cyan: \"#00cdcd\",\n  white: \"#e5e5e5\",\n  bright_black: \"#7f7f7f\",\n  bright_red: \"#ff0000\",\n  bright_green: \"#00ff00\",\n  bright_yellow: \"#ffff00\",\n  bright_blue: \"#5c5cff\",\n  bright_magenta: \"#ff00ff\",\n  bright_cyan: \"#00ffff\",\n  bright_white: \"#ffffff\",\n  bg_black: \"#000000\",\n  bg_red: \"#cd0000\",\n  bg_green: \"#00cd00\",\n  bg_yellow: \"#cdcd00\",\n  bg_blue: \"#0000ee\",\n  bg_magenta: \"#cd00cd\",\n  bg_cyan: \"#00cdcd\",\n  bg_white: \"#e5e5e5\",\n  bg_bright_black: \"#7f7f7f\",\n  bg_bright_red: \"#ff0000\",\n  bg_bright_green: \"#00ff00\",\n  bg_bright_yellow: \"#ffff00\",\n  bg_bright_blue: \"#5c5cff\",\n  bg_bright_magenta: \"#ff00ff\",\n  bg_bright_cyan: \"#00ffff\",\n  bg_bright_white: \"#ffffff\"\n};\n\n/**\n * 创建默认的StatusLine配置\n */\nexport function createDefaultStatusLineConfig(): StatusLineConfig {\n  return {\n    enabled: false,\n    currentStyle: \"default\",\n    default: { \n      modules: [\n        { type: \"workDir\", icon: \"󰉋\", text: \"{{workDirName}}\", color: \"bright_blue\" },\n        { type: \"gitBranch\", icon: \"\", text: \"{{gitBranch}}\", color: \"bright_magenta\" },\n        { type: \"model\", icon: \"󰚩\", text: \"{{model}}\", color: \"bright_cyan\" },\n        { type: \"usage\", icon: \"↑\", text: \"{{inputTokens}}\", color: \"bright_green\" },\n        { type: \"usage\", icon: \"↓\", text: \"{{outputTokens}}\", color: \"bright_yellow\" }\n      ] \n    },\n    powerline: { \n      modules: [\n        { type: \"workDir\", icon: \"󰉋\", text: \"{{workDirName}}\", color: \"white\", background: \"bg_bright_blue\" },\n        { type: \"gitBranch\", icon: \"\", text: \"{{gitBranch}}\", color: \"white\", background: \"bg_bright_magenta\" },\n        { type: \"model\", icon: \"󰚩\", text: \"{{model}}\", color: \"white\", background: \"bg_bright_cyan\" },\n        { type: \"usage\", icon: \"↑\", text: \"{{inputTokens}}\", color: \"white\", background: \"bg_bright_green\" },\n        { type: \"usage\", icon: \"↓\", text: \"{{outputTokens}}\", color: \"white\", background: \"bg_bright_yellow\" }\n      ] \n    }\n  };\n}\n\n/**\n * 创建配置备份\n */\nexport function backupConfig(config: StatusLineConfig): string {\n  const backup = {\n    config,\n    timestamp: new Date().toISOString(),\n    version: \"1.0\"\n  };\n  return JSON.stringify(backup, null, 2);\n}\n\n/**\n * 从备份恢复配置\n */\nexport function restoreConfig(backupStr: string): StatusLineConfig | null {\n  try {\n    const backup = JSON.parse(backupStr);\n    if (backup && backup.config && backup.timestamp) {\n      return backup.config as StatusLineConfig;\n    }\n    return null;\n  } catch (error) {\n    console.error(\"Failed to restore config from backup:\", error);\n    return null;\n  }\n}\n"
  },
  {
    "path": "packages/ui/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "packages/ui/tsconfig.app.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ES2022\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"moduleDetection\": \"force\",\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\n        \"./src/*\"\n      ]\n    }\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "packages/ui/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ES2022\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"moduleDetection\": \"force\",\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n    \"strict\": true,\n    \"noUnusedLocals\": false,\n    \"noUnusedParameters\": false,\n    \"noFallthroughCasesInSwitch\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\n        \"./src/*\"\n      ]\n    }\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "packages/ui/vite.config.ts",
    "content": "import path from \"path\"\nimport react from \"@vitejs/plugin-react\"\nimport { defineConfig } from \"vite\"\nimport { viteSingleFile } from \"vite-plugin-singlefile\"\nimport tailwindcss from \"@tailwindcss/vite\"\n\n \nexport default defineConfig({\n  base: './',\n  plugins: [react(), tailwindcss(), viteSingleFile()],\n  resolve: {\n    alias: {\n      \"@\": path.resolve(__dirname, \"./src\"),\n    },\n  },\n})\n"
  },
  {
    "path": "pnpm-workspace.yaml",
    "content": "packages:\n  - 'packages/*'\n  - 'docs'\n"
  },
  {
    "path": "scripts/build-cli.js",
    "content": "#!/usr/bin/env node\n\nconst { execSync } = require('child_process');\nconst path = require('path');\nconst fs = require('fs');\n\nconsole.log('Building CLI package...');\n\ntry {\n  const rootDir = path.join(__dirname, '..');\n  const sharedDir = path.join(rootDir, 'packages/shared');\n  const cliDir = path.join(rootDir, 'packages/cli');\n  const serverDir = path.join(rootDir, 'packages/server');\n  const uiDir = path.join(rootDir, 'packages/ui');\n\n  // Step 0: Ensure shared package is built first\n  console.log('Ensuring shared package is built...');\n  const sharedDistDir = path.join(sharedDir, 'dist');\n  if (!fs.existsSync(sharedDistDir) || !fs.existsSync(path.join(sharedDistDir, 'index.js'))) {\n    console.log('Shared package not found, building it first...');\n    execSync('node scripts/build-shared.js', {\n      stdio: 'inherit',\n      cwd: rootDir\n    });\n  }\n\n  // Step 1: Build Server package first\n  console.log('Building Server package...');\n  execSync('node scripts/build-server.js', {\n    stdio: 'inherit',\n    cwd: rootDir\n  });\n\n  // Step 2: Build UI package\n  console.log('Building UI package...');\n  execSync('pnpm build', {\n    stdio: 'inherit',\n    cwd: uiDir\n  });\n\n  // Step 3: Create CLI dist directory\n  const cliDistDir = path.join(cliDir, 'dist');\n  if (!fs.existsSync(cliDistDir)) {\n    fs.mkdirSync(cliDistDir, { recursive: true });\n  }\n\n  // Step 4: Build the CLI application\n  console.log('Building CLI application...');\n  execSync('esbuild src/cli.ts --bundle --platform=node --minify --tree-shaking=true --outfile=dist/cli.js', {\n    stdio: 'inherit',\n    cwd: cliDir\n  });\n\n  // Step 5: Copy tiktoken WASM file from server dist to CLI dist\n  console.log('Copying tiktoken_bg.wasm from server to CLI dist...');\n  const tiktokenSource = path.join(serverDir, 'dist/tiktoken_bg.wasm');\n  const tiktokenDest = path.join(cliDistDir, 'tiktoken_bg.wasm');\n\n  if (fs.existsSync(tiktokenSource)) {\n    fs.copyFileSync(tiktokenSource, tiktokenDest);\n    console.log('✓ tiktoken_bg.wasm copied successfully!');\n  } else {\n    console.warn('⚠ Warning: tiktoken_bg.wasm not found in server dist, skipping...');\n  }\n\n  // Step 6: Copy UI index.html from UI dist to CLI dist\n  console.log('Copying index.html from UI to CLI dist...');\n  const uiSource = path.join(uiDir, 'dist/index.html');\n  const uiDest = path.join(cliDistDir, 'index.html');\n\n  if (fs.existsSync(uiSource)) {\n    fs.copyFileSync(uiSource, uiDest);\n    console.log('✓ index.html copied successfully!');\n  } else {\n    console.warn('⚠ Warning: index.html not found in UI dist, skipping...');\n  }\n\n  // Step 7: Copy CLI dist to project root\n  console.log('\\nCopying CLI dist to project root...');\n  const rootDistDir = path.join(rootDir, 'dist');\n\n  // Remove existing dist directory in root if it exists\n  if (fs.existsSync(rootDistDir)) {\n    fs.rmSync(rootDistDir, { recursive: true, force: true });\n  }\n\n  // Copy CLI dist to root\n  fs.cpSync(cliDistDir, rootDistDir, { recursive: true });\n  console.log('✓ CLI dist copied to project root successfully!');\n\n  console.log('\\nCLI build completed successfully!');\n  console.log('\\nCLI dist contents:');\n  const files = fs.readdirSync(cliDistDir);\n  files.forEach(file => {\n    const filePath = path.join(cliDistDir, file);\n    const stats = fs.statSync(filePath);\n    const size = (stats.size / 1024 / 1024).toFixed(2);\n    console.log(`  - ${file} (${size} MB)`);\n  });\n} catch (error) {\n  console.error('CLI build failed:', error.message);\n  process.exit(1);\n}\n"
  },
  {
    "path": "scripts/build-core.js",
    "content": "#!/usr/bin/env node\n\nconst { execSync } = require('child_process');\nconst path = require('path');\nconst fs = require('fs');\n\nconsole.log('Building Core package (@musistudio/llms)...');\n\ntry {\n  const coreDir = path.join(__dirname, '../packages/core');\n\n  // Build using the core package's build script\n  console.log('Building core package (CJS and ESM)...');\n  execSync('pnpm build', {\n    stdio: 'inherit',\n    cwd: coreDir\n  });\n\n  console.log('Core package build completed successfully!');\n} catch (error) {\n  console.error('Core package build failed:', error.message);\n  process.exit(1);\n}\n"
  },
  {
    "path": "scripts/build-server.js",
    "content": "#!/usr/bin/env node\n\nconst { execSync } = require('child_process');\nconst path = require('path');\nconst fs = require('fs');\n\nconsole.log('Building Server package...');\n\ntry {\n  const serverDir = path.join(__dirname, '../packages/server');\n\n  // Create dist directory\n  const distDir = path.join(serverDir, 'dist');\n  if (!fs.existsSync(distDir)) {\n    fs.mkdirSync(distDir, { recursive: true });\n  }\n\n  // Generate type declaration files\n  console.log('Generating type declaration files...');\n  execSync('tsc --emitDeclarationOnly', {\n    stdio: 'inherit',\n    cwd: serverDir\n  });\n\n  // Build the server application\n  console.log('Building server application...');\n  // 使用 minify 和 tree-shaking 优化体积\n  execSync('esbuild src/index.ts --bundle --platform=node --minify --tree-shaking=true --outfile=dist/index.js', {\n    stdio: 'inherit',\n    cwd: serverDir\n  });\n\n  // Copy the tiktoken WASM file\n  console.log('Copying tiktoken WASM file...');\n  const tiktokenSource = path.join(__dirname, '../packages/server/node_modules/tiktoken/tiktoken_bg.wasm');\n  const tiktokenDest = path.join(__dirname, '../packages/server/dist/tiktoken_bg.wasm');\n\n  if (fs.existsSync(tiktokenSource)) {\n    fs.copyFileSync(tiktokenSource, tiktokenDest);\n    console.log('Tiktoken WASM file copied successfully!');\n  } else {\n    console.warn('Warning: tiktoken_bg.wasm not found, skipping...');\n  }\n\n  console.log('Server build completed successfully!');\n} catch (error) {\n  console.error('Server build failed:', error.message);\n  process.exit(1);\n}\n"
  },
  {
    "path": "scripts/build-shared.js",
    "content": "#!/usr/bin/env node\n\nconst { execSync } = require('child_process');\nconst path = require('path');\nconst fs = require('fs');\n\nconsole.log('Building Shared package...');\n\ntry {\n  const sharedDir = path.join(__dirname, '../packages/shared');\n\n  // Create dist directory\n  const distDir = path.join(sharedDir, 'dist');\n  if (!fs.existsSync(distDir)) {\n    fs.mkdirSync(distDir, { recursive: true });\n  }\n\n  // Generate type declaration files\n  console.log('Generating type declaration files...');\n  execSync('tsc --emitDeclarationOnly', {\n    stdio: 'inherit',\n    cwd: sharedDir\n  });\n\n  // Build the shared package\n  console.log('Building shared package...');\n  execSync('esbuild src/index.ts --bundle --platform=node --minify --tree-shaking=true --outfile=dist/index.js', {\n    stdio: 'inherit',\n    cwd: sharedDir\n  });\n\n  console.log('Shared package build completed successfully!');\n} catch (error) {\n  console.error('Shared package build failed:', error.message);\n  process.exit(1);\n}\n"
  },
  {
    "path": "scripts/build.js",
    "content": "#!/usr/bin/env node\n\nconst { execSync } = require('child_process');\n\nconsole.log('Building Claude Code Router (Monorepo)...');\n\ntry {\n  // Build core package first (@musistudio/llms)\n  console.log('Building core package (@musistudio/llms)...');\n  execSync('node scripts/build-core.js', { stdio: 'inherit' });\n\n  // Build shared package\n  console.log('Building shared package...');\n  execSync('node scripts/build-shared.js', { stdio: 'inherit' });\n\n  // Build CLI package (which will also build server and ui)\n  console.log('Building CLI package (includes server and ui)...');\n  execSync('node scripts/build-cli.js', { stdio: 'inherit' });\n\n  console.log('\\n✅ Build completed successfully!');\n  console.log('\\nArtifacts are available in packages/*/dist:');\n  console.log('  - packages/core/dist/     (Core package: @musistudio/llms)');\n  console.log('  - packages/shared/dist/   (Shared package)');\n  console.log('  - packages/cli/dist/      (CLI + UI + tiktoken)');\n  console.log('  - packages/server/dist/   (Server standalone)');\n  console.log('  - packages/ui/dist/       (UI standalone)');\n} catch (error) {\n  console.error('Build failed:', error.message);\n  process.exit(1);\n}"
  },
  {
    "path": "scripts/release.sh",
    "content": "#!/bin/bash\nset -e\n\n# 发布脚本\n# - Core 包作为 @musistudio/llms npm 包发布\n# - CLI 包作为 @CCR/cli npm 包发布\n# - Server 包发布为 Docker 镜像\n\nVERSION=$(node -p \"require('../packages/cli/package.json').version\")\nIMAGE_NAME=\"ccr/router\"\nIMAGE_TAG=\"${VERSION}\"\nLATEST_TAG=\"latest\"\n\necho \"=========================================\"\necho \"发布 Claude Code Router v${VERSION}\"\necho \"=========================================\"\n\n# 获取发布类型参数\nPUBLISH_TYPE=\"${1:-all}\"\n\ncase \"$PUBLISH_TYPE\" in\n  npm)\n    echo \"仅发布 npm 包...\"\n    ;;\n  docker)\n    echo \"仅发布 Docker 镜像...\"\n    ;;\n  all)\n    echo \"发布 npm 包和 Docker 镜像...\"\n    ;;\n  *)\n    echo \"用法: $0 [npm|docker|all]\"\n    echo \"  npm    - 仅发布到 npm\"\n    echo \"  docker - 仅发布到 Docker Hub\"\n    echo \"  all    - 发布到 npm 和 Docker Hub (默认)\"\n    exit 1\n    ;;\nesac\n\n# ===========================\n# 发布 Core npm 包 (@musistudio/llms)\n# ===========================\npublish_core_npm() {\n  echo \"\"\n  echo \"=========================================\"\n  echo \"发布 npm 包 @musistudio/llms\"\n  echo \"=========================================\"\n\n  # 检查是否已登录 npm\n  if ! npm whoami &>/dev/null; then\n    echo \"错误: 未登录 npm，请先运行: npm login\"\n    exit 1\n  fi\n\n  CORE_DIR=\"../packages/core\"\n  CORE_VERSION=$(node -p \"require('../packages/core/package.json').version\")\n\n  # 复制 README 到 core 包\n  cp ../README.md \"$CORE_DIR/\" 2>/dev/null || echo \"README.md 不存在，跳过...\"\n  cp ../LICENSE \"$CORE_DIR/\" 2>/dev/null || echo \"LICENSE 文件不存在，跳过...\"\n\n  # 发布到 npm\n  cd \"$CORE_DIR\"\n  echo \"执行 npm publish...\"\n  npm publish --access public\n\n  echo \"\"\n  echo \"✅ Core npm 包发布成功!\"\n  echo \"   包名: @musistudio/llms@${CORE_VERSION}\"\n}\n\n# ===========================\n# 发布 CLI npm 包\n# ===========================\npublish_npm() {\n  echo \"\"\n  echo \"=========================================\"\n  echo \"发布 npm 包 @CCR/cli\"\n  echo \"=========================================\"\n\n  # 检查是否已登录 npm\n  if ! npm whoami &>/dev/null; then\n    echo \"错误: 未登录 npm，请先运行: npm login\"\n    exit 1\n  fi\n\n  # 备份原始 package.json\n  CLI_DIR=\"../packages/cli\"\n  BACKUP_DIR=\"../packages/cli/.backup\"\n  mkdir -p \"$BACKUP_DIR\"\n  cp \"$CLI_DIR/package.json\" \"$BACKUP_DIR/package.json.bak\"\n\n  # 创建临时的发布用 package.json\n  node -e \"\n    const pkg = require('../packages/cli/package.json');\n    pkg.name = '@CCR/cli';\n    delete pkg.scripts;\n    pkg.files = ['dist/*', 'README.md', 'LICENSE'];\n    pkg.dependencies = {};\n    // 移除 workspace 依赖\n    delete pkg.dependencies['@CCR/shared'];\n    delete pkg.dependencies['@CCR/server'];\n    pkg.dependencies['@musistudio/llms'] = require('../packages/server/package.json').dependencies['@musistudio/llms'];\n    pkg.peerDependencies = {\n      'node': '>=18.0.0'\n    };\n    pkg.engines = {\n      'node': '>=18.0.0'\n    };\n    require('fs').writeFileSync('../packages/cli/package.publish.json', JSON.stringify(pkg, null, 2));\n  \"\n\n  # 使用发布版本的 package.json\n  mv \"$CLI_DIR/package.json\" \"$BACKUP_DIR/package.json.original\"\n  mv \"$CLI_DIR/package.publish.json\" \"$CLI_DIR/package.json\"\n\n  # 复制 README 和 LICENSE\n  cp ../README.md \"$CLI_DIR/\"\n  cp ../LICENSE \"$CLI_DIR/\" 2>/dev/null || echo \"LICENSE 文件不存在，跳过...\"\n\n  # 发布到 npm\n  cd \"$CLI_DIR\"\n  echo \"执行 npm publish...\"\n  npm publish --access public\n\n  # 恢复原始 package.json\n  mv \"$BACKUP_DIR/package.json.original\" \"$CLI_DIR/package.json\"\n\n  echo \"\"\n  echo \"✅ npm 包发布成功!\"\n  echo \"   包名: @CCR/cli@${VERSION}\"\n}\n\n# ===========================\n# 发布 Docker 镜像\n# ===========================\npublish_docker() {\n  echo \"\"\n  echo \"=========================================\"\n  echo \"发布 Docker 镜像\"\n  echo \"=========================================\"\n\n  # 检查是否已登录 Docker\n  if ! docker info &>/dev/null; then\n    echo \"错误: Docker 未运行\"\n    exit 1\n  fi\n\n  # 构建 Docker 镜像\n  echo \"构建 Docker 镜像 ${IMAGE_NAME}:${IMAGE_TAG}...\"\n  docker build -t \"${IMAGE_NAME}:${IMAGE_TAG}\" -f ../packages/server/Dockerfile ..\n\n  # 标记为 latest\n  echo \"标记为 latest...\"\n  docker tag \"${IMAGE_NAME}:${IMAGE_TAG}\" \"${IMAGE_NAME}:${LATEST_TAG}\"\n\n  # 推送到 Docker Hub\n  echo \"推送 ${IMAGE_NAME}:${IMAGE_TAG}...\"\n  docker push \"${IMAGE_NAME}:${IMAGE_TAG}\"\n\n  echo \"推送 ${IMAGE_NAME}:${LATEST_TAG}...\"\n  docker push \"${IMAGE_NAME}:${LATEST_TAG}\"\n\n  echo \"\"\n  echo \"✅ Docker 镜像发布成功!\"\n  echo \"   镜像: ${IMAGE_NAME}:${IMAGE_TAG}\"\n  echo \"   镜像: ${IMAGE_NAME}:latest\"\n}\n\n# ===========================\n# 执行发布\n# ===========================\nif [ \"$PUBLISH_TYPE\" = \"npm\" ] || [ \"$PUBLISH_TYPE\" = \"all\" ]; then\n  publish_core_npm\n  publish_npm\nfi\n\nif [ \"$PUBLISH_TYPE\" = \"docker\" ] || [ \"$PUBLISH_TYPE\" = \"all\" ]; then\n  publish_docker\nfi\n\necho \"\"\necho \"=========================================\"\necho \"🎉 发布完成!\"\necho \"=========================================\"\n"
  },
  {
    "path": "tsconfig.base.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"module\": \"CommonJS\",\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"resolveJsonModule\": true,\n    \"moduleResolution\": \"node\",\n    \"noImplicitAny\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"sourceMap\": true,\n    \"declaration\": true,\n    \"typeRoots\": [\"./node_modules/@types\", \"./packages/*/node_modules/@types\"]\n  }\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"module\": \"CommonJS\",\n    \"outDir\": \"./dist\",\n    \"rootDir\": \"./src\",\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"resolveJsonModule\": true,\n    \"moduleResolution\": \"node\",\n    \"noImplicitAny\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"sourceMap\": true,\n    \"declaration\": true\n  },\n  \"include\": [\"src/**/*.ts\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  }
]