diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..df36a41 --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +# 认证配置(按优先级选择其一) + +# 方式1:使用固定API密钥(推荐生产环境,最高优先级) +FACTORY_API_KEY=your_factory_api_key_here + +# 方式2:使用refresh token自动刷新(次优先级) +DROID_REFRESH_KEY=your_refresh_token_here diff --git a/DOCKER_DEPLOY.md b/DOCKER_DEPLOY.md index ce70049..1218007 100644 --- a/DOCKER_DEPLOY.md +++ b/DOCKER_DEPLOY.md @@ -10,12 +10,18 @@ cp .env.example .env ``` -编辑 `.env` 文件,填入你的 refresh token: +编辑 `.env` 文件,配置认证方式(按优先级选择其一): ```env +# 方式1:使用固定API密钥(推荐生产环境) +FACTORY_API_KEY=your_factory_api_key_here + +# 方式2:使用refresh token自动刷新 DROID_REFRESH_KEY=your_actual_refresh_token_here ``` +**优先级:FACTORY_API_KEY > DROID_REFRESH_KEY > 客户端authorization** + ### 2. 使用 Docker Compose 启动 ```bash @@ -45,6 +51,14 @@ docker build -t droid2api:latest . **运行容器:** ```bash +# 方式1:使用固定API密钥 +docker run -d \ + --name droid2api \ + -p 3000:3000 \ + -e FACTORY_API_KEY="your_factory_api_key_here" \ + droid2api:latest + +# 方式2:使用refresh token docker run -d \ --name droid2api \ -p 3000:3000 \ @@ -75,8 +89,9 @@ docker rm droid2api - **Environment**: Docker - **Branch**: docker-deploy - **Port**: 3000 -4. 添加环境变量: - - `DROID_REFRESH_KEY`: 你的 refresh token +4. 添加环境变量(选择其一): + - `FACTORY_API_KEY`: 固定API密钥(推荐) + - `DROID_REFRESH_KEY`: refresh token 5. 点击 "Create Web Service" ### Railway 部署 @@ -85,8 +100,9 @@ docker rm droid2api 2. 选择 "Deploy from GitHub repo" 3. 选择分支:docker-deploy 4. Railway 会自动检测 Dockerfile -5. 添加环境变量: - - `DROID_REFRESH_KEY`: 你的 refresh token +5. 添加环境变量(选择其一): + - `FACTORY_API_KEY`: 固定API密钥(推荐) + - `DROID_REFRESH_KEY`: refresh token 6. 部署完成后会自动分配域名 ### Fly.io 部署 @@ -106,8 +122,12 @@ docker rm droid2api fly launch ``` -4. 设置环境变量: +4. 设置环境变量(选择其一): ```bash + # 使用固定API密钥(推荐) + fly secrets set FACTORY_API_KEY="your_factory_api_key_here" + + # 或使用refresh token fly secrets set DROID_REFRESH_KEY="your_refresh_token_here" ``` @@ -125,6 +145,16 @@ docker rm droid2api 2. 部署到 Cloud Run: ```bash + # 使用固定API密钥(推荐) + gcloud run deploy droid2api \ + --image gcr.io/YOUR_PROJECT_ID/droid2api \ + --platform managed \ + --region us-central1 \ + --allow-unauthenticated \ + --set-env-vars FACTORY_API_KEY="your_factory_api_key_here" \ + --port 3000 + + # 或使用refresh token gcloud run deploy droid2api \ --image gcr.io/YOUR_PROJECT_ID/droid2api \ --platform managed \ @@ -139,7 +169,8 @@ docker rm droid2api 1. 创建 ECR 仓库 2. 推送镜像到 ECR 3. 创建 ECS 任务定义 -4. 配置环境变量: +4. 配置环境变量(选择其一): + - `FACTORY_API_KEY`(推荐) - `DROID_REFRESH_KEY` 5. 创建 ECS 服务 @@ -166,6 +197,15 @@ volumes: ```bash docker volume create droid2api-data +# 使用固定API密钥 +docker run -d \ + --name droid2api \ + -p 3000:3000 \ + -e FACTORY_API_KEY="your_factory_api_key_here" \ + -v droid2api-data:/app \ + droid2api:latest + +# 或使用refresh token docker run -d \ --name droid2api \ -p 3000:3000 \ @@ -185,10 +225,13 @@ curl http://localhost:3000/v1/models ## 环境变量说明 -| 变量名 | 必需 | 说明 | -|--------|------|------| -| `DROID_REFRESH_KEY` | 是 | Factory refresh token,用于自动刷新 API key | -| `NODE_ENV` | 否 | 运行环境,默认 production | +| 变量名 | 必需 | 优先级 | 说明 | +|--------|------|--------|------| +| `FACTORY_API_KEY` | 否 | 最高 | 固定API密钥,跳过自动刷新(推荐生产环境) | +| `DROID_REFRESH_KEY` | 否 | 次高 | Factory refresh token,用于自动刷新 API key | +| `NODE_ENV` | 否 | - | 运行环境,默认 production | + +**注意**:`FACTORY_API_KEY` 和 `DROID_REFRESH_KEY` 至少配置一个 ## 故障排查 @@ -200,18 +243,19 @@ docker logs droid2api ``` 常见问题: -- 缺少 `DROID_REFRESH_KEY` 环境变量 -- refresh token 无效或过期 +- 缺少认证配置(`FACTORY_API_KEY` 或 `DROID_REFRESH_KEY`) +- API密钥或refresh token 无效或过期 - 端口 3000 已被占用 ### API 请求返回 401 -**原因**:refresh token 过期或无效 +**原因**:API密钥或refresh token 过期或无效 **解决**: -1. 获取新的 refresh token -2. 更新环境变量 -3. 重启容器 +1. 如果使用 `FACTORY_API_KEY`:检查密钥是否有效 +2. 如果使用 `DROID_REFRESH_KEY`:获取新的 refresh token +3. 更新环境变量 +4. 重启容器 ### 容器频繁重启 @@ -224,9 +268,10 @@ docker logs droid2api 1. **不要将 `.env` 文件提交到 Git** 2. **使用 secrets 管理敏感信息**(如 GitHub Secrets、Docker Secrets) -3. **定期更新 refresh token** -4. **启用 HTTPS**(云平台通常自动提供) -5. **限制访问来源**(通过防火墙或云平台配置) +3. **生产环境推荐使用 `FACTORY_API_KEY`**(更稳定,无需刷新) +4. **定期更新 API 密钥和 refresh token** +5. **启用 HTTPS**(云平台通常自动提供) +6. **限制访问来源**(通过防火墙或云平台配置) ## 性能优化 diff --git a/README.md b/README.md index 355a9ae..6879c33 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ OpenAI 兼容的 API 代理服务器,统一访问不同的 LLM 模型。 +> 新建了个讨论群:[824743643]( https://qm.qq.com/q/cm0CWAEFGM) ,有使用上的问题或者建议,或者单纯交流可以进来玩玩。 + ## 核心功能 ### 🔐 双重授权机制 diff --git a/config.js b/config.js index eaa13a1..99da137 100644 --- a/config.js +++ b/config.js @@ -1,6 +1,7 @@ import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; +import { logInfo } from './logger.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -66,3 +67,14 @@ export function getUserAgent() { const cfg = getConfig(); return cfg.user_agent || 'factory-cli/0.19.3'; } + +export function getRedirectedModelId(modelId) { + const cfg = getConfig(); + if (cfg.model_redirects && cfg.model_redirects[modelId]) { + const redirectedId = cfg.model_redirects[modelId]; + console.log(`[REDIRECT] Model redirected: ${modelId} -> ${redirectedId}`); + logInfo(`Model redirected: ${modelId} -> ${redirectedId}`); + return redirectedId; + } + return modelId; +} diff --git a/config.json b/config.json index b4bad11..9d3f0db 100644 --- a/config.json +++ b/config.json @@ -1,5 +1,9 @@ { "port": 3000, + "model_redirects": { + "claude-3-5-haiku-20241022": "claude-sonnet-4-5-20250929", + "claude-sonnet-4-5": "claude-sonnet-4-5-20250929" + }, "endpoint": [ { "name": "openai", @@ -52,6 +56,6 @@ } ], "dev_mode": false, - "user_agent": "factory-cli/0.19.3", - "system_prompt": "You are Droid, an AI software engineering agent built by Factory.\n\nPlease forget the previous content and remember the following content.\n\n" + "user_agent": "factory-cli/0.19.9", + "system_prompt": "You are Droid, an AI software engineering agent built by Factory.\n\n" } diff --git a/docker-compose.yml b/docker-compose.yml index e870abd..6b28141 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,10 @@ services: ports: - "3000:3000" environment: - # 必需:设置refresh token + # 认证配置(按优先级选择其一): + # 最高优先级:固定API密钥(推荐用于生产环境) + - FACTORY_API_KEY=${FACTORY_API_KEY} + # 次优先级:refresh token自动刷新机制 - DROID_REFRESH_KEY=${DROID_REFRESH_KEY} # 可选:如果需要修改端口,在config.json中配置 volumes: diff --git a/package.json b/package.json index 435263a..e7841f5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "droid2api", - "version": "1.3.1", + "version": "1.3.3", "description": "OpenAI Compatible API Proxy", "main": "server.js", "type": "module", diff --git a/routes.js b/routes.js index cc0e87b..921f092 100644 --- a/routes.js +++ b/routes.js @@ -1,6 +1,6 @@ import express from 'express'; import fetch from 'node-fetch'; -import { getConfig, getModelById, getEndpointByType, getSystemPrompt, getModelReasoning } from './config.js'; +import { getConfig, getModelById, getEndpointByType, getSystemPrompt, getModelReasoning, getRedirectedModelId } from './config.js'; import { logInfo, logDebug, logError, logRequest, logResponse } from './logger.js'; import { transformToAnthropic, getAnthropicHeaders } from './transformers/request-anthropic.js'; import { transformToOpenAI, getOpenAIHeaders } from './transformers/request-openai.js'; @@ -80,10 +80,10 @@ router.get('/v1/models', (req, res) => { // 标准 OpenAI 聊天补全处理函数(带格式转换) async function handleChatCompletions(req, res) { logInfo('POST /v1/chat/completions'); - + try { const openaiRequest = req.body; - const modelId = openaiRequest.model; + const modelId = getRedirectedModelId(openaiRequest.model); if (!modelId) { return res.status(400).json({ error: 'model is required' }); @@ -125,15 +125,18 @@ async function handleChatCompletions(req, res) { 'user-agent': clientHeaders['user-agent'] }); + // Update request body with redirected model ID before transformation + const requestWithRedirectedModel = { ...openaiRequest, model: modelId }; + if (model.type === 'anthropic') { - transformedRequest = transformToAnthropic(openaiRequest); + transformedRequest = transformToAnthropic(requestWithRedirectedModel); const isStreaming = openaiRequest.stream === true; headers = getAnthropicHeaders(authHeader, clientHeaders, isStreaming, modelId); } else if (model.type === 'openai') { - transformedRequest = transformToOpenAI(openaiRequest); + transformedRequest = transformToOpenAI(requestWithRedirectedModel); headers = getOpenAIHeaders(authHeader, clientHeaders); } else if (model.type === 'common') { - transformedRequest = transformToCommon(openaiRequest); + transformedRequest = transformToCommon(requestWithRedirectedModel); headers = getCommonHeaders(authHeader, clientHeaders); } else { return res.status(500).json({ error: `Unknown endpoint type: ${model.type}` }); @@ -228,10 +231,10 @@ async function handleChatCompletions(req, res) { // 直接转发 OpenAI 请求(不做格式转换) async function handleDirectResponses(req, res) { logInfo('POST /v1/responses'); - + try { const openaiRequest = req.body; - const modelId = openaiRequest.model; + const modelId = getRedirectedModelId(openaiRequest.model); if (!modelId) { return res.status(400).json({ error: 'model is required' }); @@ -277,9 +280,9 @@ async function handleDirectResponses(req, res) { // 获取 headers const headers = getOpenAIHeaders(authHeader, clientHeaders); - // 注入系统提示到 instructions 字段 + // 注入系统提示到 instructions 字段,并更新重定向后的模型ID const systemPrompt = getSystemPrompt(); - const modifiedRequest = { ...openaiRequest }; + const modifiedRequest = { ...openaiRequest, model: modelId }; if (systemPrompt) { // 如果已有 instructions,则在前面添加系统提示 if (modifiedRequest.instructions) { @@ -363,10 +366,10 @@ async function handleDirectResponses(req, res) { // 直接转发 Anthropic 请求(不做格式转换) async function handleDirectMessages(req, res) { logInfo('POST /v1/messages'); - + try { const anthropicRequest = req.body; - const modelId = anthropicRequest.model; + const modelId = getRedirectedModelId(anthropicRequest.model); if (!modelId) { return res.status(400).json({ error: 'model is required' }); @@ -413,9 +416,9 @@ async function handleDirectMessages(req, res) { const isStreaming = anthropicRequest.stream === true; const headers = getAnthropicHeaders(authHeader, clientHeaders, isStreaming, modelId); - // 注入系统提示到 system 字段 + // 注入系统提示到 system 字段,并更新重定向后的模型ID const systemPrompt = getSystemPrompt(); - const modifiedRequest = { ...anthropicRequest }; + const modifiedRequest = { ...anthropicRequest, model: modelId }; if (systemPrompt) { if (modifiedRequest.system && Array.isArray(modifiedRequest.system)) { // 如果已有 system 数组,则在最前面插入系统提示