├── .markdownlint.json ├── jest.config.js ├── wrangler.toml ├── .gitignore ├── tsconfig.json ├── package.json ├── src ├── logger.ts ├── types.ts ├── formatResponseGemini.ts ├── index.ts ├── streamResponseGemini.ts └── formatRequestGemini.ts ├── docs ├── index.md └── README-outline.md ├── MODEL_SELECTION.md ├── CLAUDE.md ├── TEST_GUIDE.md ├── TEST_GUIDE.zh.md ├── router-comparison.zh.md ├── MULTI_WORKER_DEPLOYMENT.md ├── router-comparison.md ├── ENVIRONMENT_VARIABLES.zh.md ├── README.zh.md ├── ENVIRONMENT_VARIABLES.md ├── README.md └── LICENSE /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "MD013": false 3 | } -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | testMatch: ['**/tests/**/*.test.ts'], 5 | collectCoverageFrom: [ 6 | 'src/**/*.ts', 7 | '!src/**/*.d.ts', 8 | ], 9 | coverageDirectory: 'coverage', 10 | coverageReporters: ['text', 'lcov', 'html'], 11 | }; 12 | -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "claude-gemini-router" 2 | main = "src/index.ts" 3 | compatibility_date = "2023-12-01" 4 | 5 | routes = [ 6 | { pattern = "cgr.jimmysong.io", custom_domain = true } 7 | ] 8 | 9 | # Default environment variables 10 | [vars] 11 | GEMINI_MODEL = "gemini-2.5-flash" 12 | 13 | # Enable logging 14 | [observability] 15 | enabled = true 16 | head_sampling_rate = 1 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | 7 | # Build outputs 8 | dist/ 9 | build/ 10 | *.tsbuildinfo 11 | 12 | # Environment variables 13 | .env 14 | .env.local 15 | .env.production 16 | .env.test 17 | 18 | # Wrangler 19 | .wrangler/ 20 | worker/ 21 | 22 | # IDE 23 | .vscode/ 24 | .idea/ 25 | *.swp 26 | *.swo 27 | 28 | # OS 29 | .DS_Store 30 | Thumbs.db 31 | 32 | # Logs 33 | logs/ 34 | *.log 35 | 36 | # Coverage 37 | coverage/ 38 | .nyc_output/ 39 | 40 | # Temporary files 41 | *.tmp 42 | *.temp -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": ["ES2020"], 6 | "declaration": true, 7 | "outDir": "./dist", 8 | "strict": false, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "types": ["jest", "node"], 15 | "allowSyntheticDefaultImports": true, 16 | "noImplicitAny": false 17 | }, 18 | "include": [ 19 | "src/**/*.ts", 20 | "tests/**/*.ts" 21 | ], 22 | "exclude": [ 23 | "node_modules", 24 | "dist" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "claude-gemini-router", 3 | "version": "1.0.0", 4 | "description": "A Cloudflare Worker that routes Claude Code requests to Google Gemini API", 5 | "main": "src/index.ts", 6 | "scripts": { 7 | "dev": "wrangler dev", 8 | "deploy": "wrangler deploy", 9 | "test": "jest --testPathIgnorePatterns=integration", 10 | "test:integration": "jest --testPathPattern=integration", 11 | "test:all": "jest", 12 | "test:watch": "jest --watch" 13 | }, 14 | "keywords": [ 15 | "claude", 16 | "gemini", 17 | "router", 18 | "cloudflare-worker", 19 | "anthropic", 20 | "google", 21 | "llm" 22 | ], 23 | "author": "Jimmy Song", 24 | "license": "MIT", 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/rootsongjc/claude-gemini-router" 28 | }, 29 | "devDependencies": { 30 | "@types/jest": "^29.5.5", 31 | "@types/node-fetch": "^2.6.4", 32 | "jest": "^29.7.0", 33 | "node-fetch": "^2.7.0", 34 | "ts-jest": "^29.1.1", 35 | "typescript": "^5.2.2" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/logger.ts: -------------------------------------------------------------------------------- 1 | export class Logger { 2 | private prefix: string; 3 | 4 | constructor(prefix: string = 'Claude-Gemini-Router') { 5 | this.prefix = prefix; 6 | } 7 | 8 | private formatMessage(level: string, message: string, ...args: any[]): string { 9 | const timestamp = new Date().toISOString(); 10 | const formattedArgs = args.length > 0 ? ' ' + args.map(arg => 11 | typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg) 12 | ).join(' ') : ''; 13 | 14 | return `[${timestamp}] [${this.prefix}] ${level.toUpperCase()}: ${message}${formattedArgs}`; 15 | } 16 | 17 | info(message: string, ...args: any[]) { 18 | console.log(this.formatMessage('info', message, ...args)); 19 | } 20 | 21 | warn(message: string, ...args: any[]) { 22 | console.warn(this.formatMessage('warn', message, ...args)); 23 | } 24 | 25 | error(message: string, ...args: any[]) { 26 | console.error(this.formatMessage('error', message, ...args)); 27 | } 28 | 29 | debug(message: string, ...args: any[]) { 30 | console.log(this.formatMessage('debug', message, ...args)); 31 | } 32 | } 33 | 34 | export const logger = new Logger(); 35 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Claude-Gemini Router Documentation 2 | 3 | Welcome to the Claude-Gemini Router documentation. This directory contains additional documentation resources beyond the main README files. 4 | 5 | ## Quick Navigation 6 | 7 | ### Root Level Documentation 8 | 9 | - [README.md](../README.md) - Main project documentation (English) 10 | - [README.zh.md](../README.zh.md) - Main project documentation (Chinese) 11 | - [ENVIRONMENT_VARIABLES.md](../ENVIRONMENT_VARIABLES.md) - Environment variables guide (English) 12 | - [ENVIRONMENT_VARIABLES.zh.md](../ENVIRONMENT_VARIABLES.zh.md) - Environment variables guide (Chinese) 13 | - [TEST_GUIDE.md](../TEST_GUIDE.md) - Testing guide (English) 14 | - [TEST_GUIDE.zh.md](../TEST_GUIDE.zh.md) - Testing guide (Chinese) 15 | 16 | ### Documentation Resources 17 | 18 | - [README-outline.md](README-outline.md) - Documentation structure and guidelines for contributors 19 | 20 | ## Language Support 21 | 22 | This project provides documentation in two languages: 23 | 24 | - **English** (Primary) 25 | - **Chinese** (中文) 26 | 27 | ## Contributing to Documentation 28 | 29 | Please refer to the [documentation structure outline](README-outline.md) for guidelines on maintaining and expanding the documentation. 30 | 31 | ## Future Documentation 32 | 33 | This directory is prepared for future expansion with additional guides and resources as the project grows. 34 | -------------------------------------------------------------------------------- /MODEL_SELECTION.md: -------------------------------------------------------------------------------- 1 | # Model Selection Mechanism 2 | 3 | **The claude-gemini-router supports flexible model selection with the following priority order:** 4 | 5 | 1. **Request Model Parameter**: The `model` field in the API request takes precedence. You can specify any valid Gemini model name directly: 6 | ```json 7 | { 8 | "model": "gemini-1.5-flash", 9 | "messages": [{"role": "user", "content": "Hello!"}] 10 | } 11 | ``` 12 | 13 | 2. **Environment Variable Fallback**: If no model is specified in the request, the `GEMINI_MODEL` environment variable is used as a fallback default. 14 | 15 | 3. **System Default**: If neither is specified, defaults to `gemini-2.5-flash`. 16 | 17 | ### Model Name Handling 18 | 19 | - **Direct Gemini Models**: Names starting with `gemini-` are used as-is (e.g., `gemini-1.5-flash`, `gemini-2.5-pro`). 20 | - **Anthropic Model Mapping**: Automatically maps Anthropic model names: 21 | - `haiku` → `gemini-2.5-flash` 22 | - `sonnet` → `gemini-2.5-pro` 23 | - `opus` → `gemini-2.5-pro` 24 | - **Unknown Models**: Default to `gemini-2.5-flash`. 25 | 26 | ### API Response 27 | 28 | The API response includes the `model` field showing the actual model used: 29 | ```json 30 | { 31 | "id": "msg_1234567890", 32 | "type": "message", 33 | "role": "assistant", 34 | "content": [{"type": "text", "text": "Hello!"}], 35 | "model": "gemini-1.5-flash", 36 | "usage": {"input_tokens": 4, "output_tokens": 8} 37 | } 38 | ``` 39 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # CLAUDE.md 2 | 3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 | 5 | ## Common Commands 6 | 7 | - **Development Server:** `npm run dev` (starts a local development server using Wrangler) 8 | - **Deployment:** `npm run deploy` (deploys the worker to Cloudflare) 9 | - **Unit Tests:** `npm run test` (runs unit tests, ignoring integration tests) 10 | - **Integration Tests:** `npm run test:integration` (runs integration tests) 11 | - **All Tests:** `npm run test:all` (runs all tests) 12 | - **Watch Tests:** `npm run test:watch` (runs tests in watch mode) 13 | 14 | ## Code Architecture 15 | 16 | This project is a Cloudflare Worker designed to act as a proxy between the Anthropic Claude API and the Google Gemini API. 17 | 18 | - **`src/index.ts`**: The main entry point for the Cloudflare Worker, handling incoming requests and routing them. 19 | - **`src/formatRequestGemini.ts`**: Contains logic for transforming incoming Anthropic API requests into the Google Gemini API format. 20 | - **`src/formatResponseGemini.ts`**: Contains logic for transforming responses from the Google Gemini API back into the Anthropic API format. 21 | - **`src/streamResponseGemini.ts`**: Handles the specifics of streaming responses between the two API formats. 22 | - **`src/logger.ts`**: Provides logging utilities for the worker. 23 | - **`src/types.ts`**: Defines shared TypeScript types and interfaces used across the project. 24 | -------------------------------------------------------------------------------- /TEST_GUIDE.md: -------------------------------------------------------------------------------- 1 | # Test Guide for Claude-Gemini-Router 2 | 3 | This guide explains how to run the comprehensive test suite for the Claude-Gemini-Router integration. 4 | 5 | ## Test Structure 6 | 7 | The test suite is organized into three main categories: 8 | 9 | ### 1. Unit Tests (Formatters) 10 | - **Location**: `tests/formatters/` 11 | - **Purpose**: Test the request/response formatters with sample payloads 12 | - **Run**: `npm test` 13 | 14 | #### Test Coverage: 15 | - ✅ **formatRequest.test.ts**: Tests Anthropic to OpenAI request formatting 16 | - ✅ **formatRequestGemini.test.ts**: Tests Anthropic to Gemini request formatting 17 | - ✅ **formatResponse.test.ts**: Tests OpenAI to Anthropic response formatting 18 | - ✅ **formatResponseGemini.test.ts**: Tests Gemini to Anthropic response formatting 19 | 20 | ### 2. Integration Tests 21 | - **Location**: `tests/integration/` 22 | - **Purpose**: Test the actual Worker with the Gemini API for both streaming and non-streaming 23 | - **Run**: `npm run test:integration` 24 | 25 | #### Prerequisites: 26 | 1. Start the development server: `wrangler dev` 27 | 2. Set environment variable: `export GEMINI_API_KEY=your_api_key` 28 | 29 | #### Test Coverage: 30 | - ✅ **Non-streaming requests**: Simple messages, system messages, tool calls 31 | - ✅ **Streaming requests**: Streaming text, streaming tool calls 32 | - ✅ **Error handling**: Invalid models, malformed requests 33 | - ✅ **Worker endpoints**: Index page, 404 handling 34 | 35 | ### 3. Tool Call Validation Tests 36 | - **Purpose**: Validate tool_call scenarios and ensure proper formatting 37 | - **Coverage**: Complete tool call sequences, incomplete tool calls, multiple tool calls 38 | 39 | ## Running Tests 40 | 41 | ### Run All Tests 42 | ```bash 43 | npm run test:all 44 | ``` 45 | 46 | ### Run Unit Tests Only 47 | ```bash 48 | npm test 49 | ``` 50 | 51 | ### Run Integration Tests Only 52 | ```bash 53 | # Prerequisites: Start wrangler dev and set GEMINI_API_KEY 54 | wrangler dev & 55 | export GEMINI_API_KEY=your_api_key 56 | npm run test:integration 57 | ``` 58 | 59 | ### Run Tests in Watch Mode 60 | ```bash 61 | npm run test:watch 62 | ``` 63 | 64 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface Env { 2 | GEMINI_API_KEY: string; 3 | GEMINI_MODEL?: string; 4 | } 5 | 6 | export interface AnthropicMessage { 7 | role: 'user' | 'assistant' | 'system'; 8 | content: string | AnthropicContent[]; 9 | } 10 | 11 | export interface AnthropicContent { 12 | type: 'text' | 'image' | 'tool_use' | 'tool_result'; 13 | text?: string; 14 | source?: { 15 | type: 'base64'; 16 | media_type: string; 17 | data: string; 18 | }; 19 | name?: string; 20 | input?: any; 21 | tool_use_id?: string; 22 | content?: string; 23 | is_error?: boolean; 24 | } 25 | 26 | export interface AnthropicRequest { 27 | model: string; 28 | messages: AnthropicMessage[]; 29 | max_tokens?: number; 30 | temperature?: number; 31 | top_p?: number; 32 | stop_sequences?: string[]; 33 | stream?: boolean; 34 | system?: string; 35 | tools?: AnthropicTool[]; 36 | } 37 | 38 | export interface AnthropicTool { 39 | name: string; 40 | description: string; 41 | input_schema: { 42 | type: string; 43 | properties: Record; 44 | required?: string[]; 45 | }; 46 | } 47 | 48 | export interface GeminiRequest { 49 | model: string; 50 | request: { 51 | contents: GeminiContent[]; 52 | generationConfig?: { 53 | temperature?: number; 54 | topP?: number; 55 | maxOutputTokens?: number; 56 | stopSequences?: string[]; 57 | }; 58 | systemInstruction?: { 59 | parts: Array<{ text: string }>; 60 | }; 61 | tools?: { 62 | functionDeclarations: GeminiFunctionDeclaration[]; 63 | }[]; 64 | }; 65 | isStream: boolean; 66 | } 67 | 68 | export interface GeminiContent { 69 | role: 'user' | 'model'; 70 | parts: GeminiPart[]; 71 | } 72 | 73 | export interface GeminiPart { 74 | text?: string; 75 | inlineData?: { 76 | mimeType: string; 77 | data: string; 78 | }; 79 | functionCall?: { 80 | name: string; 81 | args: any; 82 | }; 83 | functionResponse?: { 84 | name: string; 85 | response: any; 86 | }; 87 | } 88 | 89 | export interface GeminiFunctionDeclaration { 90 | name: string; 91 | description: string; 92 | parameters: { 93 | type: string; 94 | properties: Record; 95 | required?: string[]; 96 | }; 97 | } 98 | -------------------------------------------------------------------------------- /docs/README-outline.md: -------------------------------------------------------------------------------- 1 | # Documentation Structure Outline 2 | 3 | This document outlines the target documentation structure for the Claude-Gemini Router project to help contributors understand the organization and maintain consistency. 4 | 5 | ## Root Level Documentation 6 | 7 | ### Primary Documentation Files 8 | 9 | - **README.md** (English, default) - Main project documentation including overview, quick start, and basic usage 10 | - **README.zh.md** (Chinese) - Chinese translation of the main README 11 | 12 | ### Specialized Documentation Files 13 | 14 | - **ENVIRONMENT_VARIABLES.md** (English) - Comprehensive guide to all environment variables and configuration options 15 | - **ENVIRONMENT_VARIABLES.zh.md** (Chinese) - Chinese translation of environment variables guide 16 | - **TEST_GUIDE.md** (English) - Testing documentation including setup, running tests, and writing new tests 17 | - **TEST_GUIDE.zh.md** (Chinese) - Chinese translation of testing guide 18 | 19 | ## Documentation Directory Structure 20 | 21 | ``` 22 | docs/ 23 | ├── README-outline.md # This file - structure documentation 24 | ├── index.md # Documentation index/landing page 25 | └── [future subdirectories] # For expanded documentation as needed 26 | ``` 27 | 28 | ## Language Support 29 | 30 | The project supports bilingual documentation: 31 | 32 | - **English** - Primary language (default files without language suffix) 33 | - **Chinese** - Secondary language (files with `.zh.md` suffix) 34 | 35 | ## File Naming Conventions 36 | 37 | - English files: `FILENAME.md` 38 | - Chinese files: `FILENAME.zh.md` 39 | - Use UPPERCASE for root-level documentation files (following common conventions) 40 | - Use lowercase for files within the `docs/` directory 41 | 42 | ## Content Guidelines 43 | 44 | ### README Files 45 | 46 | - Project overview and purpose 47 | - Quick start guide 48 | - Basic usage examples 49 | - Installation instructions 50 | - Contributing guidelines 51 | - License information 52 | 53 | ### Environment Variables Documentation 54 | 55 | - Complete list of all environment variables 56 | - Default values and acceptable ranges 57 | - Usage examples 58 | - Security considerations 59 | - Configuration best practices 60 | 61 | ### Test Guide Documentation 62 | 63 | - Testing framework setup 64 | - How to run different test suites 65 | - Writing new tests 66 | - Test coverage expectations 67 | - CI/CD integration details 68 | 69 | ### Future Expansion 70 | 71 | The `docs/` directory is prepared for future expansion and may include: 72 | 73 | - API documentation 74 | - Architecture guides 75 | - Deployment guides 76 | - Performance optimization 77 | - Troubleshooting guides 78 | - Advanced configuration examples 79 | 80 | ## Maintenance Notes 81 | 82 | - Keep language versions synchronized 83 | - Update this outline when adding new documentation 84 | - Ensure all documentation follows the established structure 85 | - Consider using automated tools for translation consistency 86 | - Review and update documentation with each major release 87 | -------------------------------------------------------------------------------- /TEST_GUIDE.zh.md: -------------------------------------------------------------------------------- 1 | # Claude-Gemini-Router 测试指南 2 | 3 | 本指南说明了如何运行 Claude-Gemini-Router 集成的全面测试套件。 4 | 5 | ## 测试结构 6 | 7 | 测试套件分为三个主要类别: 8 | 9 | ### 1. 单元测试(格式化器) 10 | 11 | - **位置**:`tests/formatters/` 12 | - **目的**:使用示例载荷测试请求/响应格式化器 13 | - **运行**:`npm test` 14 | 15 | #### 测试覆盖 16 | 17 | - ✅ **formatRequest.test.ts**:测试从 Anthropic 到 OpenAI 的请求格式化 18 | - ✅ **formatRequestGemini.test.ts**:测试从 Anthropic 到 Gemini 的请求格式化 19 | - ✅ **formatResponse.test.ts**:测试从 OpenAI 到 Anthropic 的响应格式化 20 | - ✅ **formatResponseGemini.test.ts**:测试从 Gemini 到 Anthropic 的响应格式化 21 | 22 | ### 2. 集成测试 23 | 24 | - **位置**:`tests/integration/` 25 | - **目的**:测试使用 Gemini API 的实际 Worker,包括流式和非流式请求 26 | - **运行**:`npm run test:integration` 27 | 28 | #### 先决条件 29 | 30 | 1. 启动开发服务器:`wrangler dev` 31 | 2. 设置环境变量:`export GEMINI_API_KEY=your_api_key` 32 | 33 | #### 测试覆盖 34 | 35 | - ✅ **非流式请求**:简单消息、系统消息、工具调用 36 | - ✅ **流式请求**:流式文本、流式工具调用 37 | - ✅ **错误处理**:无效模型、格式错误请求 38 | - ✅ **Worker 端点**:索引页、404 处理 39 | 40 | ### 3. 工具调用验证测试 41 | 42 | - **目的**:验证工具调用场景并确保正确的格式化 43 | - **覆盖**:完整工具调用序列、不完整工具调用、多重工具调用 44 | 45 | ## 运行测试 46 | 47 | ### 运行所有测试 48 | 49 | ```bash 50 | npm run test:all 51 | ``` 52 | 53 | ### 仅运行单元测试 54 | 55 | ```bash 56 | npm test 57 | ``` 58 | 59 | ### 仅运行集成测试 60 | 61 | ```bash 62 | # 先决条件:启动 wrangler dev 并设置 GEMINI_API_KEY 63 | wrangler dev & 64 | export GEMINI_API_KEY=your_api_key 65 | npm run test:integration 66 | ``` 67 | 68 | ### 以监视模式运行测试 69 | 70 | ```bash 71 | npm run test:watch 72 | ``` 73 | 74 | ## 测试结果摘要 75 | 76 | ### 单元测试 ✅ 77 | 78 | - **总测试数**:54 个通过 79 | - **测试套件**:4 个通过 80 | - **覆盖范围**:使用全面示例载荷的请求/响应格式化器 81 | 82 | ### 集成测试 🔄 83 | 84 | - **先决条件**:需要运行 `wrangler dev` 和有效的 `GEMINI_API_KEY` 85 | - **测试场景**: 86 | - 非流式请求 87 | - 流式请求 88 | - 工具调用验证 89 | - 错误处理 90 | - Worker 端点验证 91 | 92 | ## 测试的示例载荷 93 | 94 | ### 简单消息 95 | 96 | ```json 97 | { 98 | "model": "claude-3-haiku-20240307", 99 | "messages": [ 100 | { 101 | "role": "user", 102 | "content": "Hello, how are you today?" 103 | } 104 | ], 105 | "temperature": 0.7 106 | } 107 | ``` 108 | 109 | ### 系统消息 110 | 111 | ```json 112 | { 113 | "model": "claude-3-sonnet-20240229", 114 | "messages": [ 115 | { 116 | "role": "user", 117 | "content": "What is the weather like?" 118 | } 119 | ], 120 | "system": "You are a helpful weather assistant.", 121 | "temperature": 0.3 122 | } 123 | ``` 124 | 125 | ### 工具调用 126 | 127 | ```json 128 | { 129 | "model": "claude-3-sonnet-20240229", 130 | "messages": [ 131 | { 132 | "role": "user", 133 | "content": "What's the weather like in New York?" 134 | } 135 | ], 136 | "tools": [ 137 | { 138 | "name": "get_weather", 139 | "description": "Get the current weather in a given location", 140 | "input_schema": { 141 | "type": "object", 142 | "properties": { 143 | "location": { 144 | "type": "string", 145 | "description": "The city and state, e.g. San Francisco, CA" 146 | } 147 | }, 148 | "required": ["location"] 149 | } 150 | } 151 | ] 152 | } 153 | ``` 154 | 155 | ### 流式请求 156 | 157 | ```json 158 | { 159 | "model": "claude-3-haiku-20240307", 160 | "messages": [ 161 | { 162 | "role": "user", 163 | "content": "Tell me a short story" 164 | } 165 | ], 166 | "stream": true 167 | } 168 | ``` 169 | 170 | ## 测试验证 171 | 172 | ### 回归测试 ✅ 173 | 174 | - **模型映射**:Anthropic 和 Gemini 模型之间的正确映射 175 | - **请求格式化**:将 Anthropic 请求正确转换为 Gemini 格式 176 | - **响应格式化**:将 Gemini 响应正确转换为 Anthropic 格式 177 | - **工具调用验证**:正确处理 tool_calls 和 tool_results 178 | - **流式传输**:为流式响应生成正确的 SSE 事件 179 | 180 | ### 工具调用场景 ✅ 181 | 182 | - **完整工具调用**:与匹配工具结果的工具调用 183 | - **不完整工具调用**:没有匹配结果的工具调用(已过滤) 184 | - **多重工具调用**:多个同时工具调用 185 | - **混合内容**:同一响应中的文本和工具调用 186 | 187 | ## 故障排除 188 | 189 | ### 常见问题 190 | 191 | 1. **集成测试失败**:确保 `wrangler dev` 正在运行且 `GEMINI_API_KEY` 已设置 192 | 2. **测试超时**:为慢速 API 响应增加超时时间 193 | 3. **类型错误**:确保使用 `npm install` 安装了所有依赖项 194 | 195 | ### 调试模式 196 | 197 | ```bash 198 | # 以详细输出运行测试 199 | npm test -- --verbose 200 | 201 | # 运行特定测试文件 202 | npm test -- formatRequest.test.ts 203 | ``` 204 | 205 | ## 下一步 206 | 207 | 成功运行测试后: 208 | 209 | 1. ✅ 单元测试验证格式化器逻辑 210 | 2. ✅ 集成测试验证端到端功能 211 | 3. ✅ 工具调用场景得到正确处理 212 | 4. ✅ 流式和非流式都正常工作 213 | 214 | 测试套件为 Claude-Gemini-Router 集成提供了全面的覆盖,确保与 Google Gemini API 后端的可靠操作。 215 | -------------------------------------------------------------------------------- /router-comparison.zh.md: -------------------------------------------------------------------------------- 1 | # 路由器对比:y-router 与 claude-gemini-router 2 | 3 | ## 概览对比 4 | 5 | | 方面 | y-router | claude-gemini-router | 6 | |--------|----------|---------------------| 7 | | **项目名称** | claude-gemini-router | claude-gemini-router | 8 | | **包名** | claude-gemini-router | claude-gemini-router | 9 | | **仓库地址** | https://github.com/rootsongjc/claude-gemini-router | https://github.com/rootsongjc/claude-gemini-router | 10 | | **作者** | luohy15 | Jimmy Song | 11 | | **许可证** | MIT | Apache 2.0 | 12 | | **版本** | 1.0.0 | 1.0.0 | 13 | 14 | ## 目的与功能 15 | 16 | | 方面 | y-router | claude-gemini-router | 17 | |--------|----------|---------------------| 18 | | **描述** | 一个简单的代理,使 Claude Code 能够与 OpenRouter 配合使用 | 一个 Cloudflare Worker,将 Claude Code 请求路由到 Google Gemini API | 19 | | **目标 API** | OpenRouter (兼容 OpenAI 的 API) | Google Gemini API | 20 | | **主要用例** | Anthropic 的 Claude API 与兼容 OpenAI 的 API 之间的转换 | Anthropic 的 Claude API 与 Google Gemini API 之间的转换 | 21 | | **API 格式转换** | Anthropic → OpenAI 格式 | Anthropic → Google Gemini 格式 | 22 | 23 | ## 技术配置 24 | 25 | | 方面 | y-router | claude-gemini-router | 26 | |--------|----------|---------------------| 27 | | **平台** | Cloudflare Workers | Cloudflare Workers | 28 | | **主入口点** | index.ts | src/index.ts | 29 | | **兼容日期** | 2025-05-30 | 2023-12-01 | 30 | | **TypeScript** | ✅ | ✅ | 31 | 32 | ## 默认模型值 33 | 34 | | 方面 | y-router | claude-gemini-router | 35 | |--------|----------|---------------------| 36 | | **默认模型** | 未指定(使用 OpenRouter 模型) | gemini-2.5-flash | 37 | | **模型示例** | - claude-sonnet-4-20250514
- moonshotai/kimi-k2
- google/gemini-2.5-flash
- moonshot-v1-8k | - gemini-2.5-flash | 38 | | **模型来源** | OpenRouter 目录 | Google Gemini | 39 | 40 | ## 安装/部署命令 41 | 42 | | 方面 | y-router | claude-gemini-router | 43 | |--------|----------|---------------------| 44 | | **快速安装** | 手动部署 | 手动部署 | 45 | | **开发** | `npm run dev` 或 `wrangler dev` | `npm run dev` 或 `wrangler dev` | 46 | | **部署** | `npm run deploy` 或 `wrangler deploy` | `npm run deploy` 或 `wrangler deploy` | 47 | | **先决条件** | Node.js, Wrangler CLI | Node.js, Wrangler CLI | 48 | 49 | ## 环境变量 50 | 51 | | 方面 | y-router | claude-gemini-router | 52 | |--------|----------|---------------------| 53 | | **API 密钥** | 使用 OpenRouter API 密钥作为 `ANTHROPIC_API_KEY` | `GEMINI_API_KEY` | 54 | | **基础 URL** | `OPENROUTER_BASE_URL` (可选,默认为 https://openrouter.ai/api/v1) | 不适用 | 55 | | **模型配置** | `ANTHROPIC_MODEL`, `ANTHROPIC_SMALL_FAST_MODEL` | `GEMINI_MODEL` | 56 | | **API 端点** | `ANTHROPIC_BASE_URL` | 不适用 | 57 | 58 | ## 示例端点 59 | 60 | | 方面 | y-router | claude-gemini-router | 61 | |--------|----------|---------------------| 62 | | **主端点** | `/v1/messages` | `/v1/messages` | 63 | | **示例域名** | cgr.jimmysong.io | cgr.jimmysong.io | 64 | | **请求格式** | Anthropic API 格式 | Anthropic API 格式 | 65 | | **响应格式** | Anthropic API 格式 | Anthropic API 格式 | 66 | 67 | ## 配置示例 68 | 69 | ### claude-gemini-router 配置 70 | 71 | ```toml 72 | # wrangler.toml 73 | name = "claude-gemini-router" 74 | main = "src/index.ts" 75 | compatibility_date = "2023-12-01" 76 | 77 | routes = [ 78 | { pattern = "cgr.jimmysong.io", custom_domain = true } 79 | ] 80 | 81 | # Default environment variables 82 | [vars] 83 | GEMINI_MODEL = "gemini-2.5-flash" 84 | 85 | # Enable logging 86 | [observability] 87 | enabled = true 88 | head_sampling_rate = 1 89 | ``` 90 | 91 | ## API 使用示例 92 | 93 | ### claude-gemini-router 94 | 95 | ```bash 96 | curl -X POST https://cgr.jimmysong.io/v1/messages \ 97 | -H "Content-Type: application/json" \ 98 | -H "x-api-key: your-api-key" \ 99 | -d '{ 100 | "model": "gemini-2.5-flash", 101 | "messages": [{"role": "user", "content": "Hello, Claude"}], 102 | "max_tokens": 100 103 | }' 104 | ``` 105 | 106 | ## 主要区别 107 | 108 | 1. **目标 API**:y-router 专注于 OpenRouter/兼容 OpenAI 的 API,而 claude-gemini-router 专注于 Google Gemini 109 | 2. **模型选择**:y-router 通过 OpenRouter 提供对多个提供商模型的访问,claude-gemini-router 专注于 Gemini 模型 110 | 3. **设置复杂性**:y-router 提供一键安装脚本,claude-gemini-router 需要手动部署 111 | 4. **环境变量**:不同的命名约定和要求 112 | 5. **默认模型**:y-router 使用各种 OpenRouter 模型,claude-gemini-router 默认为 gemini-2.5-flash 113 | 6. **域名示例**:y-router 使用 cc.yovy.app,claude-gemini-router 使用 cgr.jimmysong.io 114 | 115 | ## 测试与开发 116 | 117 | | 方面 | y-router | claude-gemini-router | 118 | |--------|----------|---------------------| 119 | | **测试** | 未指定 | Jest 及测试脚本 | 120 | | **测试脚本** | 不适用 | `npm run test`, `npm run test:integration`, `npm run test:all`, `npm run test:watch` | 121 | | **开发依赖** | 基本 | @types/jest, @types/node-fetch, jest, node-fetch, ts-jest, typescript | 122 | -------------------------------------------------------------------------------- /MULTI_WORKER_DEPLOYMENT.md: -------------------------------------------------------------------------------- 1 | # 多 Worker 部署指南 2 | 3 | 本指南详细说明如何部署多个 Worker 实例来支持不同的 Gemini 模型。 4 | 5 | ## 方法 1:使用不同的 Worker 名称 6 | 7 | ### 步骤 1:创建基础配置文件 8 | 9 | 保持原始的 `wrangler.toml` 文件作为模板。 10 | 11 | ### 步骤 2:部署快速模型 Worker 12 | 13 | ```bash 14 | # 部署快速模型 Worker 15 | wrangler deploy --name claude-gemini-flash 16 | 17 | # 设置 API 密钥 18 | wrangler secret put GEMINI_API_KEY --name claude-gemini-flash 19 | 20 | # 设置模型环境变量 21 | wrangler secret put GEMINI_MODEL --name claude-gemini-flash 22 | # 当提示时输入: gemini-2.5-flash 23 | ``` 24 | 25 | ### 步骤 3:部署专业模型 Worker 26 | 27 | ```bash 28 | # 部署专业模型 Worker 29 | wrangler deploy --name claude-gemini-pro 30 | 31 | # 设置 API 密钥 32 | wrangler secret put GEMINI_API_KEY --name claude-gemini-pro 33 | 34 | # 设置模型环境变量 35 | wrangler secret put GEMINI_MODEL --name claude-gemini-pro 36 | # 当提示时输入: gemini-2.5-pro 37 | ``` 38 | 39 | ### 步骤 4:配置 Shell 别名 40 | 41 | 在 `~/.zshrc` 或 `~/.bashrc` 中添加: 42 | 43 | ```bash 44 | # 快速模型别名 45 | alias claude-flash='ANTHROPIC_BASE_URL="https://claude-gemini-flash.your-subdomain.workers.dev" ANTHROPIC_API_KEY="$GEMINI_API_KEY" claude' 46 | 47 | # 专业模型别名 48 | alias claude-pro='ANTHROPIC_BASE_URL="https://claude-gemini-pro.your-subdomain.workers.dev" ANTHROPIC_API_KEY="$GEMINI_API_KEY" claude' 49 | ``` 50 | 51 | ## 方法 2:使用环境配置 52 | 53 | ### 步骤 1:修改 wrangler.toml 54 | 55 | ```toml 56 | name = "claude-gemini-router" 57 | main = "src/index.ts" 58 | compatibility_date = "2023-12-01" 59 | 60 | [vars] 61 | GEMINI_MODEL = "gemini-2.5-flash" 62 | 63 | [env.flash.vars] 64 | GEMINI_MODEL = "gemini-2.5-flash" 65 | 66 | [env.pro.vars] 67 | GEMINI_MODEL = "gemini-2.5-pro" 68 | 69 | [env.flash15.vars] 70 | GEMINI_MODEL = "gemini-1.5-flash" 71 | ``` 72 | 73 | ### 步骤 2:部署不同环境 74 | 75 | ```bash 76 | # 部署快速模型环境 77 | wrangler deploy --env flash 78 | wrangler secret put GEMINI_API_KEY --env flash 79 | 80 | # 部署专业模型环境 81 | wrangler deploy --env pro 82 | wrangler secret put GEMINI_API_KEY --env pro 83 | 84 | # 部署 1.5 flash 环境 85 | wrangler deploy --env flash15 86 | wrangler secret put GEMINI_API_KEY --env flash15 87 | ``` 88 | 89 | ### 步骤 3:配置 Shell 别名 90 | 91 | ```bash 92 | # 不同模型的别名 93 | alias claude-flash='ANTHROPIC_BASE_URL="https://claude-gemini-router-flash.your-subdomain.workers.dev" ANTHROPIC_API_KEY="$GEMINI_API_KEY" claude' 94 | alias claude-pro='ANTHROPIC_BASE_URL="https://claude-gemini-router-pro.your-subdomain.workers.dev" ANTHROPIC_API_KEY="$GEMINI_API_KEY" claude' 95 | alias claude-flash15='ANTHROPIC_BASE_URL="https://claude-gemini-router-flash15.your-subdomain.workers.dev" ANTHROPIC_API_KEY="$GEMINI_API_KEY" claude' 96 | ``` 97 | 98 | ## 方法 3:单个 Worker + 请求时指定模型(推荐) 99 | 100 | ### 最简单的方法: 101 | 102 | 1. 只部署一个 Worker 103 | 2. 在 API 请求中指定模型 104 | 105 | ```bash 106 | # 测试不同模型 107 | curl -X POST https://cgr.jimmysong.io/v1/messages \ 108 | -H "Content-Type: application/json" \ 109 | -H "x-api-key: $GEMINI_API_KEY" \ 110 | -d '{ 111 | "model": "gemini-1.5-flash", 112 | "messages": [{"role": "user", "content": "Hello"}] 113 | }' 114 | ``` 115 | 116 | **注意:** Claude Code 的环境变量如 `ANTHROPIC_MODEL` 对 Worker 端的模型选择没有影响。 117 | 118 | ## 验证部署 119 | 120 | ### 测试快速模型: 121 | ```bash 122 | curl -X POST https://claude-gemini-flash.your-subdomain.workers.dev/v1/messages \ 123 | -H "Content-Type: application/json" \ 124 | -H "x-api-key: $GEMINI_API_KEY" \ 125 | -d '{"messages": [{"role": "user", "content": "What model are you?"}]}' | \ 126 | python3 -c "import sys, json; print(json.load(sys.stdin)['model'])" 127 | ``` 128 | 129 | ### 测试专业模型: 130 | ```bash 131 | curl -X POST https://claude-gemini-pro.your-subdomain.workers.dev/v1/messages \ 132 | -H "Content-Type: application/json" \ 133 | -H "x-api-key: $GEMINI_API_KEY" \ 134 | -d '{"messages": [{"role": "user", "content": "What model are you?"}]}' | \ 135 | python3 -c "import sys, json; print(json.load(sys.stdin)['model'])" 136 | ``` 137 | 138 | ## 关于 Claude Code 环境变量的澄清 139 | 140 | ### 这些环境变量 **不会** 影响 Worker 的行为: 141 | 142 | ```bash 143 | # 这些对 Worker 没有影响 144 | export ANTHROPIC_MODEL="gemini-1.5-flash" 145 | export ANTHROPIC_SMALL_FAST_MODEL="gemini-1.5-flash" 146 | ``` 147 | 148 | ### 模型选择的实际优先级: 149 | 150 | 1. **API 请求中的 `model` 参数** (最高优先级) 151 | 2. **Worker 的 `GEMINI_MODEL` 环境变量** (fallback) 152 | 3. **系统默认** (`gemini-2.5-flash`) 153 | 154 | ### 推荐配置: 155 | 156 | ```bash 157 | # ~/.zshrc 或 ~/.bashrc 158 | export ANTHROPIC_BASE_URL="https://cgr.jimmysong.io" 159 | export ANTHROPIC_API_KEY="$GEMINI_API_KEY" 160 | 161 | # 用于不同模型的别名 162 | alias claude-flash='ANTHROPIC_BASE_URL="https://claude-gemini-flash.your-subdomain.workers.dev" ANTHROPIC_API_KEY="$GEMINI_API_KEY" claude' 163 | alias claude-pro='ANTHROPIC_BASE_URL="https://claude-gemini-pro.your-subdomain.workers.dev" ANTHROPIC_API_KEY="$GEMINI_API_KEY" claude' 164 | ``` 165 | -------------------------------------------------------------------------------- /src/formatResponseGemini.ts: -------------------------------------------------------------------------------- 1 | interface GeminiResponse { 2 | candidates: GeminiCandidate[]; 3 | promptFeedback?: any; 4 | usageMetadata?: any; 5 | } 6 | 7 | interface GeminiCandidate { 8 | content: GeminiContent; 9 | finishReason: string; 10 | index: number; 11 | safetyRatings?: any[]; 12 | } 13 | 14 | interface GeminiContent { 15 | parts: GeminiPart[]; 16 | role: string; 17 | } 18 | 19 | interface GeminiPart { 20 | text?: string; 21 | functionCall?: { 22 | name: string; 23 | args: any; 24 | }; 25 | } 26 | 27 | /** 28 | * Converts Gemini response to Anthropic format 29 | */ 30 | export function formatGeminiToAnthropic(response: GeminiResponse, model: string): any { 31 | const messageId = "msg_" + Date.now(); 32 | 33 | // Pick the first candidate as per specification 34 | const candidate = response.candidates[0]; 35 | if (!candidate) { 36 | throw new Error("No candidates found in Gemini response"); 37 | } 38 | 39 | let content: any = []; 40 | let stopReason = "end_turn"; 41 | 42 | // Process the parts in the candidate's content 43 | if (candidate.content && candidate.content.parts) { 44 | candidate.content.parts.forEach((part: GeminiPart) => { 45 | if (part.text) { 46 | // Text content 47 | content.push({ 48 | type: "text", 49 | text: part.text 50 | }); 51 | } else if (part.functionCall) { 52 | // Function call content 53 | content.push({ 54 | type: "tool_use", 55 | id: `toolu_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, 56 | name: part.functionCall.name, 57 | input: part.functionCall.args || {} 58 | }); 59 | // Set stop reason to tool_use when function calls are present 60 | stopReason = "tool_use"; 61 | } 62 | }); 63 | } 64 | 65 | // Map Gemini finish reasons to Anthropic stop reasons 66 | if (candidate.finishReason) { 67 | switch (candidate.finishReason) { 68 | case "STOP": 69 | stopReason = "end_turn"; 70 | break; 71 | case "MAX_TOKENS": 72 | stopReason = "max_tokens"; 73 | break; 74 | case "SAFETY": 75 | stopReason = "stop_sequence"; 76 | break; 77 | case "RECITATION": 78 | stopReason = "stop_sequence"; 79 | break; 80 | case "OTHER": 81 | stopReason = "end_turn"; 82 | break; 83 | default: 84 | // If there are function calls, keep tool_use, otherwise default to end_turn 85 | if (stopReason !== "tool_use") { 86 | stopReason = "end_turn"; 87 | } 88 | } 89 | } 90 | 91 | // Build the Anthropic response format 92 | const result = { 93 | id: messageId, 94 | type: "message", 95 | role: "assistant", 96 | content: content, 97 | stop_reason: stopReason, 98 | stop_sequence: null, 99 | model, 100 | usage: { 101 | input_tokens: response.usageMetadata?.promptTokenCount || 0, 102 | output_tokens: response.usageMetadata?.candidatesTokenCount || 0, 103 | } 104 | }; 105 | 106 | return result; 107 | } 108 | 109 | /** 110 | * Converts streaming Gemini response chunk to Anthropic format 111 | */ 112 | export function formatGeminiStreamToAnthropic(chunk: any, model: string): any { 113 | const messageId = "msg_" + Date.now(); 114 | 115 | if (!chunk.candidates || chunk.candidates.length === 0) { 116 | return null; 117 | } 118 | 119 | const candidate = chunk.candidates[0]; 120 | let content: any = []; 121 | let stopReason = null; 122 | 123 | // Process the parts in the candidate's content 124 | if (candidate.content && candidate.content.parts) { 125 | candidate.content.parts.forEach((part: GeminiPart) => { 126 | if (part.text) { 127 | // Text content 128 | content.push({ 129 | type: "text", 130 | text: part.text 131 | }); 132 | } else if (part.functionCall) { 133 | // Function call content 134 | content.push({ 135 | type: "tool_use", 136 | id: `toolu_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, 137 | name: part.functionCall.name, 138 | input: part.functionCall.args || {} 139 | }); 140 | } 141 | }); 142 | } 143 | 144 | // Handle finish reason for streaming 145 | if (candidate.finishReason) { 146 | switch (candidate.finishReason) { 147 | case "STOP": 148 | stopReason = "end_turn"; 149 | break; 150 | case "MAX_TOKENS": 151 | stopReason = "max_tokens"; 152 | break; 153 | case "SAFETY": 154 | stopReason = "stop_sequence"; 155 | break; 156 | case "RECITATION": 157 | stopReason = "stop_sequence"; 158 | break; 159 | case "OTHER": 160 | stopReason = "end_turn"; 161 | break; 162 | default: 163 | stopReason = content.some((c: any) => c.type === "tool_use") ? "tool_use" : "end_turn"; 164 | } 165 | } 166 | 167 | return { 168 | id: messageId, 169 | type: "message", 170 | role: "assistant", 171 | content: content, 172 | stop_reason: stopReason, 173 | stop_sequence: null, 174 | model, 175 | usage: { 176 | input_tokens: chunk.usageMetadata?.promptTokenCount || 0, 177 | output_tokens: chunk.usageMetadata?.candidatesTokenCount || 0, 178 | } 179 | }; 180 | } 181 | -------------------------------------------------------------------------------- /router-comparison.md: -------------------------------------------------------------------------------- 1 | # Router Comparison: y-router vs claude-gemini-router 2 | 3 | ## Overview Comparison 4 | 5 | | Aspect | y-router | claude-gemini-router | 6 | |--------|----------|---------------------| 7 | | **Project Name** | claude-gemini-router | claude-gemini-router | 8 | | **Package Name** | claude-gemini-router | claude-gemini-router | 9 | | **Repository URL** | | | 10 | | **Author** | luohy15 | Jimmy Song | 11 | | **License** | MIT | Apache 2.0 | 12 | | **Version** | 1.0.0 | 1.0.0 | 13 | 14 | ## Purpose & Functionality 15 | 16 | | Aspect | y-router | claude-gemini-router | 17 | |--------|----------|---------------------| 18 | | **Description** | A Simple Proxy enabling Claude Code to work with OpenRouter | A Cloudflare Worker that routes Claude Code requests to Google Gemini API | 19 | | **Target API** | OpenRouter (OpenAI-compatible APIs) | Google Gemini API | 20 | | **Primary Use Case** | Translation between Anthropic's Claude API and OpenAI-compatible APIs | Translation between Anthropic's Claude API and Google Gemini API | 21 | | **API Format Translation** | Anthropic → OpenAI format | Anthropic → Google Gemini format | 22 | 23 | ## Technical Configuration 24 | 25 | | Aspect | y-router | claude-gemini-router | 26 | |--------|----------|---------------------| 27 | | **Platform** | Cloudflare Workers | Cloudflare Workers | 28 | | **Main Entry Point** | index.ts | src/index.ts | 29 | | **Compatibility Date** | 2025-05-30 | 2023-12-01 | 30 | | **TypeScript** | ✅ | ✅ | 31 | 32 | ## Default Model Values 33 | 34 | | Aspect | y-router | claude-gemini-router | 35 | |--------|----------|---------------------| 36 | | **Default Model** | Not specified (uses OpenRouter models) | gemini-2.5-flash | 37 | | **Model Examples** | - claude-sonnet-4-20250514
- moonshotai/kimi-k2
- google/gemini-2.5-flash
- moonshot-v1-8k | - gemini-2.5-flash | 38 | | **Model Source** | OpenRouter catalog | Google Gemini | 39 | 40 | ## Install/Deploy Commands 41 | 42 | | Aspect | y-router | claude-gemini-router | 43 | |--------|----------|---------------------| 44 | | **Quick Install** | Manual deployment | Manual deployment | 45 | | **Development** | `npm run dev` or `wrangler dev` | `npm run dev` or `wrangler dev` | 46 | | **Deployment** | `npm run deploy` or `wrangler deploy` | `npm run deploy` or `wrangler deploy` | 47 | | **Prerequisites** | Node.js, Wrangler CLI | Node.js, Wrangler CLI | 48 | 49 | ## Environment Variables 50 | 51 | | Aspect | y-router | claude-gemini-router | 52 | |--------|----------|---------------------| 53 | | **API Key** | Uses OpenRouter API key as `ANTHROPIC_API_KEY` | `GEMINI_API_KEY` | 54 | | **Base URL** | `OPENROUTER_BASE_URL` (optional, defaults to ) | N/A | 55 | | **Model Config** | `ANTHROPIC_MODEL`, `ANTHROPIC_SMALL_FAST_MODEL` | `GEMINI_MODEL` | 56 | | **API Endpoint** | `ANTHROPIC_BASE_URL` | N/A | 57 | 58 | ## Example Endpoints 59 | 60 | | Aspect | y-router | claude-gemini-router | 61 | |--------|----------|---------------------| 62 | | **Primary Endpoint** | `/v1/messages` | `/v1/messages` | 63 | | **Example Domain** | cgr.jimmysong.io | cgr.jimmysong.io | 64 | | **Request Format** | Anthropic API format | Anthropic API format | 65 | | **Response Format** | Anthropic API format | Anthropic API format | 66 | 67 | ## Configuration Examples 68 | 69 | ### claude-gemini-router Configuration 70 | 71 | ```toml 72 | # wrangler.toml 73 | name = "claude-gemini-router" 74 | main = "src/index.ts" 75 | compatibility_date = "2023-12-01" 76 | 77 | routes = [ 78 | { pattern = "cgr.jimmysong.io", custom_domain = true } 79 | ] 80 | 81 | # Default environment variables 82 | [vars] 83 | GEMINI_MODEL = "gemini-2.5-flash" 84 | 85 | # Enable logging 86 | [observability] 87 | enabled = true 88 | head_sampling_rate = 1 89 | ``` 90 | 91 | ## API Usage Examples 92 | 93 | ### claude-gemini-router 94 | 95 | ```bash 96 | curl -X POST https://cgr.jimmysong.io/v1/messages \ 97 | -H "Content-Type: application/json" \ 98 | -H "x-api-key: your-api-key" \ 99 | -d '{ 100 | "model": "gemini-2.5-flash", 101 | "messages": [{"role": "user", "content": "Hello, Claude"}], 102 | "max_tokens": 100 103 | }' 104 | ``` 105 | 106 | ## Key Differences 107 | 108 | 1. **Target API**: y-router focuses on OpenRouter/OpenAI-compatible APIs, while claude-gemini-router targets Google Gemini 109 | 2. **Model Selection**: y-router offers access to multiple provider models through OpenRouter, claude-gemini-router focuses on Gemini models 110 | 3. **Setup Complexity**: y-router provides a one-line install script, claude-gemini-router requires manual deployment 111 | 4. **Environment Variables**: Different naming conventions and requirements 112 | 5. **Default Models**: y-router uses various OpenRouter models, claude-gemini-router defaults to gemini-2.5-flash 113 | 6. **Domain Examples**: y-router uses cc.yovy.app, claude-gemini-router uses cgr.jimmysong.io 114 | 115 | ## Testing & Development 116 | 117 | | Aspect | y-router | claude-gemini-router | 118 | |--------|----------|---------------------| 119 | | **Testing** | Not specified | Jest with test scripts | 120 | | **Test Scripts** | N/A | `npm run test`, `npm run test:integration`, `npm run test:all`, `npm run test:watch` | 121 | | **Development Dependencies** | Basic | @types/jest, @types/node-fetch, jest, node-fetch, ts-jest, typescript | 122 | -------------------------------------------------------------------------------- /ENVIRONMENT_VARIABLES.zh.md: -------------------------------------------------------------------------------- 1 | # 环境变量 2 | 3 | [English](./ENVIRONMENT_VARIABLES.md) | [中文](./ENVIRONMENT_VARIABLES.zh.md) 4 | 5 | 本文档描述了 claude-gemini-router 使用的环境变量。 6 | 7 | ## 必需变量 8 | 9 | ### GEMINI_API_KEY 10 | - **描述**: 你的 Google Gemini API 密钥 11 | - **必需**: 是 12 | - **类型**: 字符串(机密) 13 | - **示例**: `AIzaSyDxxxxxxxxxxxxxxxxxxxxxxxxxxx` 14 | - **获取方式**: 从 [Google AI Studio](https://makersuite.google.com/app/apikey) 获取 15 | - **配置方式**: 通过 `wrangler secret put GEMINI_API_KEY` 设置 16 | 17 | ## 可选变量 18 | 19 | ### GEMINI_MODEL(主要模型配置) 20 | - **描述**: **这是配置路由器使用哪个 Gemini 模型的主要方式。** 当请求中未指定模型时使用的默认 Gemini 模型 21 | - **必需**: 否 22 | - **类型**: 字符串 23 | - **默认值**: `gemini-2.5-flash` 24 | - **支持的值**: 25 | - `gemini-2.5-flash` - 最新快速模型(推荐) 26 | - `gemini-2.5-pro` - 高性能模型 27 | - `gemini-1.5-flash` - 快速模型 28 | - **配置方式**: 在 `wrangler.toml` 的 `[vars]` 部分设置 29 | - **重要**: 这个 Worker 端配置控制模型选择,而不是本地 shell 环境变量 30 | 31 | ## 配置方法 32 | 33 | ### 1. Cloudflare Workers 机密(推荐用于 API 密钥) 34 | 35 | 对于 API 密钥等敏感数据,使用 Wrangler 机密: 36 | 37 | ```bash 38 | # 安全地设置 API 密钥 39 | wrangler secret put GEMINI_API_KEY 40 | ``` 41 | 42 | ### 2. wrangler.toml 中的环境变量 43 | 44 | 对于非敏感配置,使用 `wrangler.toml` 中的 `[vars]` 部分: 45 | 46 | ```toml 47 | [vars] 48 | GEMINI_MODEL = "gemini-2.5-flash" 49 | ``` 50 | 51 | ### 3. 环境特定配置 52 | 53 | 你可以为不同环境设置不同的值: 54 | 55 | ```toml 56 | # 默认/开发环境 57 | [vars] 58 | GEMINI_MODEL = "gemini-2.5-flash" 59 | 60 | # 生产环境 61 | [env.production.vars] 62 | GEMINI_MODEL = "gemini-2.5-pro" 63 | ``` 64 | 65 | ## Claude Code 集成的环境变量 66 | 67 | 当使用 claude-gemini-router 与 Claude Code 集成时,请在你的 shell 中配置这些环境变量: 68 | 69 | ### ANTHROPIC_BASE_URL 70 | - **描述**: 指向你的 claude-gemini-router 部署的基础 URL 71 | - **必需**: 是(用于 Claude Code 集成) 72 | - **类型**: 字符串 73 | - **示例**: `https://cgr.jimmysong.io` 或 `https://your-worker.your-subdomain.workers.dev` 74 | - **配置方式**: 在你的 shell 配置文件中设置(`~/.bashrc` 或 `~/.zshrc`) 75 | 76 | ### ANTHROPIC_API_KEY 77 | - **描述**: 你的 Google Gemini API 密钥(与 GEMINI_API_KEY 相同) 78 | - **必需**: 是(用于 Claude Code 集成) 79 | - **类型**: 字符串 80 | - **示例**: `AIzaSyDxxxxxxxxxxxxxxxxxxxxxxxxxxx` 81 | - **配置方式**: 在你的 shell 配置文件中设置(`~/.bashrc` 或 `~/.zshrc`) 82 | 83 | ### 模型选择 84 | 85 | **重要**: 模型选择由 Worker 端的 `GEMINI_MODEL` 环境变量控制(在 `wrangler.toml` 中配置)。本地 shell 环境变量如 `ANTHROPIC_MODEL` 不会影响使用哪个模型 - 它们会被 claude-gemini-router 忽略。 86 | 87 | 要更改路由器使用的模型,请更新 Worker 配置中的 `GEMINI_MODEL` 变量(参见上面的"可选变量"部分)。 88 | 89 | ## 配置示例 90 | 91 | ### 基本 Shell 配置 92 | 93 | 添加到你的 `~/.bashrc` 或 `~/.zshrc`: 94 | 95 | ```bash 96 | # 基本配置 97 | export ANTHROPIC_BASE_URL="https://cgr.jimmysong.io" 98 | export ANTHROPIC_API_KEY="$GEMINI_API_KEY" 99 | 100 | # 注意:模型选择由 Worker 配置中的 GEMINI_MODEL 控制, 101 | # 而不是本地 shell 环境变量 102 | ``` 103 | 104 | ### 多环境别名 105 | 106 | ```bash 107 | # 通过 claude-gemini-router 使用 Gemini 的别名 108 | # 注意:模型选择由 Worker 配置中的 GEMINI_MODEL 控制 109 | alias claude-gemini='ANTHROPIC_BASE_URL="https://cgr.jimmysong.io" ANTHROPIC_API_KEY="your-gemini-key" claude' 110 | 111 | # 官方 Anthropic API 的别名 112 | alias claude-official='ANTHROPIC_BASE_URL="https://api.anthropic.com" ANTHROPIC_API_KEY="your-anthropic-key" claude' 113 | 114 | # 使用不同 Worker 部署的不同模型 115 | alias claude-fast='ANTHROPIC_BASE_URL="https://claude-gemini-fast.your-subdomain.workers.dev" ANTHROPIC_API_KEY="your-gemini-key" claude' 116 | alias claude-pro='ANTHROPIC_BASE_URL="https://claude-gemini-pro.your-subdomain.workers.dev" ANTHROPIC_API_KEY="your-gemini-key" claude' 117 | ``` 118 | 119 | ### 为不同模型部署多个 Worker 120 | 121 | 要有效使用不同的 Gemini 模型,请部署不同的 Worker 而不是依赖 shell 变量: 122 | 123 | **方法 1:命名部署** 124 | 125 | ```bash 126 | # 为不同模型部署不同的 Worker 127 | wrangler deploy --name claude-gemini-fast 128 | wrangler deploy --name claude-gemini-pro 129 | 130 | # 为每个部署设置 API 密钥 131 | wrangler secret put GEMINI_API_KEY --name claude-gemini-fast 132 | wrangler secret put GEMINI_API_KEY --name claude-gemini-pro 133 | ``` 134 | 135 | **方法 2:基于环境的部署** 136 | 137 | ```bash 138 | # 部署到不同环境 139 | wrangler deploy --env fast 140 | wrangler deploy --env pro 141 | ``` 142 | 143 | 使用 `wrangler.toml` 配置: 144 | 145 | ```toml 146 | [env.fast.vars] 147 | GEMINI_MODEL = "gemini-2.5-flash" 148 | 149 | [env.pro.vars] 150 | GEMINI_MODEL = "gemini-2.5-pro" 151 | ``` 152 | 153 | ### GitHub Actions 配置 154 | 155 | 在你的 GitHub Actions 工作流中: 156 | 157 | ```yaml 158 | env: 159 | ANTHROPIC_BASE_URL: ${{ secrets.ANTHROPIC_BASE_URL }} 160 | ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} 161 | ``` 162 | 163 | 在你的仓库中设置这些机密: 164 | - `ANTHROPIC_BASE_URL`: 你的 claude-gemini-router 部署 URL 165 | - `ANTHROPIC_API_KEY`: 你的 Google Gemini API 密钥 166 | 167 | ## 安全最佳实践 168 | 169 | 1. **永远不要将 API 密钥提交到版本控制中** 170 | 2. **在 Workers 中使用 Wrangler 机密存储敏感数据** 171 | 3. **本地开发使用环境变量或 shell 配置** 172 | 4. **GitHub Actions 使用仓库机密** 173 | 5. **定期轮换你的 API 密钥** 174 | 6. **检查 API 密钥权限和使用限制** 175 | 176 | ## 故障排除 177 | 178 | ### 常见问题 179 | 180 | 1. **API 密钥不工作**: 确保你的 Google Gemini API 密钥有效且具有必要的权限 181 | 2. **找不到模型**: 检查模型名称是否正确拼写且受支持 182 | 3. **Worker 无法启动**: 验证所有必需的环境变量是否已设置 183 | 4. **Claude Code 无法连接**: 确保 ANTHROPIC_BASE_URL 指向你已部署的 Worker 184 | 185 | ### 调试环境变量 186 | 187 | 要验证你的环境变量是否正确设置: 188 | 189 | ```bash 190 | # 检查 shell 环境 191 | echo $ANTHROPIC_BASE_URL 192 | echo $ANTHROPIC_API_KEY 193 | 194 | # 检查 Worker 环境(在开发中) 195 | wrangler dev --local 196 | ``` 197 | 198 | ## 相关文档 199 | 200 | - [README.md](./README.md) - 主要项目文档 201 | - [wrangler.toml](./wrangler.toml) - Worker 配置文件 202 | - [Google AI Studio](https://makersuite.google.com/app/apikey) - 获取 API 密钥 203 | - [Cloudflare Workers 文档](https://developers.cloudflare.com/workers/) - 平台文档 204 | -------------------------------------------------------------------------------- /README.zh.md: -------------------------------------------------------------------------------- 1 | # claude-gemini-router 2 | 3 | > **注意:** 本文档为中文版本,英文版本请参考 [README.md](README.md) 4 | 5 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 6 | [![TypeScript](https://img.shields.io/badge/TypeScript-007ACC?style=flat&logo=typescript&logoColor=white)](https://www.typescriptlang.org/) 7 | [![Cloudflare Workers](https://img.shields.io/badge/Cloudflare-Workers-orange?style=flat&logo=cloudflare&logoColor=white)](https://workers.cloudflare.com/) 8 | 9 | 一个运行在 Cloudflare Worker 上的 API 转换器,可以在 Anthropic 的 Claude API 和 Google Gemini API 之间进行转换,让您能够在 Claude Code 中使用 Google 的 Gemini 模型。 10 | 11 | ## 快速使用 12 | 13 | ### 手动设置 14 | 15 | **第 1 步:** 安装 Claude Code 16 | 17 | ```bash 18 | npm install -g @anthropic-ai/claude-code 19 | ``` 20 | 21 | **第 2 步:** 从 [Google AI Studio](https://makersuite.google.com/app/apikey) 获取 Google Gemini API 密钥 22 | 23 | **第 3 步:** 在您的 shell 配置文件中配置环境变量(`~/.bashrc` 或 `~/.zshrc`): 24 | 25 | ```bash 26 | # 对于快速测试,您可以使用我们的共享实例。日常使用建议部署自己的实例以获得更好的可靠性。 27 | export ANTHROPIC_BASE_URL="https://cgr.jimmysong.io" 28 | export ANTHROPIC_API_KEY="$GEMINI_API_KEY" 29 | ``` 30 | 31 | **第 4 步:** 重新加载您的 shell 并运行 Claude Code: 32 | 33 | ```bash 34 | source ~/.bashrc 35 | claude 36 | ``` 37 | 38 | 就这样!Claude Code 现在将通过 claude-gemini-router 使用 Google Gemini 模型。 39 | 40 | ### 多种配置 41 | 42 | 要为不同的提供商维护多个 Claude Code 配置,请使用 shell 别名: 43 | 44 | ```bash 45 | # 不同配置的示例别名 46 | alias claude-gemini='ANTHROPIC_BASE_URL="https://cgr.jimmysong.io" ANTHROPIC_API_KEY="your-gemini-key" claude' 47 | alias claude-official='ANTHROPIC_BASE_URL="https://api.anthropic.com" ANTHROPIC_API_KEY="your-anthropic-key" claude' 48 | ``` 49 | 50 | 将这些别名添加到您的 shell 配置文件(`~/.bashrc` 或 `~/.zshrc`)中,然后使用 `claude-gemini` 或 `claude-official` 在配置之间切换。 51 | 52 | #### 使用不同的 Worker 部署来支持不同模型 53 | 54 | 要使用不同的 Gemini 模型,请部署不同的 Worker 而不是依赖 shell 变量: 55 | 56 | **选项 1:多个 Worker 部署** 57 | 58 | ```bash 59 | # 为快速模型部署 worker 60 | wrangler deploy --name claude-gemini-fast 61 | wrangler secret put GEMINI_API_KEY --name claude-gemini-fast 62 | 63 | # 为专业模型部署 worker 64 | wrangler deploy --name claude-gemini-pro 65 | wrangler secret put GEMINI_API_KEY --name claude-gemini-pro 66 | ``` 67 | 68 | 然后配置不同的 `wrangler.toml` 文件或使用特定环境配置: 69 | 70 | ```toml 71 | # 对于快速 worker 72 | [vars] 73 | GEMINI_MODEL = "gemini-2.5-flash" 74 | 75 | # 对于专业 worker 76 | [vars] 77 | GEMINI_MODEL = "gemini-2.5-pro" 78 | ``` 79 | 80 | **选项 2:基于环境的部署** 81 | 82 | ```bash 83 | # 部署到不同环境 84 | wrangler deploy --env fast 85 | wrangler deploy --env pro 86 | ``` 87 | 88 | 使用相应的 `wrangler.toml` 配置: 89 | 90 | ```toml 91 | [env.fast.vars] 92 | GEMINI_MODEL = "gemini-2.5-flash" 93 | 94 | [env.pro.vars] 95 | GEMINI_MODEL = "gemini-2.5-pro" 96 | ``` 97 | 98 | 然后使用别名指向不同的部署: 99 | 100 | ```bash 101 | # 不同模型部署的别名 102 | alias claude-fast='ANTHROPIC_BASE_URL="https://claude-gemini-fast.your-subdomain.workers.dev" ANTHROPIC_API_KEY="your-gemini-key" claude' 103 | alias claude-pro='ANTHROPIC_BASE_URL="https://claude-gemini-pro.your-subdomain.workers.dev" ANTHROPIC_API_KEY="your-gemini-key" claude' 104 | ``` 105 | 106 | ## GitHub Actions 使用 107 | 108 | 要在 GitHub Actions 工作流中使用 Claude Code,请将环境变量添加到您的工作流中: 109 | 110 | ```yaml 111 | env: 112 | ANTHROPIC_BASE_URL: ${{ secrets.ANTHROPIC_BASE_URL }} 113 | ``` 114 | 115 | 在您的仓库密钥中将 `ANTHROPIC_BASE_URL` 设置为 `https://cgr.jimmysong.io`。 116 | 117 | 示例工作流可用于 GitHub Actions 集成。 118 | 119 | ## 功能说明 120 | 121 | claude-gemini-router 作为一个转换层,具有以下功能: 122 | 123 | - 接受 Anthropic API 格式的请求(`/v1/messages`) 124 | - 将其转换为 Google Gemini API 格式 125 | - 转发到 Google Gemini API 126 | - 将响应转换回 Anthropic 格式 127 | - 支持流式和非流式响应 128 | 129 | ## 完美适配 Claude Code + Google Gemini 130 | 131 | 这使您能够将 [Claude Code](https://claude.ai/code) 与 Google 的 Gemini 模型一起使用,通过: 132 | 133 | 1. 将 Claude Code 指向您的 claude-gemini-router 部署 134 | 2. 使用您的 Google Gemini API 密钥 135 | 3. 通过 Claude Code 的界面访问 Gemini 模型 136 | 137 | ## 设置 138 | 139 | 1. **克隆并部署:** 140 | 141 | ```bash 142 | git clone https://github.com/rootsongjc/claude-gemini-router 143 | cd claude-gemini-router 144 | npm install 145 | npm install -g wrangler 146 | wrangler deploy 147 | ``` 148 | 149 | 2. **设置环境变量:** 150 | 151 | ```bash 152 | # 必需:Google Gemini API 密钥 153 | wrangler secret put GEMINI_API_KEY 154 | ``` 155 | 156 | 3. **配置 Claude Code:** 157 | - 将 API 端点设置为您部署的 Worker URL 158 | - 使用您的 Google Gemini API 密钥 159 | - 享受通过 Claude Code 访问 Gemini 模型的体验! 160 | 161 | ## 环境变量 162 | 163 | - `GEMINI_API_KEY`(必需):您的 Google Gemini API 密钥 164 | - `GEMINI_MODEL`(可选):要使用的默认模型。默认为 `gemini-2.5-flash` 165 | 166 | ## 支持的模型 167 | 168 | | 模型名称 | 描述 | 169 | |----------|------| 170 | | `gemini-2.5-flash` | 最新快速模型(默认) | 171 | | `gemini-2.5-pro` | 高性能模型 | 172 | | `gemini-1.5-flash` | 快速模型 | 173 | 174 | ## 模型选择 175 | 176 | **claude-gemini-router 支持灵活的模型选择,优先级顺序如下:** 177 | 178 | 1. **请求模型参数**(最高优先级):API 请求中的 `model` 字段具有最高优先级。您可以直接指定任何有效的 Gemini 模型名称: 179 | 180 | ```bash 181 | curl -X POST https://cgr.jimmysong.io/v1/messages \ 182 | -H "Content-Type: application/json" \ 183 | -H "x-api-key: $GEMINI_API_KEY" \ 184 | -d '{ 185 | "model": "gemini-1.5-flash", 186 | "messages": [{"role": "user", "content": "你好!"}] 187 | }' 188 | ``` 189 | 190 | 2. **环境变量后备**:如果请求中没有指定模型,则使用 `GEMINI_MODEL` 环境变量作为后备默认值。 191 | 192 | 3. **系统默认**:如果两者都没有指定,则默认使用 `gemini-2.5-flash`。 193 | 194 | ### 模型名称处理 195 | 196 | 路由器智能处理 Anthropic 和 Gemini 模型名称: 197 | 198 | - **直接 Gemini 模型**:以 `gemini-` 开头的名称将按原样使用(例如 `gemini-1.5-flash`、`gemini-2.5-pro`) 199 | - **Anthropic 模型映射**:Anthropic 模型名称自动映射: 200 | - `haiku` → `gemini-2.5-flash` 201 | - `sonnet` → `gemini-2.5-pro` 202 | - `opus` → `gemini-2.5-pro` 203 | - **未知模型**:默认使用 `gemini-2.5-flash` 204 | 205 | ### 响应模型字段 206 | 207 | API 响应现在正确包含 `model` 字段,显示实际使用的模型: 208 | 209 | ```json 210 | { 211 | "id": "msg_1234567890", 212 | "type": "message", 213 | "role": "assistant", 214 | "content": [{"type": "text", "text": "你好!"}], 215 | "model": "gemini-1.5-flash", 216 | "usage": {"input_tokens": 4, "output_tokens": 8} 217 | } 218 | ``` 219 | 220 | ## API 使用 221 | 222 | 使用 Anthropic 格式向 `/v1/messages` 发送请求: 223 | 224 | ```bash 225 | curl -X POST https://cgr.jimmysong.io/v1/messages \ 226 | -H "Content-Type: application/json" \ 227 | -H "x-api-key: $GEMINI_API_KEY" \ 228 | -d '{ 229 | "model": "gemini-2.5-flash", 230 | "messages": [{"role": "user", "content": "Hello, Claude"}], 231 | "max_tokens": 100 232 | }' 233 | ``` 234 | 235 | ## 开发 236 | 237 | ```bash 238 | npm run dev # 启动开发服务器 239 | npm run deploy # 部署到 Cloudflare Workers 240 | ``` 241 | 242 | ## 测试 243 | 244 | ```bash 245 | npm run test # 运行单元测试 246 | npm run test:integration # 运行集成测试 247 | npm run test:all # 运行所有测试 248 | npm run test:watch # 在监视模式下运行测试 249 | ``` 250 | 251 | ## 配置 252 | 253 | 项目使用 `wrangler.toml` 进行配置: 254 | 255 | ```toml 256 | name = "claude-gemini-router" 257 | main = "src/index.ts" 258 | compatibility_date = "2023-12-01" 259 | 260 | [vars] 261 | GEMINI_MODEL = "gemini-2.5-flash" 262 | ``` 263 | 264 | ## 致谢 265 | 266 | 特别感谢以下项目对 claude-gemini-router 的启发: 267 | 268 | - [y-router](https://github.com/luohy15/y-router) 269 | - [claude-code-router](https://github.com/musistudio/claude-code-router) 270 | - [claude-code-proxy](https://github.com/kiyo-e/claude-code-proxy) 271 | 272 | ## 免责声明 273 | 274 | **重要法律声明:** 275 | 276 | - **第三方工具**:claude-gemini-router 是一个独立的、非官方工具,未得到 Anthropic PBC 或 Google LLC 的关联、认可或支持 277 | - **服务条款**:用户有责任确保遵守所有相关方(Anthropic、Google 和任何其他 API 提供商)的服务条款 278 | - **API 密钥责任**:用户必须使用自己的有效 API 密钥,并对与这些密钥相关的任何使用、成本或违规行为承担全部责任 279 | - **无担保**:本软件按"原样"提供,不提供任何担保。作者不对因使用本软件而产生的任何损害、服务中断或法律问题负责 280 | - **数据隐私**:虽然 claude-gemini-router 不会有意存储用户数据,但用户应查看所有连接服务的隐私政策 281 | - **合规性**:用户有责任确保其使用符合其管辖区域内的适用法律法规 282 | - **商业使用**:任何商业使用都应根据相关服务条款和许可要求进行仔细评估 283 | 284 | **使用风险自负。** 285 | 286 | ## 许可证 287 | 288 | Apache 2.0 289 | -------------------------------------------------------------------------------- /ENVIRONMENT_VARIABLES.md: -------------------------------------------------------------------------------- 1 | # Environment Variables 2 | 3 | [English](./ENVIRONMENT_VARIABLES.md) | [中文](./ENVIRONMENT_VARIABLES.zh.md) 4 | 5 | This document describes the environment variables used by claude-gemini-router. 6 | 7 | ## Required Variables 8 | 9 | ### GEMINI_API_KEY 10 | - **Description**: Your Google Gemini API key 11 | - **Required**: Yes 12 | - **Type**: String (Secret) 13 | - **Example**: `AIzaSyDxxxxxxxxxxxxxxxxxxxxxxxxxxx` 14 | - **How to obtain**: Get from [Google AI Studio](https://makersuite.google.com/app/apikey) 15 | - **Configuration**: Set via `wrangler secret put GEMINI_API_KEY` 16 | 17 | ## Optional Variables 18 | 19 | ### GEMINI_MODEL (Model Selection Fallback) 20 | - **Description**: Default model to use when no model is specified in requests. Acts as a fallback. 21 | - **Required**: No 22 | - **Type**: String 23 | - **Default**: `gemini-2.5-flash` 24 | - **Supported values**: 25 | - `gemini-2.5-flash` - Latest fast model (recommended) 26 | - `gemini-2.5-pro` - High-performance model 27 | - `gemini-1.5-flash` - Fast model 28 | - **Configuration**: Set in `wrangler.toml` under `[vars]` section 29 | - **Notes**: Request `model` parameter takes precedence. This configuration serves as a fallback. 30 | - **Important**: This worker-side configuration controls model selection along with request parameters, not local shell environment variables. 31 | 32 | ## Configuration Methods 33 | 34 | ### 1. Cloudflare Workers Secrets (Recommended for API Keys) 35 | 36 | For sensitive data like API keys, use Wrangler secrets: 37 | 38 | ```bash 39 | # Set the API key securely 40 | wrangler secret put GEMINI_API_KEY 41 | ``` 42 | 43 | ### 2. Environment Variables in wrangler.toml 44 | 45 | For non-sensitive configuration, use the `[vars]` section in `wrangler.toml`: 46 | 47 | ```toml 48 | [vars] 49 | GEMINI_MODEL = "gemini-2.5-flash" 50 | ``` 51 | 52 | ### 3. Environment-specific Configuration 53 | 54 | You can set different values for different environments: 55 | 56 | ```toml 57 | # Default/development environment 58 | [vars] 59 | GEMINI_MODEL = "gemini-2.5-flash" 60 | 61 | # Production environment 62 | [env.production.vars] 63 | GEMINI_MODEL = "gemini-2.5-pro" 64 | ``` 65 | 66 | ## Environment Variables for Claude Code Integration 67 | 68 | When using claude-gemini-router with Claude Code, configure these environment variables in your shell: 69 | 70 | ### ANTHROPIC_BASE_URL 71 | - **Description**: Base URL pointing to your claude-gemini-router deployment 72 | - **Required**: Yes (for Claude Code integration) 73 | - **Type**: String 74 | - **Example**: `https://cgr.jimmysong.io` or `https://your-worker.your-subdomain.workers.dev` 75 | - **Configuration**: Set in your shell config (`~/.bashrc` or `~/.zshrc`) 76 | 77 | ### ANTHROPIC_API_KEY 78 | - **Description**: Your Google Gemini API key (same as GEMINI_API_KEY) 79 | - **Required**: Yes (for Claude Code integration) 80 | - **Type**: String 81 | - **Example**: `AIzaSyDxxxxxxxxxxxxxxxxxxxxxxxxxxx` 82 | - **Configuration**: Set in your shell config (`~/.bashrc` or `~/.zshrc`) 83 | 84 | ### Model Selection Priority 85 | 86 | **The router uses the following priority order for model selection:** 87 | 88 | 1. **Request Model Parameter** (Highest Priority): The `model` field in API requests 89 | 2. **Environment Variable Fallback**: The `GEMINI_MODEL` worker environment variable (configured in `wrangler.toml`) 90 | 3. **System Default**: `gemini-2.5-flash` if neither is specified 91 | 92 | **Important**: Local shell environment variables like `ANTHROPIC_MODEL` do not affect which model is used - they are ignored by the claude-gemini-router. The router prioritizes the `model` field in requests over worker environment variables. 93 | 94 | To use a specific model, either: 95 | - Specify it in the request: `{"model": "gemini-1.5-flash"}` 96 | - Set the `GEMINI_MODEL` variable in your worker configuration as a fallback default 97 | 98 | ## Example Configurations 99 | 100 | ### Basic Shell Configuration 101 | 102 | Add to your `~/.bashrc` or `~/.zshrc`: 103 | 104 | ```bash 105 | # Basic configuration 106 | export ANTHROPIC_BASE_URL="https://cgr.jimmysong.io" 107 | export ANTHROPIC_API_KEY="$GEMINI_API_KEY" 108 | 109 | # Note: Model selection is controlled by GEMINI_MODEL in your worker configuration, 110 | # not by local shell environment variables 111 | ``` 112 | 113 | ### Multiple Environment Aliases 114 | 115 | ```bash 116 | # Alias for Gemini via claude-gemini-router 117 | # Note: Model selection is controlled by GEMINI_MODEL in your worker configuration 118 | alias claude-gemini='ANTHROPIC_BASE_URL="https://cgr.jimmysong.io" ANTHROPIC_API_KEY="your-gemini-key" claude' 119 | 120 | # Alias for official Anthropic API 121 | alias claude-official='ANTHROPIC_BASE_URL="https://api.anthropic.com" ANTHROPIC_API_KEY="your-anthropic-key" claude' 122 | 123 | # For different models using separate worker deployments 124 | alias claude-fast='ANTHROPIC_BASE_URL="https://claude-gemini-fast.your-subdomain.workers.dev" ANTHROPIC_API_KEY="your-gemini-key" claude' 125 | alias claude-pro='ANTHROPIC_BASE_URL="https://claude-gemini-pro.your-subdomain.workers.dev" ANTHROPIC_API_KEY="your-gemini-key" claude' 126 | ``` 127 | 128 | ### Deploying Multiple Workers for Different Models 129 | 130 | To use different Gemini models effectively, deploy separate Workers instead of relying on shell variables: 131 | 132 | **Method 1: Named Deployments** 133 | 134 | ```bash 135 | # Deploy separate workers for different models 136 | wrangler deploy --name claude-gemini-fast 137 | wrangler deploy --name claude-gemini-pro 138 | 139 | # Set API keys for each deployment 140 | wrangler secret put GEMINI_API_KEY --name claude-gemini-fast 141 | wrangler secret put GEMINI_API_KEY --name claude-gemini-pro 142 | ``` 143 | 144 | **Method 2: Environment-Based Deployments** 145 | 146 | ```bash 147 | # Deploy to different environments 148 | wrangler deploy --env fast 149 | wrangler deploy --env pro 150 | ``` 151 | 152 | With `wrangler.toml` configuration: 153 | 154 | ```toml 155 | [env.fast.vars] 156 | GEMINI_MODEL = "gemini-2.5-flash" 157 | 158 | [env.pro.vars] 159 | GEMINI_MODEL = "gemini-2.5-pro" 160 | ``` 161 | 162 | ### GitHub Actions Configuration 163 | 164 | In your GitHub Actions workflow: 165 | 166 | ```yaml 167 | env: 168 | ANTHROPIC_BASE_URL: ${{ secrets.ANTHROPIC_BASE_URL }} 169 | ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} 170 | ``` 171 | 172 | Set these secrets in your repository: 173 | - `ANTHROPIC_BASE_URL`: Your claude-gemini-router deployment URL 174 | - `ANTHROPIC_API_KEY`: Your Google Gemini API key 175 | 176 | ## Security Best Practices 177 | 178 | 1. **Never commit API keys to version control** 179 | 2. **Use Wrangler secrets for sensitive data in Workers** 180 | 3. **Use environment variables or shell config for local development** 181 | 4. **Use repository secrets for GitHub Actions** 182 | 5. **Regularly rotate your API keys** 183 | 6. **Review API key permissions and usage limits** 184 | 185 | ## Troubleshooting 186 | 187 | ### Common Issues 188 | 189 | 1. **API Key not working**: Ensure your Google Gemini API key is valid and has the necessary permissions 190 | 2. **Model not found**: Check that the model name is correctly spelled and supported 191 | 3. **Worker not starting**: Verify that all required environment variables are set 192 | 4. **Claude Code not connecting**: Ensure ANTHROPIC_BASE_URL points to your deployed Worker 193 | 194 | ### Debug Environment Variables 195 | 196 | To verify your environment variables are set correctly: 197 | 198 | ```bash 199 | # Check shell environment 200 | echo $ANTHROPIC_BASE_URL 201 | echo $ANTHROPIC_API_KEY 202 | 203 | # Check Worker environment (in development) 204 | wrangler dev --local 205 | ``` 206 | 207 | ## Related Documentation 208 | 209 | - [README.md](./README.md) - Main project documentation 210 | - [wrangler.toml](./wrangler.toml) - Worker configuration file 211 | - [Google AI Studio](https://makersuite.google.com/app/apikey) - Get API key 212 | - [Cloudflare Workers Docs](https://developers.cloudflare.com/workers/) - Platform documentation 213 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | formatAnthropicToGemini, 3 | getGeminiEndpoint, 4 | getGeminiHeaders, 5 | } from "./formatRequestGemini"; 6 | import { formatGeminiToAnthropic } from "./formatResponseGemini"; 7 | import { streamGeminiToAnthropic } from "./streamResponseGemini"; 8 | import { logger } from "./logger"; 9 | import { Env } from "./types"; 10 | 11 | export default { 12 | async fetch( 13 | request: Request, 14 | env: Env, 15 | ctx: ExecutionContext 16 | ): Promise { 17 | // Handle CORS preflight requests 18 | if (request.method === "OPTIONS") { 19 | return new Response(null, { 20 | status: 200, 21 | headers: { 22 | "Access-Control-Allow-Origin": "*", 23 | "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", 24 | "Access-Control-Allow-Headers": 25 | "Content-Type, Authorization, x-api-key", 26 | }, 27 | }); 28 | } 29 | 30 | // Only handle POST requests to /v1/messages 31 | const url = new URL(request.url); 32 | 33 | // Serve HTML page on the root path for GET requests 34 | if (request.method === "GET" && url.pathname === "/") { 35 | const htmlContent = ` 36 | 37 | 38 | 39 | 40 | Claude Gemini Router 41 | 42 | 98 | 99 | 100 |
101 |

Welcome to Claude Gemini Router!

102 |

This Cloudflare Worker acts as a translation layer, allowing you to use Anthropic's Claude Code CLI with Google's Gemini models.

103 |

It translates requests from Anthropic API format to Google Gemini API format, forwards them to Google Gemini, and then translates the responses back to Anthropic's format, supporting both streaming and non-streaming.

104 | 105 |

How to Use:

106 |

1. Get your Google Gemini API key from Google AI Studio.

107 |

2. Configure your Claude Code CLI to point to this router and use your Gemini API key:

108 |
export ANTHROPIC_BASE_URL="https://cgr.jimmysong.io"
109 | export ANTHROPIC_API_KEY="your-gemini-api-key"
110 | export ANTHROPIC_MODEL="gemini-2.5-flash" # Optional, specify your preferred Gemini model
111 |

3. Now you can use the Claude Code CLI as usual, powered by Google Gemini!

112 | 113 |

Project Repository:

114 |

Find more details and contribute on GitHub: rootsongjc/claude-gemini-router

115 |
116 | 117 | `; 118 | return new Response(htmlContent, { 119 | headers: { "Content-Type": "text/html" }, 120 | }); 121 | } 122 | 123 | // Only handle POST requests to /v1/messages for API calls 124 | if (request.method !== "POST") { 125 | return new Response("Method Not Allowed", { status: 405 }); 126 | } 127 | 128 | if (url.pathname !== "/v1/messages") { 129 | return new Response("Not Found", { status: 404 }); 130 | } 131 | 132 | try { 133 | // Get API key from headers 134 | const apiKey = 135 | request.headers.get("x-api-key") || 136 | request.headers.get("Authorization")?.replace("Bearer ", ""); 137 | 138 | if (!apiKey) { 139 | return new Response("Missing API key", { status: 401 }); 140 | } 141 | 142 | // Parse request body 143 | const body = await request.json(); 144 | 145 | // Log the incoming request 146 | logger.debug("Incoming request:", body); 147 | 148 | // Convert Anthropic request to Gemini format 149 | const { 150 | request: geminiRequest, 151 | isStream, 152 | model, 153 | } = formatAnthropicToGemini(body, env); 154 | 155 | // Get the appropriate Gemini endpoint 156 | const endpoint = getGeminiEndpoint(model, isStream); 157 | 158 | // Log the converted request 159 | logger.debug("Converted to Gemini request:", geminiRequest); 160 | logger.debug("Endpoint:", endpoint); 161 | 162 | // Make request to Gemini API 163 | const geminiResponse = await fetch(endpoint, { 164 | method: "POST", 165 | headers: getGeminiHeaders(apiKey), 166 | body: JSON.stringify(geminiRequest), 167 | }); 168 | 169 | if (!geminiResponse.ok) { 170 | const errorText = await geminiResponse.text(); 171 | logger.error("Gemini API error:", errorText); 172 | 173 | // Try to parse error response 174 | let errorData; 175 | try { 176 | errorData = JSON.parse(errorText); 177 | } catch { 178 | errorData = { error: { message: errorText } }; 179 | } 180 | 181 | return new Response( 182 | JSON.stringify({ 183 | error: { 184 | type: "api_error", 185 | message: 186 | errorData.error?.message || "Unknown error from Gemini API", 187 | }, 188 | }), 189 | { 190 | status: geminiResponse.status, 191 | headers: { 192 | "Content-Type": "application/json", 193 | "Access-Control-Allow-Origin": "*", 194 | }, 195 | } 196 | ); 197 | } 198 | 199 | // Handle streaming response 200 | if (isStream) { 201 | const stream = streamGeminiToAnthropic(geminiResponse.body!, model); 202 | return new Response(stream, { 203 | headers: { 204 | "Content-Type": "text/event-stream", 205 | "Cache-Control": "no-cache", 206 | Connection: "keep-alive", 207 | "Access-Control-Allow-Origin": "*", 208 | }, 209 | }); 210 | } 211 | 212 | // Handle non-streaming response 213 | const geminiData = await geminiResponse.json(); 214 | logger.debug("Gemini response:", geminiData); 215 | 216 | const anthropicResponse = formatGeminiToAnthropic(geminiData, model); 217 | logger.debug("Converted to Anthropic response:", anthropicResponse); 218 | 219 | return new Response(JSON.stringify(anthropicResponse), { 220 | headers: { 221 | "Content-Type": "application/json", 222 | "Access-Control-Allow-Origin": "*", 223 | }, 224 | }); 225 | } catch (error) { 226 | logger.error("Error processing request:", error); 227 | 228 | return new Response( 229 | JSON.stringify({ 230 | error: { 231 | type: "internal_error", 232 | message: error instanceof Error ? error.message : "Unknown error", 233 | }, 234 | }), 235 | { 236 | status: 500, 237 | headers: { 238 | "Content-Type": "application/json", 239 | "Access-Control-Allow-Origin": "*", 240 | }, 241 | } 242 | ); 243 | } 244 | }, 245 | }; 246 | -------------------------------------------------------------------------------- /src/streamResponseGemini.ts: -------------------------------------------------------------------------------- 1 | export function streamGeminiToAnthropic(geminiStream: ReadableStream, model: string): ReadableStream { 2 | const messageId = "msg_" + Date.now(); 3 | 4 | const enqueueSSE = (controller: ReadableStreamDefaultController, eventType: string, data: any) => { 5 | const sseMessage = `event: ${eventType}\ndata: ${JSON.stringify(data)}\n\n`; 6 | controller.enqueue(new TextEncoder().encode(sseMessage)); 7 | }; 8 | 9 | return new ReadableStream({ 10 | async start(controller) { 11 | // Send message_start event 12 | const messageStart = { 13 | type: "message_start", 14 | message: { 15 | id: messageId, 16 | type: "message", 17 | role: "assistant", 18 | content: [], 19 | model, 20 | stop_reason: null, 21 | stop_sequence: null, 22 | usage: { input_tokens: 1, output_tokens: 1 }, 23 | }, 24 | }; 25 | enqueueSSE(controller, "message_start", messageStart); 26 | 27 | let contentBlockIndex = 0; 28 | let hasStartedTextBlock = false; 29 | let isToolUse = false; 30 | let currentToolCallId: string | null = null; 31 | let toolCallJsonMap = new Map(); 32 | 33 | const reader = geminiStream.getReader(); 34 | const decoder = new TextDecoder(); 35 | let buffer = ''; 36 | 37 | try { 38 | while (true) { 39 | const { done, value } = await reader.read(); 40 | if (done) { 41 | // Process final buffer 42 | if (buffer.trim()) { 43 | try { 44 | const parsed = JSON.parse(buffer.trim()); 45 | if (Array.isArray(parsed) && parsed.length > 0) { 46 | const item = parsed[0]; 47 | if (item.candidates && item.candidates.length > 0) { 48 | processGeminiDelta(item); 49 | } 50 | } 51 | } catch (e) { 52 | // Ignore parse errors 53 | } 54 | } 55 | break; 56 | } 57 | 58 | // Decode chunk and add to buffer 59 | const chunk = decoder.decode(value, { stream: true }); 60 | buffer += chunk; 61 | 62 | // Try to parse complete JSON array 63 | try { 64 | const parsed = JSON.parse(buffer); 65 | if (Array.isArray(parsed) && parsed.length > 0) { 66 | const item = parsed[0]; 67 | if (item.candidates && item.candidates.length > 0) { 68 | processGeminiDelta(item); 69 | buffer = ''; // Clear buffer after processing 70 | } 71 | } 72 | } catch (e) { 73 | // Buffer doesn't contain complete JSON yet, continue 74 | } 75 | } 76 | } finally { 77 | reader.releaseLock(); 78 | } 79 | 80 | function processGeminiDelta(geminiChunk: any) { 81 | const candidate = geminiChunk.candidates[0]; 82 | if (!candidate || !candidate.content || !candidate.content.parts) { 83 | return; 84 | } 85 | 86 | // Process each part in the candidate's content 87 | for (const part of candidate.content.parts) { 88 | // Handle function calls (tool use) 89 | if (part.functionCall) { 90 | const toolCallId = `toolu_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; 91 | 92 | // Close previous content block if any 93 | if (isToolUse || hasStartedTextBlock) { 94 | enqueueSSE(controller, "content_block_stop", { 95 | type: "content_block_stop", 96 | index: contentBlockIndex, 97 | }); 98 | } 99 | 100 | isToolUse = true; 101 | hasStartedTextBlock = false; 102 | currentToolCallId = toolCallId; 103 | contentBlockIndex++; 104 | 105 | // Start new tool use block 106 | const toolBlock = { 107 | type: "tool_use", 108 | id: toolCallId, 109 | name: part.functionCall.name, 110 | input: {}, 111 | }; 112 | 113 | enqueueSSE(controller, "content_block_start", { 114 | type: "content_block_start", 115 | index: contentBlockIndex, 116 | content_block: toolBlock, 117 | }); 118 | 119 | // Send function call arguments as input_json_delta 120 | if (part.functionCall.args) { 121 | const argsJson = JSON.stringify(part.functionCall.args); 122 | toolCallJsonMap.set(toolCallId, argsJson); 123 | 124 | enqueueSSE(controller, "content_block_delta", { 125 | type: "content_block_delta", 126 | index: contentBlockIndex, 127 | delta: { 128 | type: "input_json_delta", 129 | partial_json: argsJson, 130 | }, 131 | }); 132 | } 133 | } 134 | // Handle text content 135 | else if (part.text) { 136 | // Close tool use block if switching from tool to text 137 | if (isToolUse) { 138 | enqueueSSE(controller, "content_block_stop", { 139 | type: "content_block_stop", 140 | index: contentBlockIndex, 141 | }); 142 | isToolUse = false; 143 | currentToolCallId = null; 144 | contentBlockIndex++; 145 | } 146 | 147 | // Start text block if not already started 148 | if (!hasStartedTextBlock) { 149 | enqueueSSE(controller, "content_block_start", { 150 | type: "content_block_start", 151 | index: contentBlockIndex, 152 | content_block: { 153 | type: "text", 154 | text: "", 155 | }, 156 | }); 157 | hasStartedTextBlock = true; 158 | } 159 | 160 | // Send text delta 161 | enqueueSSE(controller, "content_block_delta", { 162 | type: "content_block_delta", 163 | index: contentBlockIndex, 164 | delta: { 165 | type: "text_delta", 166 | text: part.text, 167 | }, 168 | }); 169 | } 170 | } 171 | 172 | // Handle finish reason from Gemini 173 | if (candidate.finishReason) { 174 | // Close current content block 175 | if (isToolUse || hasStartedTextBlock) { 176 | enqueueSSE(controller, "content_block_stop", { 177 | type: "content_block_stop", 178 | index: contentBlockIndex, 179 | }); 180 | } 181 | 182 | // Map Gemini finish reasons to Anthropic stop reasons 183 | let stopReason = "end_turn"; 184 | switch (candidate.finishReason) { 185 | case "STOP": 186 | stopReason = isToolUse ? "tool_use" : "end_turn"; 187 | break; 188 | case "MAX_TOKENS": 189 | stopReason = "max_tokens"; 190 | break; 191 | case "SAFETY": 192 | case "RECITATION": 193 | stopReason = "stop_sequence"; 194 | break; 195 | case "OTHER": 196 | default: 197 | stopReason = isToolUse ? "tool_use" : "end_turn"; 198 | } 199 | 200 | // Send message_delta and message_stop 201 | enqueueSSE(controller, "message_delta", { 202 | type: "message_delta", 203 | delta: { 204 | stop_reason: stopReason, 205 | stop_sequence: null, 206 | }, 207 | usage: { 208 | input_tokens: geminiChunk.usageMetadata?.promptTokenCount || 100, 209 | output_tokens: geminiChunk.usageMetadata?.candidatesTokenCount || 150 210 | }, 211 | }); 212 | 213 | enqueueSSE(controller, "message_stop", { 214 | type: "message_stop", 215 | }); 216 | 217 | controller.close(); 218 | return; 219 | } 220 | } 221 | 222 | // If we reach here without a finish reason, still close properly 223 | if (isToolUse || hasStartedTextBlock) { 224 | enqueueSSE(controller, "content_block_stop", { 225 | type: "content_block_stop", 226 | index: contentBlockIndex, 227 | }); 228 | } 229 | 230 | // Send final message_delta and message_stop 231 | enqueueSSE(controller, "message_delta", { 232 | type: "message_delta", 233 | delta: { 234 | stop_reason: isToolUse ? "tool_use" : "end_turn", 235 | stop_sequence: null, 236 | }, 237 | usage: { input_tokens: 100, output_tokens: 150 }, 238 | }); 239 | 240 | enqueueSSE(controller, "message_stop", { 241 | type: "message_stop", 242 | }); 243 | 244 | controller.close(); 245 | }, 246 | }); 247 | } 248 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # claude-gemini-router 2 | 3 | [简体中文](README.zh.md) 4 | 5 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 6 | [![TypeScript](https://img.shields.io/badge/TypeScript-007ACC?style=flat&logo=typescript&logoColor=white)](https://www.typescriptlang.org/) 7 | [![Cloudflare Workers](https://img.shields.io/badge/Cloudflare-Workers-orange?style=flat&logo=cloudflare&logoColor=white)](https://workers.cloudflare.com/) 8 | 9 | A Cloudflare Worker that translates between Anthropic's Claude API and Google Gemini API, enabling you to use Claude Code with Google's Gemini models. 10 | 11 | ## Quick Usage 12 | 13 | ### Manual Setup 14 | 15 | **Step 1:** Install Claude Code 16 | 17 | ```bash 18 | npm install -g @anthropic-ai/claude-code 19 | ``` 20 | 21 | **Step 2:** Get Google Gemini API key from [Google AI Studio](https://makersuite.google.com/app/apikey) 22 | 23 | **Step 3:** Configure environment variables in your shell config (`~/.bashrc` or `~/.zshrc`): 24 | 25 | ```bash 26 | # For quick testing, you can use our shared instance. For daily use, deploy your own instance for better reliability. 27 | export ANTHROPIC_BASE_URL="https://cgr.jimmysong.io" 28 | export ANTHROPIC_API_KEY="$GEMINI_API_KEY" 29 | ``` 30 | 31 | **Step 4:** Reload your shell and run Claude Code: 32 | 33 | ```bash 34 | source ~/.bashrc 35 | claude 36 | ``` 37 | 38 | That's it! Claude Code will now use Google Gemini models through claude-gemini-router. 39 | 40 | ### Multiple Configurations 41 | 42 | To maintain multiple Claude Code configurations for different providers, use shell aliases: 43 | 44 | ```bash 45 | # Example aliases for different configurations 46 | alias claude-gemini='ANTHROPIC_BASE_URL="https://cgr.jimmysong.io" ANTHROPIC_API_KEY="your-gemini-key" claude' 47 | alias claude-official='ANTHROPIC_BASE_URL="https://api.anthropic.com" ANTHROPIC_API_KEY="your-anthropic-key" claude' 48 | ``` 49 | 50 | Add these aliases to your shell config file (`~/.bashrc` or `~/.zshrc`), then use `claude-gemini` or `claude-official` to switch between configurations. 51 | 52 | #### Using Different Models with Separate Workers 53 | 54 | For advanced users who want dedicated Workers for different models, see [MULTI_WORKER_DEPLOYMENT.md](./MULTI_WORKER_DEPLOYMENT.md) for detailed instructions. 55 | 56 | **Quick Example:** 57 | 58 | ```bash 59 | # Deploy separate workers 60 | wrangler deploy --name claude-gemini-flash 61 | wrangler secret put GEMINI_API_KEY --name claude-gemini-flash 62 | wrangler secret put GEMINI_MODEL --name claude-gemini-flash # Enter: gemini-2.5-flash 63 | 64 | wrangler deploy --name claude-gemini-pro 65 | wrangler secret put GEMINI_API_KEY --name claude-gemini-pro 66 | wrangler secret put GEMINI_MODEL --name claude-gemini-pro # Enter: gemini-2.5-pro 67 | ``` 68 | 69 | **Alternative: Single Worker with Request-level Model Selection (Recommended)** 70 | 71 | ```bash 72 | # Use different models with the same Worker 73 | curl -X POST https://cgr.jimmysong.io/v1/messages \ 74 | -H "Content-Type: application/json" \ 75 | -H "x-api-key: $GEMINI_API_KEY" \ 76 | -d '{"model": "gemini-1.5-flash", "messages": [{"role": "user", "content": "Hello"}]}' 77 | ``` 78 | 79 | ## GitHub Actions Usage 80 | 81 | To use Claude Code in GitHub Actions workflows, add the environment variable to your workflow: 82 | 83 | ```yaml 84 | env: 85 | ANTHROPIC_BASE_URL: ${{ secrets.ANTHROPIC_BASE_URL }} 86 | ``` 87 | 88 | Set `ANTHROPIC_BASE_URL` to `https://cgr.jimmysong.io` in your repository secrets. 89 | 90 | Example workflows are available for GitHub Actions integration. 91 | 92 | ## What it does 93 | 94 | claude-gemini-router acts as a translation layer that: 95 | 96 | - Accepts requests in Anthropic's API format (`/v1/messages`) 97 | - Converts them to Google Gemini's API format 98 | - Forwards to Google Gemini API 99 | - Translates the response back to Anthropic's format 100 | - Supports both streaming and non-streaming responses 101 | 102 | ## Perfect for Claude Code + Google Gemini 103 | 104 | This allows you to use [Claude Code](https://claude.ai/code) with Google's Gemini models by: 105 | 106 | 1. Pointing Claude Code to your claude-gemini-router deployment 107 | 2. Using your Google Gemini API key 108 | 3. Accessing Gemini models through Claude Code's interface 109 | 110 | ## Setup 111 | 112 | 1. **Clone and deploy:** 113 | 114 | ```bash 115 | git clone https://github.com/rootsongjc/claude-gemini-router 116 | cd claude-gemini-router 117 | npm install 118 | npm install -g wrangler 119 | wrangler deploy 120 | ``` 121 | 122 | 2. **Set environment variables:** 123 | 124 | ```bash 125 | # Required: Google Gemini API key 126 | wrangler secret put GEMINI_API_KEY 127 | ``` 128 | 129 | 3. **Configure Claude Code:** 130 | - Set API endpoint to your deployed Worker URL 131 | - Use your Google Gemini API key 132 | - Enjoy access to Gemini models via Claude Code! 133 | 134 | ## Environment Variables 135 | 136 | - `GEMINI_API_KEY` (required): Your Google Gemini API key 137 | - `GEMINI_MODEL` (optional): Default model to use. Defaults to `gemini-2.5-flash` 138 | 139 | ## Supported Models 140 | 141 | | Model Name | Description | 142 | |------------|-------------| 143 | | `gemini-2.5-flash` | Latest fast model (default) | 144 | | `gemini-2.5-pro` | High-performance model | 145 | | `gemini-1.5-flash` | Fast model | 146 | 147 | ## Model Selection 148 | 149 | **The claude-gemini-router supports flexible model selection with the following priority order:** 150 | 151 | 1. **Request Model Parameter** (Highest Priority): The `model` field in the API request takes precedence. You can specify any valid Gemini model name directly: 152 | 153 | ```bash 154 | curl -X POST https://cgr.jimmysong.io/v1/messages \ 155 | -H "Content-Type: application/json" \ 156 | -H "x-api-key: $GEMINI_API_KEY" \ 157 | -d '{ 158 | "model": "gemini-1.5-flash", 159 | "messages": [{"role": "user", "content": "Hello!"}] 160 | }' 161 | ``` 162 | 163 | 2. **Environment Variable Fallback**: If no model is specified in the request, the `GEMINI_MODEL` environment variable is used as a fallback default. 164 | 165 | 3. **System Default**: If neither is specified, defaults to `gemini-2.5-flash`. 166 | 167 | ### Model Name Handling 168 | 169 | The router intelligently handles both Anthropic and Gemini model names: 170 | 171 | - **Direct Gemini Models**: Names starting with `gemini-` are used as-is (e.g., `gemini-1.5-flash`, `gemini-2.5-pro`) 172 | - **Anthropic Model Mapping**: Anthropic model names are automatically mapped: 173 | - `haiku` → `gemini-2.5-flash` 174 | - `sonnet` → `gemini-2.5-pro` 175 | - `opus` → `gemini-2.5-pro` 176 | - **Unknown Models**: Default to `gemini-2.5-flash` 177 | 178 | ### Response Model Field 179 | 180 | The API response now correctly includes the `model` field showing which model was actually used: 181 | 182 | ```json 183 | { 184 | "id": "msg_1234567890", 185 | "type": "message", 186 | "role": "assistant", 187 | "content": [{"type": "text", "text": "Hello!"}], 188 | "model": "gemini-1.5-flash", 189 | "usage": {"input_tokens": 4, "output_tokens": 8} 190 | } 191 | ``` 192 | 193 | ### Important: Claude Code Environment Variables 194 | 195 | **These environment variables DO NOT affect Worker model selection:** 196 | 197 | ```bash 198 | # ❌ These will NOT change the model used by the Worker 199 | export ANTHROPIC_MODEL="gemini-1.5-flash" 200 | export ANTHROPIC_SMALL_FAST_MODEL="gemini-1.5-flash" 201 | ``` 202 | 203 | **Why?** The Worker controls model selection server-side, not client-side environment variables. 204 | 205 | **To use different models:** 206 | - Use multiple Workers with different `GEMINI_MODEL` configurations 207 | - Or specify the model in API requests: `{"model": "gemini-1.5-flash"}` 208 | 209 | ## API Usage 210 | 211 | Send requests to `/v1/messages` using Anthropic's format: 212 | 213 | ```bash 214 | curl -X POST https://cgr.jimmysong.io/v1/messages \ 215 | -H "Content-Type: application/json" \ 216 | -H "x-api-key: $GEMINI_API_KEY" \ 217 | -d '{ 218 | "model": "gemini-2.5-flash", 219 | "messages": [{"role": "user", "content": "Hello, Claude"}], 220 | "max_tokens": 100 221 | }' 222 | ``` 223 | 224 | ## Development 225 | 226 | ```bash 227 | npm run dev # Start development server 228 | npm run deploy # Deploy to Cloudflare Workers 229 | ``` 230 | 231 | ## Testing 232 | 233 | ```bash 234 | npm run test # Run unit tests 235 | npm run test:integration # Run integration tests 236 | npm run test:all # Run all tests 237 | npm run test:watch # Run tests in watch mode 238 | ``` 239 | 240 | ## Configuration 241 | 242 | The project uses `wrangler.toml` for configuration: 243 | 244 | ```toml 245 | name = "claude-gemini-router" 246 | main = "src/index.ts" 247 | compatibility_date = "2023-12-01" 248 | 249 | [vars] 250 | GEMINI_MODEL = "gemini-2.5-flash" 251 | ``` 252 | 253 | ## Thanks 254 | 255 | Special thanks to these projects that inspired claude-gemini-router: 256 | 257 | - [y-router](https://github.com/luohy15/y-router) 258 | - [claude-code-router](https://github.com/musistudio/claude-code-router) 259 | - [claude-code-proxy](https://github.com/kiyo-e/claude-code-proxy) 260 | 261 | ## Disclaimer 262 | 263 | **Important Legal Notice:** 264 | 265 | - **Third-party Tool**: claude-gemini-router is an independent, unofficial tool and is not affiliated with, endorsed by, or supported by Anthropic PBC or Google LLC 266 | - **Service Terms**: Users are responsible for ensuring compliance with the Terms of Service of all involved parties (Anthropic, Google, and any other API providers) 267 | - **API Key Responsibility**: Users must use their own valid API keys and are solely responsible for any usage, costs, or violations associated with those keys 268 | - **No Warranty**: This software is provided "as is" without any warranties. The authors are not responsible for any damages, service interruptions, or legal issues arising from its use 269 | - **Data Privacy**: While claude-gemini-router does not intentionally store user data, users should review the privacy policies of all connected services 270 | - **Compliance**: Users are responsible for ensuring their use complies with applicable laws and regulations in their jurisdiction 271 | - **Commercial Use**: Any commercial use should be carefully evaluated against relevant terms of service and licensing requirements 272 | 273 | **Use at your own risk and discretion.** 274 | 275 | ## License 276 | 277 | Apache 2.0 278 | -------------------------------------------------------------------------------- /src/formatRequestGemini.ts: -------------------------------------------------------------------------------- 1 | interface MessageCreateParamsBase { 2 | model: string; 3 | messages: any[]; 4 | system?: any; 5 | temperature?: number; 6 | tools?: any[]; 7 | stream?: boolean; 8 | } 9 | 10 | interface GeminiContent { 11 | role: "user" | "model"; 12 | parts: GeminiPart[]; 13 | } 14 | 15 | interface GeminiPart { 16 | text?: string; 17 | functionCall?: { 18 | name: string; 19 | args: any; 20 | }; 21 | functionResponse?: { 22 | name: string; 23 | response: any; 24 | }; 25 | } 26 | 27 | interface GeminiGenerationConfig { 28 | temperature?: number; 29 | topP?: number; 30 | topK?: number; 31 | candidateCount?: number; 32 | maxOutputTokens?: number; 33 | stopSequences?: string[]; 34 | } 35 | 36 | interface GeminiSafetySetting { 37 | category: string; 38 | threshold: string; 39 | } 40 | 41 | interface GeminiTool { 42 | functionDeclarations: GeminiFunctionDeclaration[]; 43 | } 44 | 45 | interface GeminiFunctionDeclaration { 46 | name: string; 47 | description: string; 48 | parameters: any; 49 | } 50 | 51 | interface GeminiRequest { 52 | contents: GeminiContent[]; 53 | generationConfig?: GeminiGenerationConfig; 54 | safetySettings?: GeminiSafetySetting[]; 55 | tools?: GeminiTool[]; 56 | } 57 | 58 | /** 59 | * Maps Anthropic model names to Gemini model names 60 | */ 61 | export function mapModelToGemini(modelName: string): string { 62 | // If model already contains '/', it might be a full model path 63 | if (modelName.includes('/')) { 64 | return modelName; 65 | } 66 | 67 | // If it's already a Gemini model name, return it as is 68 | if (modelName.startsWith('gemini-')) { 69 | return modelName; 70 | } 71 | 72 | // Map common Anthropic models to Gemini equivalents 73 | if (modelName.includes('haiku')) { 74 | return 'gemini-2.5-flash'; 75 | } else if (modelName.includes('sonnet')) { 76 | return 'gemini-2.5-pro'; 77 | } else if (modelName.includes('opus')) { 78 | return 'gemini-2.5-pro'; 79 | } 80 | 81 | // Default to gemini-2.5-flash for unknown models 82 | return 'gemini-2.5-flash'; 83 | } 84 | 85 | /** 86 | * Converts Anthropic safety settings to Gemini safety settings 87 | */ 88 | function getDefaultSafetySettings(): GeminiSafetySetting[] { 89 | return [ 90 | { 91 | category: "HARM_CATEGORY_HARASSMENT", 92 | threshold: "BLOCK_MEDIUM_AND_ABOVE" 93 | }, 94 | { 95 | category: "HARM_CATEGORY_HATE_SPEECH", 96 | threshold: "BLOCK_MEDIUM_AND_ABOVE" 97 | }, 98 | { 99 | category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", 100 | threshold: "BLOCK_MEDIUM_AND_ABOVE" 101 | }, 102 | { 103 | category: "HARM_CATEGORY_DANGEROUS_CONTENT", 104 | threshold: "BLOCK_MEDIUM_AND_ABOVE" 105 | } 106 | ]; 107 | } 108 | 109 | /** 110 | * Converts Anthropic tools to Gemini function declarations 111 | */ 112 | function convertToolsToGemini(tools: any[]): GeminiTool[] { 113 | if (!tools || tools.length === 0) { 114 | return []; 115 | } 116 | 117 | const functionDeclarations: GeminiFunctionDeclaration[] = tools.map(tool => ({ 118 | name: tool.name, 119 | description: tool.description, 120 | parameters: filterSchema(tool.input_schema) 121 | })); 122 | 123 | function filterSchema(schema: any): any { 124 | if (!schema || typeof schema !== 'object') { 125 | return schema; 126 | } 127 | 128 | // Handle arrays 129 | if (Array.isArray(schema)) { 130 | return schema.map(item => filterSchema(item)); 131 | } 132 | 133 | const filteredSchema = { ...schema }; 134 | 135 | // Remove unsupported JSON Schema fields 136 | delete filteredSchema['$schema']; 137 | delete filteredSchema['additionalProperties']; 138 | 139 | // Filter format field - Gemini only supports 'enum' and 'date-time' for STRING type 140 | if (filteredSchema.format && 141 | filteredSchema.type === 'string' && 142 | filteredSchema.format !== 'enum' && 143 | filteredSchema.format !== 'date-time') { 144 | delete filteredSchema.format; 145 | } 146 | 147 | // Remove other unsupported fields that might cause issues 148 | delete filteredSchema['examples']; 149 | delete filteredSchema['$ref']; 150 | delete filteredSchema['$id']; 151 | delete filteredSchema['$comment']; 152 | delete filteredSchema['default']; 153 | delete filteredSchema['const']; 154 | delete filteredSchema['contentMediaType']; 155 | delete filteredSchema['contentEncoding']; 156 | delete filteredSchema['if']; 157 | delete filteredSchema['then']; 158 | delete filteredSchema['else']; 159 | delete filteredSchema['allOf']; 160 | delete filteredSchema['anyOf']; 161 | delete filteredSchema['oneOf']; 162 | delete filteredSchema['not']; 163 | delete filteredSchema['contains']; 164 | delete filteredSchema['patternProperties']; 165 | delete filteredSchema['dependencies']; 166 | delete filteredSchema['dependentSchemas']; 167 | delete filteredSchema['dependentRequired']; 168 | delete filteredSchema['propertyNames']; 169 | delete filteredSchema['unevaluatedItems']; 170 | delete filteredSchema['unevaluatedProperties']; 171 | 172 | // Recursively filter nested objects 173 | for (const key in filteredSchema) { 174 | if (typeof filteredSchema[key] === 'object') { 175 | filteredSchema[key] = filterSchema(filteredSchema[key]); 176 | } 177 | } 178 | 179 | return filteredSchema; 180 | } 181 | 182 | return [{ 183 | functionDeclarations 184 | }]; 185 | } 186 | 187 | /** 188 | * Converts Anthropic message content to Gemini parts 189 | */ 190 | function convertContentToGeminiParts(content: any[]): GeminiPart[] { 191 | const parts: GeminiPart[] = []; 192 | 193 | content.forEach(contentPart => { 194 | if (contentPart.type === "text") { 195 | parts.push({ 196 | text: typeof contentPart.text === "string" 197 | ? contentPart.text 198 | : JSON.stringify(contentPart.text) 199 | }); 200 | } else if (contentPart.type === "tool_use") { 201 | parts.push({ 202 | functionCall: { 203 | name: contentPart.name, 204 | args: contentPart.input 205 | } 206 | }); 207 | } else if (contentPart.type === "tool_result") { 208 | parts.push({ 209 | functionResponse: { 210 | name: contentPart.tool_use_id, // Gemini uses the function call ID 211 | response: typeof contentPart.content === "string" 212 | ? { result: contentPart.content } 213 | : contentPart.content 214 | } 215 | }); 216 | } 217 | }); 218 | 219 | return parts; 220 | } 221 | 222 | /** 223 | * Converts Anthropic messages to Gemini contents 224 | */ 225 | function convertMessagesToGeminiContents(messages: any[], systemPrompt?: string): GeminiContent[] { 226 | const contents: GeminiContent[] = []; 227 | 228 | // Add system prompt as first user message if provided 229 | if (systemPrompt) { 230 | contents.push({ 231 | role: "user", 232 | parts: [{ text: systemPrompt }] 233 | }); 234 | 235 | // Add a dummy model response to maintain conversation flow 236 | contents.push({ 237 | role: "model", 238 | parts: [{ text: "I understand. I'll follow these instructions." }] 239 | }); 240 | } 241 | 242 | messages.forEach(message => { 243 | if (message.role === "user") { 244 | const parts = Array.isArray(message.content) 245 | ? convertContentToGeminiParts(message.content) 246 | : [{ text: message.content }]; 247 | 248 | if (parts.length > 0) { 249 | contents.push({ 250 | role: "user", 251 | parts 252 | }); 253 | } 254 | } else if (message.role === "assistant") { 255 | const parts = Array.isArray(message.content) 256 | ? convertContentToGeminiParts(message.content) 257 | : [{ text: message.content }]; 258 | 259 | if (parts.length > 0) { 260 | contents.push({ 261 | role: "model", 262 | parts 263 | }); 264 | } 265 | } 266 | }); 267 | 268 | return contents; 269 | } 270 | 271 | /** 272 | * Converts Anthropic Claude /v1/messages request to Gemini generateContent format 273 | */ 274 | export function formatAnthropicToGemini(body: MessageCreateParamsBase, env?: { GEMINI_MODEL?: string }): { 275 | request: GeminiRequest; 276 | isStream: boolean; 277 | model: string; 278 | } { 279 | const { model, messages, system, temperature, tools, stream } = body; 280 | 281 | // Convert system prompt to string if it's an array 282 | let systemPrompt: string | undefined; 283 | if (system) { 284 | if (Array.isArray(system)) { 285 | systemPrompt = system.map(item => item.text || item).join('\n'); 286 | } else { 287 | systemPrompt = typeof system === 'string' ? system : system.text || JSON.stringify(system); 288 | } 289 | } 290 | 291 | // Convert messages to Gemini contents 292 | const contents = convertMessagesToGeminiContents(messages, systemPrompt); 293 | 294 | // Build generation config 295 | const generationConfig: GeminiGenerationConfig = {}; 296 | if (temperature !== undefined) { 297 | generationConfig.temperature = temperature; 298 | } 299 | 300 | // Build the request 301 | const geminiRequest: GeminiRequest = { 302 | contents, 303 | generationConfig, 304 | safetySettings: getDefaultSafetySettings() 305 | }; 306 | 307 | // Add tools if provided 308 | if (tools && tools.length > 0) { 309 | geminiRequest.tools = convertToolsToGemini(tools); 310 | } else if (tools && tools.length === 0) { 311 | geminiRequest.tools = []; 312 | } 313 | 314 | // Use request model if available, otherwise use environment variable model 315 | const selectedModel = model || env?.GEMINI_MODEL || 'gemini-2.5-flash'; 316 | 317 | return { 318 | request: geminiRequest, 319 | isStream: stream || false, 320 | model: mapModelToGemini(selectedModel) 321 | }; 322 | } 323 | 324 | /** 325 | * Builds the appropriate Gemini API endpoint URL 326 | */ 327 | export function getGeminiEndpoint(model: string, isStream: boolean): string { 328 | const baseUrl = 'https://generativelanguage.googleapis.com/v1beta/models'; 329 | const method = isStream ? 'streamGenerateContent' : 'generateContent'; 330 | return `${baseUrl}/${model}:${method}`; 331 | } 332 | 333 | /** 334 | * Builds headers for Gemini API request 335 | */ 336 | export function getGeminiHeaders(apiKey: string): Record { 337 | return { 338 | 'Content-Type': 'application/json', 339 | 'x-goog-api-key': apiKey 340 | }; 341 | } 342 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------