merge:增加对count_tokens的支持
This commit is contained in:
7
.env.example
Normal file
7
.env.example
Normal file
@@ -0,0 +1,7 @@
|
||||
# 认证配置(按优先级选择其一)
|
||||
|
||||
# 方式1:使用固定API密钥(推荐生产环境,最高优先级)
|
||||
FACTORY_API_KEY=your_factory_api_key_here
|
||||
|
||||
# 方式2:使用refresh token自动刷新(次优先级)
|
||||
DROID_REFRESH_KEY=your_refresh_token_here
|
||||
@@ -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. **限制访问来源**(通过防火墙或云平台配置)
|
||||
|
||||
## 性能优化
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
OpenAI 兼容的 API 代理服务器,统一访问不同的 LLM 模型。
|
||||
|
||||
> 新建了个讨论群:[824743643]( https://qm.qq.com/q/cm0CWAEFGM) ,有使用上的问题或者建议,或者单纯交流可以进来玩玩。
|
||||
|
||||
## 核心功能
|
||||
|
||||
### 🔐 双重授权机制
|
||||
|
||||
12
config.js
12
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;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "droid2api",
|
||||
"version": "1.3.1",
|
||||
"version": "1.3.3",
|
||||
"description": "OpenAI Compatible API Proxy",
|
||||
"main": "server.js",
|
||||
"type": "module",
|
||||
|
||||
31
routes.js
31
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 数组,则在最前面插入系统提示
|
||||
|
||||
Reference in New Issue
Block a user