Files
lingma-openai-gateway/README.md
GitHub Actions 3498b81fa2 fix: enable anthropic agent mode for tooling requests
Use agent ask_mode for Anthropic messages with tooling context so tool/write flows are executed, and add regression coverage plus docs/env updates for TOOL_FORWARD_ENABLED.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-19 20:15:14 +08:00

397 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Lingma OpenAI Gateway
把本地 Lingma 插件封装成 OpenAI 兼容接口。任何能调 OpenAI 的客户端Cursor、Dify、LangChain、curl…都能直接接入。
**支持:**
- OpenAI 兼容:`GET /v1/models` / `POST /v1/chat/completions`(含 SSE 流式) / Bearer 鉴权
- **Anthropic 兼容**`POST /v1/messages`(含 Anthropic SSE 事件流) / `x-api-key` 鉴权
- Prometheus / 多账号实例池 / 会话复用(跨两种协议共享) / 免浏览器登录态注入
> 想看架构、模块划分、设计决策、二开路线图 → 直接读 [`DESIGN.md`](./DESIGN.md)。
---
## 架构速览
```
┌─────────────┐ OpenAI 协议 ┌─────────────────────────────────────────┐
│ 任意客户端 │ ───────────▶ │ FastAPI (app/main.py) │
│ (curl/ │ │ ├─ auth_guard / admin_guard │
│ Cursor/ │ │ ├─ chat_guard (InFlightGuard 背压) │
│ Dify…) │ │ ├─ SessionCache (LRU+TTL, KV 复用) │
└─────────────┘ │ └─ StatsCollector + Prometheus │
└────────────────┬────────────────────────┘
│ 选实例 (least-in-flight + affinity)
┌────────────────▼────────────────────────┐
│ LingmaPool (app/lingma_pool.py) │
│ ├─ inst-0 inst-1 inst-N … │
│ └─ 启动前自动 restore session bundle │
└────────────────┬────────────────────────┘
┌───────────────────────┼───────────────────────┐
▼ ▼ ▼
┌────────────────────┐ ┌────────────────────┐ ┌────────────────────┐
│ LingmaGatewayClient│ │ … │ │ … │
│ (LSP over WS) │ │ │ │ │
│ ├─ Popen (PID管理) │ │ │ │ │
│ ├─ reconnect loop │ │ │ │ │
│ └─ ws://:PORT │ │ │ │ │
└──────────┬─────────┘ └────────────────────┘ └────────────────────┘
│ spawn + ws
┌──────────▼─────────┐
│ Lingma 二进制 │
│ --workDir /… │
└────────────────────┘
```
---
## 一、快速开始
```bash
git clone <repo>
cd lingma-openai-gateway
cp .env.example .env
# 至少填 API_KEYS + LINGMA_USERNAME + LINGMA_PASSWORD或 session bundle
mkdir -p data secrets
docker compose up -d --build
docker compose logs -f # 看到 "Uvicorn running on..." 就 OK
```
冒烟测试:
```bash
API_KEY=$(grep '^API_KEYS=' .env | cut -d= -f2 | cut -d, -f1)
curl -s http://127.0.0.1:8317/healthz
curl -s http://127.0.0.1:8317/v1/models -H "Authorization: Bearer $API_KEY"
curl -s http://127.0.0.1:8317/v1/chat/completions \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"org_auto","messages":[{"role":"user","content":"hi"}]}'
```
---
## 二、配置参考
`.env.example` 是权威说明,这里按主题分组。
### 2.1 核心
| 变量 | 默认 | 说明 |
|---|---|---|
| `HOST` / `PORT` | `0.0.0.0` / `8317` | 网关监听地址与端口 |
| `API_KEYS` | — | Bearer key多个逗号分隔**留空则 /v1/\* 无鉴权**,启动会 warn |
| `LOG_LEVEL` | `INFO` | `DEBUG`/`INFO`/`WARNING`/`ERROR`,日志为结构化 JSON`request_id` |
| `DEFAULT_MODEL` | `org_auto` | 模型无法映射时兜底 |
| `DEFAULT_ASK_MODE` | `chat` | `chat``agent`(传 `model: "agent"` 时自动切) |
| `DEDICATED_DOMAIN_URL` | — | 企业专属域(可空) |
### 2.2 权限分层(生产建议全配)
| 变量 | 默认 | 说明 |
|---|---|---|
| `ADMIN_TOKEN` | — | `/internal/*` 专属 token未配置时 fallback 到 `API_KEYS`(兼容);都为空 → 503 |
| `METRICS_TOKEN` | — | `/metrics` 专属 token未配置时 fallback 到 `API_KEYS` |
| `METRICS_PUBLIC` | `false` | 显式公开 `/metrics`(仅用于私网采集器) |
> `ADMIN_TOKEN` / `METRICS_TOKEN` / `API_KEYS` 三者都为空时,`/metrics` 和 `/internal/*` 会返回 503拒绝裸奔
### 2.3 并发与背压
| 变量 | 默认 | 说明 |
|---|---|---|
| `GATEWAY_MAX_IN_FLIGHT` | `4` | 并发上限;`<=0` 表示不限 |
| `GATEWAY_QUEUE_TIMEOUT_SEC` | `30` | 排队超时;超时直接返回 `429 + Retry-After` |
### 2.4 Lingma 进程
| 变量 | 默认 | 说明 |
|---|---|---|
| `LINGMA_BIN` | `/app/data/bin/Lingma` | 容器内二进制路径 |
| `LINGMA_SOURCE_TYPE` | `marketplace` | `marketplace``vsix` |
| `LINGMA_MARKETPLACE_PUBLISHER` | `Alibaba-Cloud` | Marketplace 发布者 |
| `LINGMA_MARKETPLACE_EXTENSION` | `tongyi-lingma` | Marketplace 扩展名 |
| `LINGMA_VSIX_URL` | 官方地址 | 兜底 VSIX 下载地址 |
| `LINGMA_BOOTSTRAP_ALWAYS` | `true` | 启动时总是尝试刷新二进制 |
| `LINGMA_FORCE_REFRESH` | `false` | 强制忽略本地缓存重新下载 |
| `LINGMA_WORK_DIR` | `/app/data/.lingma/vscode/sharedClientCache` | 登录态/缓存所在目录 |
| `LINGMA_SOCKET_PORT` | `36510` | 单实例模式下的 Lingma WS 端口 |
| `LINGMA_STARTUP_TIMEOUT` | `40` | 启动超时秒 |
| `LINGMA_RPC_TIMEOUT` | `30` | 单次 RPC 超时秒 |
### 2.5 多账号 / 多实例池
| 变量 | 默认 | 说明 |
|---|---|---|
| `LINGMA_ACCOUNTS` | — | `u1:p1,u2:p2` 或 JSON 数组;配置后每个账号 = 一个独立 Lingma 子进程 |
| `LINGMA_INSTANCE_COUNT` | 账号数 | 显式指定实例数;不足账号循环复用并打 warn |
| `LINGMA_USERNAME` / `LINGMA_PASSWORD` | — | 单实例兼容模式(仅 `LINGMA_ACCOUNTS` 为空时生效) |
### 2.6 会话复用KV cache 优化)
| 变量 | 默认 | 说明 |
|---|---|---|
| `SESSION_REUSE_ENABLED` | `true` | 多轮对话命中时只发增量 user 消息 + 复用上游 `sessionId` |
| `SESSION_CACHE_MAX_ENTRIES` | `256` | LRU 容量 |
| `SESSION_CACHE_TTL_SEC` | `1800` | TTL避免命中已回收的 session |
### 2.7 登录态注入(跳过 Playwright
| 变量 | 默认 | 说明 |
|---|---|---|
| `LINGMA_SESSION_BUNDLE` | — | base64 格式的 bundleinline适合短字符串 |
| `LINGMA_SESSION_BUNDLE_FILE` | — | bundle 文件路径(推荐,避免 env 过长) |
### 2.8 自动登录
| 变量 | 默认 | 说明 |
|---|---|---|
| `AUTO_LOGIN_ENABLED` | `true` | 未登录时自动启 Playwright |
| `AUTO_LOGIN_HEADLESS` | `true` | 无头浏览器 |
| `AUTO_LOGIN_TIMEOUT` | `180` | 登录超时秒 |
| `AUTO_LOGIN_MAX_RETRY` | `2` | 登录失败重试次数 |
---
## 三、API 参考
### 3.1 公共(`API_KEYS`
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | `/healthz` | 免鉴权;返回 `ok` / `pool_size` / `pool_ready` / 每实例状态 |
| GET | `/v1/models` | OpenAI 兼容;`id` 是 Lingma 原 key`name` 是可读名 |
| POST | `/v1/chat/completions` | OpenAI 兼容;`stream=true` 走 SSE`model: "agent"` 切 agent 模式 |
| POST | `/v1/messages` | **Anthropic Messages 兼容**`x-api-key``Authorization: Bearer``stream=true` 走 Anthropic 命名事件 SSE |
**chat 请求示例(非流式)**
```bash
curl -s http://127.0.0.1:8317/v1/chat/completions \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
-d '{"model":"dashscope_qmodel","messages":[{"role":"user","content":"你好"}]}'
```
**chat 请求示例(流式 + usage**
```bash
curl -N http://127.0.0.1:8317/v1/chat/completions \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
-d '{
"model":"dashscope_qmodel",
"stream":true,
"stream_options":{"include_usage":true},
"messages":[{"role":"user","content":"介绍一下你自己"}]
}'
```
**Anthropic Messages 示例(非流式)**
```bash
curl -s http://127.0.0.1:8317/v1/messages \
-H "x-api-key: $API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "Content-Type: application/json" \
-d '{
"model":"claude-3-5-sonnet-20241022",
"max_tokens":256,
"system":"你是一个简洁的助手",
"messages":[{"role":"user","content":"你好"}]
}'
```
**Anthropic Messages 示例(流式)**
```bash
curl -N http://127.0.0.1:8317/v1/messages \
-H "x-api-key: $API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "Content-Type: application/json" \
-d '{
"model":"claude-3-5-sonnet-20241022",
"max_tokens":256,
"stream":true,
"messages":[{"role":"user","content":"写一首四行诗"}]
}'
# 返回 message_start / content_block_start / content_block_delta* /
# content_block_stop / message_delta / message_stop
```
说明:
- **模型名兼容**:客户端可以继续传 `claude-3-*` 等名字;未识别的 model 会回退到 `DEFAULT_MODEL` 对应的 Lingma key后端实际仍由 Lingma 提供Qwen 系列)。如需显式选模型,直接传 Lingma key`dashscope_qmodel` 等)。
- **会话复用共享**Anthropic 与 OpenAI 两个端点共用同一 `SessionCache`,只要 API key 相同、对话前缀相同,就会命中同一上游 `sessionId`
- **多模态**`image` 块会被降级为 `[image]` 占位符Lingma 不支持 vision
- **工具事件桥接**:当 Lingma 上游返回 `tool` 事件时,网关会输出为 OpenAI `tool_calls`(含 stream/non-stream和 Anthropic `tool_use`/`tool_result` blocks含 stream/non-stream请求侧 `tools`/`tool_choice``TOOL_FORWARD_ENABLED=true` 时会透传到 Lingma默认关闭
- **鉴权**:优先 `x-api-key`Anthropic 官方 SDK 默认),回退 `Authorization: Bearer`(方便 curl / OpenAI 风格客户端)。
### 3.2 观测(`METRICS_TOKEN` 或 `API_KEYS`
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | `/metrics` | Prometheus 文本;含池每实例 gauge、并发、session cache 命中率、token 计数 |
### 3.3 管理(`ADMIN_TOKEN` 或 fallback 到 `API_KEYS`
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | `/internal/stats` | JSON`stats` + `concurrency` + `pool` + `session_cache` |
| GET | `/internal/auto-login/status` | 每实例登录态与 auto_login 状态 |
| POST | `/internal/auto-login/start?instance=inst-0` | 主动触发某实例登录(可不传,由 pool.pick 选) |
| POST | `/internal/session/export?instance=inst-0` | 把已登录实例的 cache 打包成 base64 bundle |
| GET | `/internal/models/raw?instance=inst-0` | Lingma 原始 `config/queryModels` 响应displayName / isReasoning / isVl 等) |
---
## 四、常用场景
### 4.1 多账号池
```env
LINGMA_ACCOUNTS=user1:pass1,user2:pass2,user3:pass3
# LINGMA_INSTANCE_COUNT=3 # 不写默认=账号数
```
- 每个账号一个独立 Lingma 子进程 + 独立 `workDir``data/.lingma/pool/inst-<i>/`)。
- 路由:同 `user` 字段或同 system prompt 的请求**粘性**分到同一实例;其他按**最小在途**分配。
- 一个实例挂掉不影响整体,`/healthz.pool_ready` 下降,自动重连。
### 4.2 跳过 Playwrightsession bundle
**从已登录实例导出:**
```bash
curl -sS -X POST \
-H "Authorization: Bearer $ADMIN_TOKEN" \
"http://host:port/internal/session/export" \
| jq -r '.bundle_b64' > secrets/lingma-session.b64
chmod 600 secrets/lingma-session.b64
```
**在新部署注入(选一种):**
```env
# 文件注入(推荐)—— 需要在 docker-compose.yml 挂载 secrets 目录
LINGMA_SESSION_BUNDLE_FILE=/secrets/lingma-session.b64
# 或 inline适合小 bundle
LINGMA_SESSION_BUNDLE=H4sIAAAA...
# 多账号 JSON 模式,每账号独立 bundle
LINGMA_ACCOUNTS=[
{"username":"u1","password":"p1","session_bundle_file":"/secrets/u1.b64"},
{"username":"u2","password":"p2","session_bundle":"H4sIAAAA..."}
]
```
**行为保证:**
- 只在目标 `workDir` 空(`cache/user` 不存在或 empty时才注入不会覆盖活跃登录态。
- 注入失败(损坏/权限)自动 fallback 到 Playwright。
- bundle 只含 `cache/{id,user,quota,config.json}` 4 个文件;大小上限 4 MiB实际通常 < 10 KB。
- **bundle 等同于密钥**,落盘需 `chmod 600`,不要进 git。
### 4.3 Prometheus 接入
```yaml
# prometheus scrape_configs 片段
- job_name: lingma-gateway
bearer_token: <METRICS_TOKEN>
static_configs: [{targets: ['host:8317']}]
metrics_path: /metrics
```
关键指标:
| 指标 | 类型 | 意义 |
|---|---|---|
| `gateway_in_flight` / `gateway_queued` | gauge | 并发 / 排队 |
| `gateway_rejected_total` | counter | 背压拒绝429累计 |
| `gateway_pool_instance_ready{name}` | gauge | 每实例是否就绪0/1 |
| `gateway_pool_instance_in_flight{name}` | gauge | 每实例在途 |
| `gateway_session_cache_hit_total` / `_miss_total` | counter | 会话复用命中率原料 |
| `gateway_chat_requests_success` / `_error` | counter | chat 成功率 |
---
## 五、升级注意事项
从旧版本升级时注意**破坏性变更**(每一项都有 fallback默认不会炸但建议显式配置
| 版本 | 变更 | 应对 |
|---|---|---|
| v0.3 | `/metrics` 裸奔时(无 token / 无 key由公开改为 503 | 显式配 `METRICS_PUBLIC=true``METRICS_TOKEN` |
| v0.3 | `/internal/*` 引入 `ADMIN_TOKEN` | 未配置自动 fallback 到 `API_KEYS`,生产建议单独配 |
| v0.2 | 默认会话复用(多轮对话只发增量) | 如果你的客户端裁剪了历史导致语义不连续,设 `SESSION_REUSE_ENABLED=false` |
| v0.2 | Chat 请求走 JSON-RPC `notify` 而非 `request`(修复 30s TTFB bug | 无需行动 |
| v0.2 | 多实例池(`LINGMA_ACCOUNTS` 存在时启用) | 不配则保持单实例行为 |
---
## 六、故障排查FAQ
| 症状 | 排查方向 |
|---|---|
| `/healthz` 返回 `ok=false` / `pool_ready=0` | 查 `docker logs`,关键字 `lingma spawned` / `state ... -> ready`;若卡在 `starting` → Lingma 二进制或 workDir 权限问题 |
| 返回 `401` 且带 `Invalid admin token` | 你用了 `API_KEYS` 去打 `/internal/*`,但服务端已设了 `ADMIN_TOKEN`;用 `ADMIN_TOKEN` 或清空 `ADMIN_TOKEN` |
| 返回 `503 metrics scraping disabled` | 三个 env 全空,按 "权限分层" 章节配任一 |
| 返回 `429 Too many in-flight` | 并发超过 `GATEWAY_MAX_IN_FLIGHT`;增大或客户端加重试 |
| 首 token 延迟 2-3 秒 | Lingma 侧常态;多轮对话第二轮起,会话复用命中后 TTFB 明显降低(看 `gateway_session_cache_hit_total` |
| Playwright 登录失败 | 导出一个已登录 bundle 注入(见 4.2),彻底跳过浏览器 |
| 容器重启后 Lingma 要重新登录 | `data/` 没挂在卷上或被清过;确认 `./data:/app/data` 挂载 + bundle fallback |
| 升级后 `/metrics` 返回 503 | v0.3 默认严格;按表格 5.1 配置 |
`LOG_LEVEL=DEBUG` 可以看到 Lingma 子进程的 stderr 输出,便于定位 native 崩溃。
---
## 七、开发与二开
项目本身是单仓 FastAPI3400 行 Python。推荐阅读路径
1. **先读 [`DESIGN.md`](./DESIGN.md)** —— 架构、模块职责、关键设计决策、二开指引。
2. 再按需读对应模块:
- 想改请求入口 / 路由 → `app/main.py`
- 想加实例调度策略 → `app/lingma_pool.py::pick()`
- 想改 Lingma 通信协议 → `app/lingma_client.py`
- 想扩展会话复用 → `app/session_cache.py` + `main.py` 的 reuse 块
- 想做认证改造 → `app/auth.py` + `main.py::*_guard`
3. 本地跑:`pip install -r requirements.txt && uvicorn app.main:app --reload`
---
## 八、目录结构
```
lingma-openai-gateway/
├── app/ # 主代码(见 DESIGN.md 模块一览)
│ ├── main.py # FastAPI 入口 + 路由
│ ├── lingma_pool.py # N 实例池
│ ├── lingma_client.py # LSP over WS + 子进程管理
│ ├── session_cache.py # 多轮对话 sessionId 复用
│ ├── session_bundle.py # 登录态 export/import
│ ├── concurrency.py # InFlightGuard 背压
│ ├── auto_login.py # Playwright 登录
│ ├── auth.py # Bearer / admin / metrics 三档鉴权
│ ├── config.py # 环境变量 → dataclass
│ ├── model_map.py # 模型 key ↔ displayName
│ ├── openai_schema.py # OpenAI 请求/响应 Pydantic
│ ├── stats.py # StatsCollector + Prometheus
│ ├── logging_config.py # 结构化 JSON log + request_id 上下文
│ └── bootstrap_lingma.py # 启动时下载/提取 Lingma 二进制
├── data/ # 持久化Lingma 二进制 + workDir不进 git
├── secrets/ # 注入的 bundle 等敏感文件,不进 git
├── Dockerfile # Playwright base + HEALTHCHECK
├── docker-compose.yml
├── .env.example # 配置权威文档
├── requirements.txt
├── README.md # 本文件
└── DESIGN.md # 架构与二开手册
```
---
## License
内部使用,按需调整。