feat: Anthropic Messages API compat (/v1/messages)
Add a wire-compatible Anthropic endpoint alongside the existing OpenAI one
so Claude Code / anthropic-sdk / Cursor Agent can hit Lingma directly.
- app/anthropic_schema.py (new): request model + content-block flattener
+ internal-messages adapter + affinity key helper. Handles text / image /
tool_use / tool_result blocks; unknown types degrade gracefully.
- app/auth.py: add require_anthropic_key (x-api-key, Bearer fallback)
and AnthropicAuthError so auth failures render in Anthropic's error
envelope instead of FastAPI's {detail:...} wrapper.
- app/main.py: POST /v1/messages. Shares LingmaPool / SessionCache /
InFlightGuard / StatsCollector with the OpenAI path — same api_key +
same conversation prefix hits the same upstream sessionId across both
protocols (KV cache carries over). Streaming emits the named Anthropic
event sequence (message_start / content_block_start / content_block_delta
/ content_block_stop / message_delta / message_stop). No claude-*
model mapping table: resolve_model's default fallback handles it.
- README.md / DESIGN.md: document the new endpoint, add decision 5.12,
iteration history M5, and a 4.3b streaming flow diagram.
- Bump FastAPI app version to 0.4.0.
Made-with: Cursor
This commit is contained in:
76
DESIGN.md
76
DESIGN.md
@@ -37,6 +37,7 @@
|
||||
### 目标
|
||||
|
||||
1. **OpenAI 协议兼容**:任何支持 OpenAI 的客户端(curl、Cursor、Dify、LangChain、LiteLLM…)不改代码就能接入 Lingma。
|
||||
1b. **Anthropic Messages 协议兼容**:Claude Code / anthropic-sdk-python / Cursor Agent 等只会说 Anthropic 的客户端也能直接接入,和 OpenAI 共享同一 session cache 与池。
|
||||
2. **单节点生产可用**:自用场景下能长期跑 7×24,包含合理的观测、鉴权、背压、错误恢复。
|
||||
3. **最大化利用单账号 / 多账号的配额**:通过多实例池 + 会话复用把后端吞吐做到接近原始 VSCode 插件水平。
|
||||
4. **降低运维成本**:首次登录成功后,可以导出一份 bundle 永久复用,彻底摆脱浏览器自动化的不稳定性。
|
||||
@@ -101,6 +102,7 @@
|
||||
| `config.py` | 178 | env → `Settings` dataclass;`LINGMA_ACCOUNTS` 多格式解析;bundle 字段归一化 | `main.py` | — |
|
||||
| `model_map.py` | 84 | Lingma 模型 `key ↔ displayName` 双向映射;请求 `model` 解析(`id` 或 `name` 都认) | `main.py` | — |
|
||||
| `openai_schema.py` | 91 | OpenAI 请求/响应 Pydantic;多模态内容 `flatten_content` 降级 | `main.py`, `session_cache.py` | — |
|
||||
| `anthropic_schema.py` | ~140 | Anthropic Messages 请求 Pydantic;content blocks `flatten_anthropic_content`;`anthropic_to_internal_messages` 归一化到内部消息;`affinity_key_for_anthropic` 选池键 | `main.py` | — |
|
||||
| `stats.py` | 85 | 请求次数 / token 估算 / Prometheus 文本 | `main.py` | — |
|
||||
| `logging_config.py` | 56 | 结构化 JSON logger;`request_id` 通过 `ContextVar` 注入每行 | 所有模块 | — |
|
||||
| `bootstrap_lingma.py` | 199 | 启动时从 Marketplace / VSIX 提取 Lingma 二进制到 `data/bin/` | 容器启动脚本 | — |
|
||||
@@ -274,6 +276,58 @@ async def event_stream():
|
||||
1. `ticket_transferred=True` 一旦设成 true,外层 `finally` 就不会 release ticket;责任转交给 `event_stream()` 的 finally。否则会 release 两次(虽然幂等,但会把 in_flight 计成 -1)。
|
||||
2. `chat_stream` 走的是 JSON-RPC **notify** 而非 request。早期版本用 request 会等 30s 才下第一个字节(见决策 5.1)。
|
||||
|
||||
### 4.3b 流式 Anthropic Messages(/v1/messages)
|
||||
|
||||
输入输出协议都不同于 OpenAI,但中间层完全复用:
|
||||
|
||||
```
|
||||
client ──► POST /v1/messages (x-api-key / Bearer)
|
||||
│
|
||||
▼
|
||||
require_anthropic_key # x-api-key 优先;缺了 → AnthropicAuthError
|
||||
│
|
||||
anthropic_to_internal_messages(req) # system → role="system";content blocks flatten
|
||||
│ # 结果与 OpenAI 路径完全同构 (role/content dict)
|
||||
▼
|
||||
session_cache lookup / affinity pick # 与 OpenAI 共享同一 SessionCache 实例
|
||||
│ # → 同一用户切协议不丢 KV cache
|
||||
▼
|
||||
pool.pick(affinity) + ensure_logged_in
|
||||
│
|
||||
▼
|
||||
resolve_model("claude-3-5-sonnet-*") # 兜底到 default_model
|
||||
│
|
||||
▼
|
||||
chat_guard.try_acquire() # 与 OpenAI 路径同一 in-flight 池
|
||||
│
|
||||
▼ stream=true
|
||||
StreamingResponse(event_stream())
|
||||
│
|
||||
├─ event: message_start ← 一次性:id / model / usage.input_tokens
|
||||
├─ event: content_block_start ← index=0, type=text
|
||||
├─ event: content_block_delta ← 每片 chunk 包一次
|
||||
│ ...
|
||||
├─ event: content_block_stop
|
||||
├─ event: message_delta ← stop_reason (+ output_tokens)
|
||||
└─ event: message_stop ← 终止,无 [DONE]
|
||||
│
|
||||
▼ finally
|
||||
session_cache.put(write_key, upstream_sessionId, inst.name) # 仅 success
|
||||
ticket.release() + inst.in_flight--
|
||||
```
|
||||
|
||||
与 OpenAI 路径的差异点:
|
||||
|
||||
| 环节 | OpenAI | Anthropic |
|
||||
|---|---|---|
|
||||
| 鉴权 | `Authorization: Bearer` | `x-api-key`(fallback Bearer)|
|
||||
| 系统消息 | messages 数组里的 `role:"system"` | 顶层 `system` 字段 |
|
||||
| 内容结构 | `str` 或 `[{type:"text"|"image_url"...}]` | `str` 或 `[{type:"text"|"image"|"tool_use"|"tool_result"...}]` |
|
||||
| 流式帧 | `data: {delta:{content:"..."}}` + `[DONE]` | 命名事件序列 `message_start / content_block_* / message_delta / message_stop` |
|
||||
| usage 语义 | `prompt_tokens / completion_tokens` | `input_tokens / output_tokens` |
|
||||
| 错误 envelope | `{"error":{...}}` | `{"type":"error","error":{...}}` |
|
||||
| finish 语义 | `finish_reason: "stop"\|"length"` | `stop_reason: "end_turn"\|"max_tokens"` |
|
||||
|
||||
### 4.4 Lingma 子进程与 LSP 通信
|
||||
|
||||
```
|
||||
@@ -520,6 +574,16 @@ FastAPI `lifespan` 退出 → `pool.close()` → 每个 `client.close()` → 进
|
||||
- **方案**:`client._proc` + `client._terminate_proc()`。pool 只负责 `client.start()` / `client.close()` 的调度,进程操作封装在 client 内部。
|
||||
- **权衡**:client 文件变长,但边界清晰——pool 只看状态和在途数,具体进程是 client 的事。
|
||||
|
||||
### 5.12 Anthropic Messages 端点独立编排而非内部转发
|
||||
|
||||
- **问题**:既要兼容 Anthropic API,又不能把 `v1_chat_completions` 的编排路径搞成大杂烩。
|
||||
- **方案**:单独写一个 `v1_messages` 端点,前半段(auth / 归一化到内部 messages / affinity / session cache lookup / instance pick / prompt 构造 / ticket 获取)与 OpenAI 端点结构对齐但各自实现;后半段(SSE 事件生成 / 响应包装)按 Anthropic 格式输出。
|
||||
- **共享的下沉层**:`LingmaPool` / `SessionCache` / `InFlightGuard` / `StatsCollector` / `LingmaGatewayClient.chat_stream|chat_complete` / `resolve_model`。
|
||||
- **为何不用一层统一抽象**:两端的输入/输出对象形状差异足够大(system 位置、content 类型、SSE 事件名、错误 envelope),抽象出来的中间类型反而掩盖差异、增加维护成本。当前重复代码约 150 行,但每条分支读起来直接对应 wire 协议,调试、改协议时都是线性阅读。
|
||||
- **会话复用跨协议**:`session_cache.build_key(api_key, messages)` 在两端都接收归一化后的 `{role, content}` 列表——同一用户从 OpenAI 切 Anthropic(只要对话前缀一致)可直接命中同一上游 `sessionId`,等于白送 KV cache。
|
||||
- **错误路径**:`AnthropicAuthError` 专用异常 + `@app.exception_handler` 渲染 Anthropic envelope;端点内部其他错误(HTTPException、backpressure)用 `_anthropic_error()` helper 直接返 `JSONResponse`,绕过 FastAPI 默认 `{"detail":...}` 包装。
|
||||
- **模型名**:不维护 `claude-* → dashscope_*` 映射表。`resolve_model` 的末位兜底(default_model / first available)会把所有陌生 id 退回到实际可用的 Lingma key,Anthropic 客户端继续传 `claude-3-5-sonnet-*` 即可工作。
|
||||
|
||||
---
|
||||
|
||||
## 6. 扩展指引(要做 X 改哪里)
|
||||
@@ -527,6 +591,7 @@ FastAPI `lifespan` 退出 → `pool.close()` → 每个 `client.close()` → 进
|
||||
| 需求 | 改哪些文件 | 关键入口 |
|
||||
|---|---|---|
|
||||
| 加一个新的 OpenAI 端点(如 embeddings) | `main.py`, `openai_schema.py` | 仿照 `v1_models` 加 `@app.post("/v1/embeddings", dependencies=[Depends(auth_guard)])` |
|
||||
| 扩展 Anthropic 端点(如 count_tokens / tool_use 贯通) | `main.py::v1_messages`, `anthropic_schema.py` | count_tokens 只读:复用 `estimate_tokens`;tool_use 需要 Lingma 上游支持,payload 转发点在 `chat_stream` / `chat_complete` |
|
||||
| 加一种新的实例调度策略(如加权轮询) | `lingma_pool.py::pick()` | 当前是 affinity → least-in-flight → round-robin |
|
||||
| 改认证为 JWT / OAuth | `auth.py` | 三个 `require_*` 函数是全部入口;`main.py` 里只有 `*_guard` 代理 |
|
||||
| 增加限流(按 api_key 配额) | `concurrency.py` 加 `PerKeyGuard`;`main.py` 在 `chat_guard.try_acquire()` 后再来一层 | 注意 ticket 释放顺序(内层先释放) |
|
||||
@@ -604,6 +669,17 @@ uvicorn app.main:app --reload --port 8317
|
||||
|
||||
收益:单轮没有显著改变(推理仍然花最多时间),但第 2 轮起 TTFB 降 40%~60%,视 prompt 长度。
|
||||
|
||||
### M5 — Anthropic Messages 兼容
|
||||
|
||||
- **场景**:Claude Code / Cursor Agent / anthropic-sdk-python / 各种 agent 框架只会说 Anthropic 协议。
|
||||
- **改动**:
|
||||
- 新增 `anthropic_schema.py`:`AnthropicMessagesRequest` + `anthropic_to_internal_messages` + `flatten_anthropic_content` + `affinity_key_for_anthropic`。
|
||||
- `auth.py` 新增 `require_anthropic_key`(`x-api-key` 优先,Bearer 回退)+ `AnthropicAuthError`。
|
||||
- `main.py` 新增 `/v1/messages` 端点:复用 `LingmaPool` / `SessionCache` / `InFlightGuard`;流式按 `message_start / content_block_start|delta|stop / message_delta / message_stop` Anthropic SSE 协议输出;错误 envelope 改写成 `{"type":"error","error":{...}}`。
|
||||
- `@app.exception_handler(AnthropicAuthError)` 渲染 Anthropic 错误 wire 格式。
|
||||
- **关键设计**:两端共享同一 `SessionCache`,同一 api_key 下的会话前缀哈希一致 → 跨协议命中同一上游 `sessionId`。详见 §5.12。
|
||||
- **模型名**:不维护 `claude-* → dashscope_*` 映射表,靠 `resolve_model` 末位兜底。
|
||||
|
||||
### M4 — 生产硬化包(commit `2febc37`)
|
||||
|
||||
用户代号"选项 A"。
|
||||
|
||||
Reference in New Issue
Block a user