Add common endpoint support and system prompt injection, v1.1.0
- Add common endpoint type for GLM-4.6 model - Implement automatic system prompt injection for all requests - Simplify README documentation for better user focus - Update version to 1.1.0 - Add *.txt to .gitignore Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,3 +2,4 @@ node_modules/
|
|||||||
*.log
|
*.log
|
||||||
.env
|
.env
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
*.txt
|
||||||
|
|||||||
374
README.md
374
README.md
@@ -1,20 +1,14 @@
|
|||||||
# droid2api
|
# droid2api
|
||||||
|
|
||||||
OpenAI 兼容 API 代理服务器,用于在不同 LLM API 格式之间进行转换。
|
OpenAI 兼容的 API 代理服务器,统一访问不同的 LLM 模型。
|
||||||
|
|
||||||
## 功能特性
|
## 功能特性
|
||||||
|
|
||||||
- **三种接口模式**:
|
- 🎯 **标准 OpenAI API 接口** - 使用熟悉的 OpenAI API 格式访问所有模型
|
||||||
- **统一格式接口**:`/v1/chat/completions` - 支持所有端点类型,自动格式转换
|
- 🔄 **自动格式转换** - 自动处理不同 LLM 提供商的格式差异
|
||||||
- **OpenAI 透明代理**:`/v1/responses` - 直接转发 OpenAI 请求,零转换
|
- 🌊 **流式响应支持** - 支持实时流式输出
|
||||||
- **Anthropic 透明代理**:`/v1/messages` - 直接转发 Anthropic 请求,零转换
|
- 🔐 **自动认证管理** - 自动刷新和管理 API 访问令牌
|
||||||
- **标准 OpenAI API 接口**:提供完全兼容 OpenAI 的 API 端点
|
- ⚙️ **灵活配置** - 通过配置文件自定义模型和端点
|
||||||
- **多格式支持**:支持 Anthropic 和自定义 OpenAI 格式之间的自动转换
|
|
||||||
- **流式响应**:自动转换 SSE (Server-Sent Events) 流式响应为标准 OpenAI 格式
|
|
||||||
- **自动刷新 API Key**:集成 WorkOS 认证,自动管理和刷新访问令牌(8小时有效期,每6小时自动刷新)
|
|
||||||
- **智能 Header 管理**:自动添加和管理所有必需的 Factory 特定 headers
|
|
||||||
- **配置化路由**:通过 config.json 灵活配置模型和端点映射
|
|
||||||
- **开发模式**:详细的日志输出,便于调试
|
|
||||||
|
|
||||||
## 安装
|
## 安装
|
||||||
|
|
||||||
@@ -22,25 +16,30 @@ OpenAI 兼容 API 代理服务器,用于在不同 LLM API 格式之间进行
|
|||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
## 配置
|
## 快速开始
|
||||||
|
|
||||||
### 1. 配置端点和模型
|
### 1. 配置认证
|
||||||
|
|
||||||
编辑 `config.json` 文件:
|
设置环境变量或配置文件:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 方式1:环境变量
|
||||||
|
export DROID_REFRESH_KEY="your_refresh_token_here"
|
||||||
|
|
||||||
|
# 方式2:配置文件 ~/.factory/auth.json
|
||||||
|
{
|
||||||
|
"access_token": "your_access_token",
|
||||||
|
"refresh_token": "your_refresh_token"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 配置模型(可选)
|
||||||
|
|
||||||
|
编辑 `config.json` 添加或修改模型:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"port": 3000,
|
"port": 3000,
|
||||||
"endpoint": [
|
|
||||||
{
|
|
||||||
"name": "openai",
|
|
||||||
"base_url": "https://app.factory.ai/api/llm/o/v1/responses"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "anthropic",
|
|
||||||
"base_url": "https://app.factory.ai/api/llm/a/v1/messages"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"models": [
|
"models": [
|
||||||
{
|
{
|
||||||
"name": "Claude Opus 4",
|
"name": "Claude Opus 4",
|
||||||
@@ -48,38 +47,14 @@ npm install
|
|||||||
"type": "anthropic"
|
"type": "anthropic"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "GPT-5 Codex",
|
"name": "GPT-5",
|
||||||
"id": "gpt-5-codex",
|
"id": "gpt-5-2025-08-07",
|
||||||
"type": "openai"
|
"type": "openai"
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"dev_mode": false
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. 配置认证(二选一)
|
|
||||||
|
|
||||||
#### 方式一:使用环境变量(推荐用于开发/测试)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export DROID_REFRESH_KEY="your_refresh_token_here"
|
|
||||||
```
|
|
||||||
|
|
||||||
刷新后的 API key 会保存到工作目录的 `auth.json` 文件。
|
|
||||||
|
|
||||||
#### 方式二:使用配置文件(推荐用于生产环境)
|
|
||||||
|
|
||||||
确保 `~/.factory/auth.json` 文件存在并包含有效的 tokens:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"access_token": "your_access_token_here",
|
|
||||||
"refresh_token": "your_refresh_token_here"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
刷新后的 tokens 会自动更新到原文件。
|
|
||||||
|
|
||||||
## 使用方法
|
## 使用方法
|
||||||
|
|
||||||
### 启动服务器
|
### 启动服务器
|
||||||
@@ -96,260 +71,52 @@ npm start
|
|||||||
|
|
||||||
服务器默认运行在 `http://localhost:3000`。
|
服务器默认运行在 `http://localhost:3000`。
|
||||||
|
|
||||||
### API 端点总览
|
### API 使用
|
||||||
|
|
||||||
| 端点 | 方法 | 支持类型 | 格式转换 | 适用场景 |
|
#### 获取模型列表
|
||||||
|------|------|---------|---------|---------|
|
|
||||||
| `/v1/models` | GET | - | - | 获取模型列表 |
|
|
||||||
| `/v1/chat/completions` | POST | anthropic, openai | ✅ 自动转换 | 需要统一OpenAI格式 |
|
|
||||||
| `/v1/responses` | POST | 仅 openai | ❌ 直接转发 | 已是目标格式,追求性能 |
|
|
||||||
| `/v1/messages` | POST | 仅 anthropic | ❌ 直接转发 | 已是目标格式,追求性能 |
|
|
||||||
|
|
||||||
### API 端点详细说明
|
|
||||||
|
|
||||||
#### 1. 获取可用模型列表
|
|
||||||
|
|
||||||
```bash
|
|
||||||
GET /v1/models
|
|
||||||
```
|
|
||||||
|
|
||||||
**示例:**
|
|
||||||
```bash
|
```bash
|
||||||
curl http://localhost:3000/v1/models
|
curl http://localhost:3000/v1/models
|
||||||
```
|
```
|
||||||
|
|
||||||
**响应:**
|
#### 对话补全
|
||||||
```json
|
|
||||||
{
|
|
||||||
"object": "list",
|
|
||||||
"data": [
|
|
||||||
{
|
|
||||||
"id": "claude-opus-4-1-20250805",
|
|
||||||
"object": "model",
|
|
||||||
"created": 1704067200000,
|
|
||||||
"owned_by": "factory"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. 统一格式接口 - 对话补全(带格式转换)
|
使用标准 OpenAI 格式调用任何模型:
|
||||||
|
|
||||||
```bash
|
|
||||||
POST /v1/chat/completions
|
|
||||||
```
|
|
||||||
|
|
||||||
**功能特点:**
|
|
||||||
- ✅ 支持所有端点类型(anthropic, openai)
|
|
||||||
- ✅ 自动转换请求格式到目标端点格式
|
|
||||||
- ✅ 自动转换响应为标准 OpenAI 格式
|
|
||||||
- ✅ 适合需要统一接口的场景
|
|
||||||
|
|
||||||
**请求参数:**
|
|
||||||
- `model` (必需): 模型 ID
|
|
||||||
- `messages` (必需): 标准 OpenAI 格式消息数组
|
|
||||||
- `stream` (可选): 是否使用流式响应,默认 true
|
|
||||||
- `max_tokens` (可选): 最大输出 tokens 数
|
|
||||||
- `temperature` (可选): 温度参数 0-1
|
|
||||||
- `top_p` (可选): Top-p 采样参数
|
|
||||||
|
|
||||||
**示例(Anthropic 模型,自动转换):**
|
|
||||||
```bash
|
```bash
|
||||||
curl http://localhost:3000/v1/chat/completions \
|
curl http://localhost:3000/v1/chat/completions \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{
|
-d '{
|
||||||
"model": "claude-opus-4-1-20250805",
|
"model": "claude-opus-4-1-20250805",
|
||||||
"messages": [
|
"messages": [
|
||||||
{"role": "user", "content": "你好,请介绍一下你自己"}
|
{"role": "user", "content": "你好"}
|
||||||
],
|
],
|
||||||
"stream": true,
|
|
||||||
"max_tokens": 2000
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
**示例(OpenAI 模型,自动转换):**
|
|
||||||
```bash
|
|
||||||
curl http://localhost:3000/v1/chat/completions \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"model": "gpt-5-codex",
|
|
||||||
"messages": [
|
|
||||||
{"role": "user", "content": "写一个 Python 快速排序"}
|
|
||||||
],
|
|
||||||
"stream": false
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3. OpenAI 透明代理接口(不做转换)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
POST /v1/responses
|
|
||||||
```
|
|
||||||
|
|
||||||
**功能特点:**
|
|
||||||
- ⚠️ **仅支持 openai 类型端点**
|
|
||||||
- ❌ 请求体不做任何转换,直接转发
|
|
||||||
- ❌ 响应体不做任何转换,直接转发
|
|
||||||
- ✅ 适合已是目标格式,追求最高性能的场景
|
|
||||||
|
|
||||||
**限制:**
|
|
||||||
使用非 openai 类型模型会返回 400 错误:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"error": "Invalid endpoint type",
|
|
||||||
"message": "/v1/responses 接口只支持 openai 类型端点"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**示例:**
|
|
||||||
```bash
|
|
||||||
curl http://localhost:3000/v1/responses \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"model": "gpt-5-codex",
|
|
||||||
"messages": [{"role": "user", "content": "Hello"}],
|
|
||||||
"stream": true
|
"stream": true
|
||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 4. Anthropic 透明代理接口(不做转换)
|
**支持的参数:**
|
||||||
|
- `model` - 模型 ID(必需)
|
||||||
|
- `messages` - 对话消息数组(必需)
|
||||||
|
- `stream` - 是否流式输出(默认 true)
|
||||||
|
- `max_tokens` - 最大输出长度
|
||||||
|
- `temperature` - 温度参数(0-1)
|
||||||
|
|
||||||
```bash
|
## 常见问题
|
||||||
POST /v1/messages
|
|
||||||
```
|
|
||||||
|
|
||||||
**功能特点:**
|
### 如何更改端口?
|
||||||
- ⚠️ **仅支持 anthropic 类型端点**
|
|
||||||
- ❌ 请求体不做任何转换,直接转发
|
编辑 `config.json` 中的 `port` 字段:
|
||||||
- ❌ 响应体不做任何转换,直接转发
|
|
||||||
- ✅ 适合已是目标格式,追求最高性能的场景
|
|
||||||
|
|
||||||
**限制:**
|
|
||||||
使用非 anthropic 类型模型会返回 400 错误:
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"error": "Invalid endpoint type",
|
"port": 8080
|
||||||
"message": "/v1/messages 接口只支持 anthropic 类型端点"
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**示例:**
|
### 如何启用调试日志?
|
||||||
```bash
|
|
||||||
curl http://localhost:3000/v1/messages \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"model": "claude-opus-4-1-20250805",
|
|
||||||
"messages": [{"role": "user", "content": "Hello"}],
|
|
||||||
"max_tokens": 1024,
|
|
||||||
"stream": true
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
## API Key 自动刷新机制
|
在 `config.json` 中设置:
|
||||||
|
|
||||||
代理服务器会自动管理 API key 的刷新:
|
|
||||||
|
|
||||||
1. **启动时刷新**:服务器启动时自动获取新的 access token
|
|
||||||
2. **定期刷新**:每次 API 请求前检查,如果距离上次刷新超过 6 小时则自动刷新
|
|
||||||
3. **令牌有效期**:access token 有效期为 8 小时
|
|
||||||
4. **自动保存**:刷新后的 tokens 自动保存到相应的配置文件
|
|
||||||
|
|
||||||
**刷新日志示例:**
|
|
||||||
```
|
|
||||||
[INFO] Refreshing API key...
|
|
||||||
[INFO] Authenticated as: user@example.com (John Doe)
|
|
||||||
[INFO] User ID: user_01K69S755R2TWYFWKPSP74TRKZ
|
|
||||||
[INFO] Organization ID: org_01K69S7KKYK6F2WYJ8CB384GW6
|
|
||||||
[INFO] API key refreshed successfully
|
|
||||||
```
|
|
||||||
|
|
||||||
## 接口模式选择指南
|
|
||||||
|
|
||||||
### 何时使用 `/v1/chat/completions`(统一格式)
|
|
||||||
|
|
||||||
✅ **推荐场景:**
|
|
||||||
- 需要统一的 OpenAI 兼容接口
|
|
||||||
- 应用代码已使用 OpenAI SDK
|
|
||||||
- 需要在不同 LLM 提供商之间切换
|
|
||||||
- 不关心轻微的性能损耗
|
|
||||||
|
|
||||||
❌ **不推荐场景:**
|
|
||||||
- 已有原生格式的请求/响应处理逻辑
|
|
||||||
- 对性能要求极高(需要避免格式转换开销)
|
|
||||||
|
|
||||||
### 何时使用 `/v1/responses`(OpenAI 透明代理)
|
|
||||||
|
|
||||||
✅ **推荐场景:**
|
|
||||||
- 请求已经是目标 OpenAI 端点格式
|
|
||||||
- 追求最高性能,避免格式转换开销
|
|
||||||
- 只使用 OpenAI 端点
|
|
||||||
|
|
||||||
❌ **不推荐场景:**
|
|
||||||
- 使用 Anthropic 端点(会返回错误)
|
|
||||||
- 需要格式转换
|
|
||||||
|
|
||||||
### 何时使用 `/v1/messages`(Anthropic 透明代理)
|
|
||||||
|
|
||||||
✅ **推荐场景:**
|
|
||||||
- 请求已经是标准 Anthropic 格式
|
|
||||||
- 追求最高性能,避免格式转换开销
|
|
||||||
- 只使用 Anthropic 端点
|
|
||||||
|
|
||||||
❌ **不推荐场景:**
|
|
||||||
- 使用 OpenAI 端点(会返回错误)
|
|
||||||
- 需要格式转换
|
|
||||||
|
|
||||||
## 格式转换说明
|
|
||||||
|
|
||||||
> 注意:仅 `/v1/chat/completions` 接口会进行格式转换,`/v1/responses` 和 `/v1/messages` 直接转发,不做任何转换。
|
|
||||||
|
|
||||||
### Anthropic 格式转换(仅 /v1/chat/completions)
|
|
||||||
|
|
||||||
**请求转换:**
|
|
||||||
- `messages` → `messages`(提取 system 消息到顶层)
|
|
||||||
- `max_tokens` → `max_tokens`(默认 4096)
|
|
||||||
- 文本内容包装为 `{type: 'text', text: '...'}`
|
|
||||||
- 工具格式转换
|
|
||||||
|
|
||||||
**响应转换:**
|
|
||||||
- 转换 SSE 事件:`message_start`, `content_block_delta`, `message_delta`, `message_stop`
|
|
||||||
- 转换为标准 OpenAI chunk 格式
|
|
||||||
- 映射停止原因:`end_turn` → `stop`, `max_tokens` → `length`
|
|
||||||
|
|
||||||
### OpenAI 格式转换(仅 /v1/chat/completions)
|
|
||||||
|
|
||||||
**请求转换:**
|
|
||||||
- `messages` → `input`
|
|
||||||
- `max_tokens` → `max_output_tokens`
|
|
||||||
- 用户消息:`text` → `input_text`
|
|
||||||
- 助手消息:`text` → `output_text`
|
|
||||||
- 提取 system 消息为 `instructions` 参数
|
|
||||||
|
|
||||||
**响应转换:**
|
|
||||||
- 转换 SSE 事件:`response.created`, `response.in_progress`, `response.done`
|
|
||||||
- 转换为标准 OpenAI chunk 格式
|
|
||||||
|
|
||||||
## Header 管理
|
|
||||||
|
|
||||||
代理服务器会自动添加所有必需的 headers:
|
|
||||||
|
|
||||||
### Anthropic 端点
|
|
||||||
- `x-model-provider: anthropic`
|
|
||||||
- `x-factory-client: cli`
|
|
||||||
- `user-agent: a$/JS 0.57.0`
|
|
||||||
- `anthropic-version: 2023-06-01`
|
|
||||||
- `anthropic-beta: interleaved-thinking-2025-05-14`
|
|
||||||
- `x-stainless-helper-method: stream`(流式请求)
|
|
||||||
- 自动生成的 UUID:`x-session-id`, `x-assistant-message-id`
|
|
||||||
|
|
||||||
### OpenAI 端点
|
|
||||||
- `x-factory-client: cli`
|
|
||||||
- `user-agent: cB/JS 5.22.0`
|
|
||||||
- 自动生成的 UUID:`x-session-id`, `x-assistant-message-id`
|
|
||||||
|
|
||||||
## 开发模式
|
|
||||||
|
|
||||||
在 `config.json` 中设置 `dev_mode: true` 可以启用详细日志:
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -357,58 +124,17 @@ curl http://localhost:3000/v1/messages \
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**日志内容包括:**
|
|
||||||
- 完整的请求和响应 headers
|
|
||||||
- 请求体和响应体
|
|
||||||
- 格式转换过程
|
|
||||||
- SSE 事件处理详情
|
|
||||||
|
|
||||||
## 端口冲突处理
|
|
||||||
|
|
||||||
如果端口 3000 已被占用,可以:
|
|
||||||
|
|
||||||
1. **修改配置文件**:编辑 `config.json` 中的 `port` 字段
|
|
||||||
2. **或者结束占用进程**:
|
|
||||||
```bash
|
|
||||||
lsof -ti:3000 | xargs kill -9
|
|
||||||
```
|
|
||||||
|
|
||||||
## 故障排查
|
## 故障排查
|
||||||
|
|
||||||
### 启动时报错 "Refresh token not found"
|
### 认证失败
|
||||||
|
|
||||||
**原因**:未配置 refresh token
|
确保已正确配置 refresh token:
|
||||||
|
|
||||||
**解决方案**:
|
|
||||||
- 设置环境变量 `DROID_REFRESH_KEY`
|
- 设置环境变量 `DROID_REFRESH_KEY`
|
||||||
- 或配置 `~/.factory/auth.json` 文件
|
- 或创建 `~/.factory/auth.json` 文件
|
||||||
|
|
||||||
### 请求返回 401 错误
|
### 模型不可用
|
||||||
|
|
||||||
**可能原因**:
|
检查 `config.json` 中的模型配置,确保模型 ID 和类型正确。
|
||||||
1. refresh token 已过期或无效
|
|
||||||
2. API key 刷新失败
|
|
||||||
|
|
||||||
**解决方案**:
|
|
||||||
- 检查日志中的刷新错误信息
|
|
||||||
- 重新获取有效的 refresh token
|
|
||||||
- 确认 `~/.factory/auth.json` 中的 tokens 正确
|
|
||||||
|
|
||||||
### 响应格式错误
|
|
||||||
|
|
||||||
**原因**:模型类型配置错误
|
|
||||||
|
|
||||||
**解决方案**:
|
|
||||||
- 检查 `config.json` 中模型的 `type` 字段
|
|
||||||
- Anthropic 模型使用 `"type": "anthropic"`
|
|
||||||
- OpenAI 模型使用 `"type": "openai"`
|
|
||||||
|
|
||||||
## 技术架构
|
|
||||||
|
|
||||||
- **语言**:Node.js (ES Modules)
|
|
||||||
- **框架**:Express
|
|
||||||
- **HTTP 客户端**:node-fetch
|
|
||||||
- **认证**:WorkOS OAuth 2.0 Refresh Token Flow
|
|
||||||
|
|
||||||
## 许可证
|
## 许可证
|
||||||
|
|
||||||
|
|||||||
@@ -44,3 +44,8 @@ export function getPort() {
|
|||||||
const cfg = getConfig();
|
const cfg = getConfig();
|
||||||
return cfg.port || 3000;
|
return cfg.port || 3000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getSystemPrompt() {
|
||||||
|
const cfg = getConfig();
|
||||||
|
return cfg.system_prompt || '';
|
||||||
|
}
|
||||||
|
|||||||
12
config.json
12
config.json
@@ -8,6 +8,10 @@
|
|||||||
{
|
{
|
||||||
"name": "anthropic",
|
"name": "anthropic",
|
||||||
"base_url": "https://app.factory.ai/api/llm/a/v1/messages"
|
"base_url": "https://app.factory.ai/api/llm/a/v1/messages"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "common",
|
||||||
|
"base_url": "https://app.factory.ai/api/llm/o/v1/chat/completions"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"models": [
|
"models": [
|
||||||
@@ -35,7 +39,13 @@
|
|||||||
"name": "GPT-5-Codex",
|
"name": "GPT-5-Codex",
|
||||||
"id": "gpt-5-codex",
|
"id": "gpt-5-codex",
|
||||||
"type": "openai"
|
"type": "openai"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "GLM-4.6",
|
||||||
|
"id": "glm-4.6",
|
||||||
|
"type": "common"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"dev_mode": false
|
"dev_mode": false,
|
||||||
|
"system_prompt": "You are Droid, an AI software engineering agent built by Factory.\n\n"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "droid2api",
|
"name": "droid2api",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"description": "OpenAI Compatible API Proxy",
|
"description": "OpenAI Compatible API Proxy",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
98
routes.js
98
routes.js
@@ -1,9 +1,10 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import fetch from 'node-fetch';
|
import fetch from 'node-fetch';
|
||||||
import { getConfig, getModelById, getEndpointByType } from './config.js';
|
import { getConfig, getModelById, getEndpointByType, getSystemPrompt } from './config.js';
|
||||||
import { logInfo, logDebug, logError, logRequest, logResponse } from './logger.js';
|
import { logInfo, logDebug, logError, logRequest, logResponse } from './logger.js';
|
||||||
import { transformToAnthropic, getAnthropicHeaders } from './transformers/request-anthropic.js';
|
import { transformToAnthropic, getAnthropicHeaders } from './transformers/request-anthropic.js';
|
||||||
import { transformToOpenAI, getOpenAIHeaders } from './transformers/request-openai.js';
|
import { transformToOpenAI, getOpenAIHeaders } from './transformers/request-openai.js';
|
||||||
|
import { transformToCommon, getCommonHeaders } from './transformers/request-common.js';
|
||||||
import { AnthropicResponseTransformer } from './transformers/response-anthropic.js';
|
import { AnthropicResponseTransformer } from './transformers/response-anthropic.js';
|
||||||
import { OpenAIResponseTransformer } from './transformers/response-openai.js';
|
import { OpenAIResponseTransformer } from './transformers/response-openai.js';
|
||||||
import { getApiKey } from './auth.js';
|
import { getApiKey } from './auth.js';
|
||||||
@@ -93,6 +94,9 @@ async function handleChatCompletions(req, res) {
|
|||||||
} else if (model.type === 'openai') {
|
} else if (model.type === 'openai') {
|
||||||
transformedRequest = transformToOpenAI(openaiRequest);
|
transformedRequest = transformToOpenAI(openaiRequest);
|
||||||
headers = getOpenAIHeaders(authHeader, clientHeaders);
|
headers = getOpenAIHeaders(authHeader, clientHeaders);
|
||||||
|
} else if (model.type === 'common') {
|
||||||
|
transformedRequest = transformToCommon(openaiRequest);
|
||||||
|
headers = getCommonHeaders(authHeader, clientHeaders);
|
||||||
} else {
|
} else {
|
||||||
return res.status(500).json({ error: `Unknown endpoint type: ${model.type}` });
|
return res.status(500).json({ error: `Unknown endpoint type: ${model.type}` });
|
||||||
}
|
}
|
||||||
@@ -123,22 +127,37 @@ async function handleChatCompletions(req, res) {
|
|||||||
res.setHeader('Cache-Control', 'no-cache');
|
res.setHeader('Cache-Control', 'no-cache');
|
||||||
res.setHeader('Connection', 'keep-alive');
|
res.setHeader('Connection', 'keep-alive');
|
||||||
|
|
||||||
let transformer;
|
// common 类型直接转发,不使用 transformer
|
||||||
if (model.type === 'anthropic') {
|
if (model.type === 'common') {
|
||||||
transformer = new AnthropicResponseTransformer(modelId, `chatcmpl-${Date.now()}`);
|
try {
|
||||||
} else if (model.type === 'openai') {
|
for await (const chunk of response.body) {
|
||||||
transformer = new OpenAIResponseTransformer(modelId, `chatcmpl-${Date.now()}`);
|
res.write(chunk);
|
||||||
}
|
}
|
||||||
|
res.end();
|
||||||
try {
|
logInfo('Stream forwarded (common type)');
|
||||||
for await (const chunk of transformer.transformStream(response.body)) {
|
} catch (streamError) {
|
||||||
res.write(chunk);
|
logError('Stream error', streamError);
|
||||||
|
res.end();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// anthropic 和 openai 类型使用 transformer
|
||||||
|
let transformer;
|
||||||
|
if (model.type === 'anthropic') {
|
||||||
|
transformer = new AnthropicResponseTransformer(modelId, `chatcmpl-${Date.now()}`);
|
||||||
|
} else if (model.type === 'openai') {
|
||||||
|
transformer = new OpenAIResponseTransformer(modelId, `chatcmpl-${Date.now()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for await (const chunk of transformer.transformStream(response.body)) {
|
||||||
|
res.write(chunk);
|
||||||
|
}
|
||||||
|
res.end();
|
||||||
|
logInfo('Stream completed');
|
||||||
|
} catch (streamError) {
|
||||||
|
logError('Stream error', streamError);
|
||||||
|
res.end();
|
||||||
}
|
}
|
||||||
res.end();
|
|
||||||
logInfo('Stream completed');
|
|
||||||
} catch (streamError) {
|
|
||||||
logError('Stream error', streamError);
|
|
||||||
res.end();
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
@@ -201,16 +220,29 @@ async function handleDirectResponses(req, res) {
|
|||||||
|
|
||||||
const clientHeaders = req.headers;
|
const clientHeaders = req.headers;
|
||||||
|
|
||||||
// 获取 headers,但请求体不做任何转换
|
// 获取 headers
|
||||||
const headers = getOpenAIHeaders(authHeader, clientHeaders);
|
const headers = getOpenAIHeaders(authHeader, clientHeaders);
|
||||||
|
|
||||||
logRequest('POST', endpoint.base_url, headers, openaiRequest);
|
// 注入系统提示到 instructions 字段
|
||||||
|
const systemPrompt = getSystemPrompt();
|
||||||
|
const modifiedRequest = { ...openaiRequest };
|
||||||
|
if (systemPrompt) {
|
||||||
|
// 如果已有 instructions,则在前面添加系统提示
|
||||||
|
if (modifiedRequest.instructions) {
|
||||||
|
modifiedRequest.instructions = systemPrompt + modifiedRequest.instructions;
|
||||||
|
} else {
|
||||||
|
// 否则直接设置系统提示
|
||||||
|
modifiedRequest.instructions = systemPrompt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 直接转发原始请求
|
logRequest('POST', endpoint.base_url, headers, modifiedRequest);
|
||||||
|
|
||||||
|
// 转发修改后的请求
|
||||||
const response = await fetch(endpoint.base_url, {
|
const response = await fetch(endpoint.base_url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers,
|
headers,
|
||||||
body: JSON.stringify(openaiRequest) // 不做任何转换,直接转发
|
body: JSON.stringify(modifiedRequest)
|
||||||
});
|
});
|
||||||
|
|
||||||
logInfo(`Response status: ${response.status}`);
|
logInfo(`Response status: ${response.status}`);
|
||||||
@@ -305,17 +337,35 @@ async function handleDirectMessages(req, res) {
|
|||||||
|
|
||||||
const clientHeaders = req.headers;
|
const clientHeaders = req.headers;
|
||||||
|
|
||||||
// 获取 headers,但请求体不做任何转换
|
// 获取 headers
|
||||||
const isStreaming = anthropicRequest.stream !== false;
|
const isStreaming = anthropicRequest.stream !== false;
|
||||||
const headers = getAnthropicHeaders(authHeader, clientHeaders, isStreaming);
|
const headers = getAnthropicHeaders(authHeader, clientHeaders, isStreaming);
|
||||||
|
|
||||||
logRequest('POST', endpoint.base_url, headers, anthropicRequest);
|
// 注入系统提示到 system 字段
|
||||||
|
const systemPrompt = getSystemPrompt();
|
||||||
|
const modifiedRequest = { ...anthropicRequest };
|
||||||
|
if (systemPrompt) {
|
||||||
|
if (modifiedRequest.system && Array.isArray(modifiedRequest.system)) {
|
||||||
|
// 如果已有 system 数组,则在最前面插入系统提示
|
||||||
|
modifiedRequest.system = [
|
||||||
|
{ type: 'text', text: systemPrompt },
|
||||||
|
...modifiedRequest.system
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
// 否则创建新的 system 数组
|
||||||
|
modifiedRequest.system = [
|
||||||
|
{ type: 'text', text: systemPrompt }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 直接转发原始请求
|
logRequest('POST', endpoint.base_url, headers, modifiedRequest);
|
||||||
|
|
||||||
|
// 转发修改后的请求
|
||||||
const response = await fetch(endpoint.base_url, {
|
const response = await fetch(endpoint.base_url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers,
|
headers,
|
||||||
body: JSON.stringify(anthropicRequest) // 不做任何转换,直接转发
|
body: JSON.stringify(modifiedRequest)
|
||||||
});
|
});
|
||||||
|
|
||||||
logInfo(`Response status: ${response.status}`);
|
logInfo(`Response status: ${response.status}`);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { logDebug } from '../logger.js';
|
import { logDebug } from '../logger.js';
|
||||||
|
import { getSystemPrompt } from '../config.js';
|
||||||
|
|
||||||
export function transformToAnthropic(openaiRequest) {
|
export function transformToAnthropic(openaiRequest) {
|
||||||
logDebug('Transforming OpenAI request to Anthropic format');
|
logDebug('Transforming OpenAI request to Anthropic format');
|
||||||
@@ -77,9 +78,19 @@ export function transformToAnthropic(openaiRequest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add system parameter if system content exists
|
// Add system parameter with system prompt prepended
|
||||||
if (systemContent.length > 0) {
|
const systemPrompt = getSystemPrompt();
|
||||||
anthropicRequest.system = systemContent;
|
if (systemPrompt || systemContent.length > 0) {
|
||||||
|
anthropicRequest.system = [];
|
||||||
|
// Prepend system prompt as first element if it exists
|
||||||
|
if (systemPrompt) {
|
||||||
|
anthropicRequest.system.push({
|
||||||
|
type: 'text',
|
||||||
|
text: systemPrompt
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Add user-provided system content
|
||||||
|
anthropicRequest.system.push(...systemContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transform tools if present
|
// Transform tools if present
|
||||||
@@ -125,11 +136,11 @@ export function getAnthropicHeaders(authHeader, clientHeaders = {}, isStreaming
|
|||||||
'anthropic-beta': 'interleaved-thinking-2025-05-14',
|
'anthropic-beta': 'interleaved-thinking-2025-05-14',
|
||||||
'x-api-key': 'placeholder',
|
'x-api-key': 'placeholder',
|
||||||
'authorization': authHeader || '',
|
'authorization': authHeader || '',
|
||||||
'x-model-provider': 'anthropic',
|
'x-api-provider': 'anthropic',
|
||||||
'x-factory-client': 'cli',
|
'x-factory-client': 'cli',
|
||||||
'x-session-id': sessionId,
|
'x-session-id': sessionId,
|
||||||
'x-assistant-message-id': messageId,
|
'x-assistant-message-id': messageId,
|
||||||
'user-agent': 'a$/JS 0.57.0',
|
'user-agent': 'uX/JS 0.57.0',
|
||||||
'x-stainless-timeout': '600',
|
'x-stainless-timeout': '600',
|
||||||
'connection': 'keep-alive'
|
'connection': 'keep-alive'
|
||||||
};
|
};
|
||||||
|
|||||||
88
transformers/request-common.js
Normal file
88
transformers/request-common.js
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { logDebug } from '../logger.js';
|
||||||
|
import { getSystemPrompt } from '../config.js';
|
||||||
|
|
||||||
|
export function transformToCommon(openaiRequest) {
|
||||||
|
logDebug('Transforming OpenAI request to Common format');
|
||||||
|
|
||||||
|
// 基本保持 OpenAI 格式,只在 messages 前面插入 system 消息
|
||||||
|
const commonRequest = {
|
||||||
|
...openaiRequest
|
||||||
|
};
|
||||||
|
|
||||||
|
const systemPrompt = getSystemPrompt();
|
||||||
|
|
||||||
|
if (systemPrompt) {
|
||||||
|
// 检查是否已有 system 消息
|
||||||
|
const hasSystemMessage = commonRequest.messages?.some(m => m.role === 'system');
|
||||||
|
|
||||||
|
if (hasSystemMessage) {
|
||||||
|
// 如果已有 system 消息,在第一个 system 消息前插入我们的 system prompt
|
||||||
|
commonRequest.messages = commonRequest.messages.map((msg, index) => {
|
||||||
|
if (msg.role === 'system' && index === commonRequest.messages.findIndex(m => m.role === 'system')) {
|
||||||
|
// 找到第一个 system 消息,前置我们的 prompt
|
||||||
|
return {
|
||||||
|
role: 'system',
|
||||||
|
content: systemPrompt + (typeof msg.content === 'string' ? msg.content : '')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return msg;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 如果没有 system 消息,在 messages 数组最前面插入
|
||||||
|
commonRequest.messages = [
|
||||||
|
{
|
||||||
|
role: 'system',
|
||||||
|
content: systemPrompt
|
||||||
|
},
|
||||||
|
...(commonRequest.messages || [])
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logDebug('Transformed Common request', commonRequest);
|
||||||
|
return commonRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCommonHeaders(authHeader, clientHeaders = {}) {
|
||||||
|
// Generate unique IDs if not provided
|
||||||
|
const sessionId = clientHeaders['x-session-id'] || generateUUID();
|
||||||
|
const messageId = clientHeaders['x-assistant-message-id'] || generateUUID();
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
'accept': 'application/json',
|
||||||
|
'content-type': 'application/json',
|
||||||
|
'authorization': authHeader || '',
|
||||||
|
'x-api-provider': 'baseten',
|
||||||
|
'x-factory-client': 'cli',
|
||||||
|
'x-session-id': sessionId,
|
||||||
|
'x-assistant-message-id': messageId,
|
||||||
|
'user-agent': 'pB/JS 5.23.2',
|
||||||
|
'connection': 'keep-alive'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pass through Stainless SDK headers with defaults
|
||||||
|
const stainlessDefaults = {
|
||||||
|
'x-stainless-arch': 'x64',
|
||||||
|
'x-stainless-lang': 'js',
|
||||||
|
'x-stainless-os': 'MacOS',
|
||||||
|
'x-stainless-runtime': 'node',
|
||||||
|
'x-stainless-retry-count': '0',
|
||||||
|
'x-stainless-package-version': '5.23.2',
|
||||||
|
'x-stainless-runtime-version': 'v24.3.0'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Copy Stainless headers from client or use defaults
|
||||||
|
Object.keys(stainlessDefaults).forEach(header => {
|
||||||
|
headers[header] = clientHeaders[header] || stainlessDefaults[header];
|
||||||
|
});
|
||||||
|
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateUUID() {
|
||||||
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||||
|
const r = Math.random() * 16 | 0;
|
||||||
|
const v = c == 'x' ? r : (r & 0x3 | 0x8);
|
||||||
|
return v.toString(16);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { logDebug } from '../logger.js';
|
import { logDebug } from '../logger.js';
|
||||||
|
import { getSystemPrompt } from '../config.js';
|
||||||
|
|
||||||
export function transformToOpenAI(openaiRequest) {
|
export function transformToOpenAI(openaiRequest) {
|
||||||
logDebug('Transforming OpenAI request to target OpenAI format');
|
logDebug('Transforming OpenAI request to target OpenAI format');
|
||||||
@@ -66,18 +67,25 @@ export function transformToOpenAI(openaiRequest) {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract system message as instructions
|
// Extract system message as instructions and prepend system prompt
|
||||||
|
const systemPrompt = getSystemPrompt();
|
||||||
const systemMessage = openaiRequest.messages?.find(m => m.role === 'system');
|
const systemMessage = openaiRequest.messages?.find(m => m.role === 'system');
|
||||||
|
|
||||||
if (systemMessage) {
|
if (systemMessage) {
|
||||||
|
let userInstructions = '';
|
||||||
if (typeof systemMessage.content === 'string') {
|
if (typeof systemMessage.content === 'string') {
|
||||||
targetRequest.instructions = systemMessage.content;
|
userInstructions = systemMessage.content;
|
||||||
} else if (Array.isArray(systemMessage.content)) {
|
} else if (Array.isArray(systemMessage.content)) {
|
||||||
targetRequest.instructions = systemMessage.content
|
userInstructions = systemMessage.content
|
||||||
.filter(p => p.type === 'text')
|
.filter(p => p.type === 'text')
|
||||||
.map(p => p.text)
|
.map(p => p.text)
|
||||||
.join('\n');
|
.join('\n');
|
||||||
}
|
}
|
||||||
|
targetRequest.instructions = systemPrompt + userInstructions;
|
||||||
targetRequest.input = targetRequest.input.filter(m => m.role !== 'system');
|
targetRequest.input = targetRequest.input.filter(m => m.role !== 'system');
|
||||||
|
} else if (systemPrompt) {
|
||||||
|
// If no user-provided system message, just add the system prompt
|
||||||
|
targetRequest.instructions = systemPrompt;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass through other parameters
|
// Pass through other parameters
|
||||||
@@ -109,11 +117,11 @@ export function getOpenAIHeaders(authHeader, clientHeaders = {}) {
|
|||||||
const headers = {
|
const headers = {
|
||||||
'content-type': 'application/json',
|
'content-type': 'application/json',
|
||||||
'authorization': authHeader || '',
|
'authorization': authHeader || '',
|
||||||
'x-api-key': 'placeholder',
|
'x-api-provider': 'azure_openai',
|
||||||
'x-factory-client': 'cli',
|
'x-factory-client': 'cli',
|
||||||
'x-session-id': sessionId,
|
'x-session-id': sessionId,
|
||||||
'x-assistant-message-id': messageId,
|
'x-assistant-message-id': messageId,
|
||||||
'user-agent': 'cB/JS 5.22.0',
|
'user-agent': 'pB/JS 5.23.2',
|
||||||
'connection': 'keep-alive'
|
'connection': 'keep-alive'
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -124,7 +132,7 @@ export function getOpenAIHeaders(authHeader, clientHeaders = {}) {
|
|||||||
'x-stainless-os': 'MacOS',
|
'x-stainless-os': 'MacOS',
|
||||||
'x-stainless-runtime': 'node',
|
'x-stainless-runtime': 'node',
|
||||||
'x-stainless-retry-count': '0',
|
'x-stainless-retry-count': '0',
|
||||||
'x-stainless-package-version': '5.22.0',
|
'x-stainless-package-version': '5.23.2',
|
||||||
'x-stainless-runtime-version': 'v24.3.0'
|
'x-stainless-runtime-version': 'v24.3.0'
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -133,8 +141,6 @@ export function getOpenAIHeaders(authHeader, clientHeaders = {}) {
|
|||||||
headers[header] = clientHeaders[header] || stainlessDefaults[header];
|
headers[header] = clientHeaders[header] || stainlessDefaults[header];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return headers;
|
return headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user