diff --git a/AGENTS.md b/AGENTS.md index dd72710..97221a8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,7 +6,7 @@ ## 1. Project overview -- **What:** OpenAI-compatible gateway in front of a local [OpenCode](https://opencode.ai) runtime. Endpoints: `GET /health`, `GET /health/details`, `GET /metrics`, `GET /v1/models`, `POST /v1/chat/completions`, `POST /v1/responses`, `POST /v1/messages` (Anthropic-compatible). +- **What:** OpenAI-, Anthropic-, and Gemini-compatible gateway in front of a local [OpenCode](https://opencode.ai) runtime. Endpoints: `GET /health`, `GET /health/details`, `GET /metrics`, `GET /v1/models`, `POST /v1/chat/completions`, `POST /v1/responses`, `POST /v1/messages` (Anthropic-compatible), `POST /v1beta/interactions` (alias `POST /v1/interactions`, Gemini-compatible thin layer). - **Production entry:** `index.ts` (env > file > default merge, `index.ts:113-199`, then `startProxy`, `index.ts:276`). Never import `index.ts` from tests — it boots the server on import. - **Library entry:** `startProxy` / `createApp` in `src/proxy.ts` (`startProxy` at `src/proxy.ts:860-894`); test/library config builder is `buildProxyConfig` in `src/config/proxy-config.ts:97-223`. - **Runtime matrix:** local dev `tsx watch index.ts` vs prod `node dist/index.js` vs Docker (multi-stage `Dockerfile`, backend owned by `entrypoint.sh`). `index.ts:76-80` probes three `config.json` locations to cover the `index.ts` ↔ `dist/index.js` directory shift. @@ -31,7 +31,7 @@ index.ts # prod bootstrap + config merge (300 lines) src/proxy.ts # createApp/startProxy (894 lines; god file, see §9) src/config/proxy-config.ts # buildProxyConfig, normalizeBool, resolveDisableTools -src/routes/ # chat.ts (872) / responses.ts (938) / messages.ts (563) / system.ts +src/routes/ # chat.ts (953) / responses.ts (1144) / messages.ts (635) / interactions.ts (439) / system.ts src/tool-runtime/ # contracts / registry / router / parser (945) / validator / policy src/backend/manager.ts # backend lifecycle + request lock/queue src/stream/collector.ts # prompt/poll/collect SSE pipeline @@ -108,9 +108,9 @@ config.json.example / .env.example # examples only, never real secrets ## 9. Architecture notes (maintainability / extensibility) -- `src/proxy.ts:createApp` is a god closure (~800 lines) assembling `AppContext` (50+ fields, `src/types/context.ts`); the three routes (`chat`/`responses`/`messages`, ~2400 lines combined) duplicate the preflight → stream/non-stream → retry → usage/cleanup template. Prefer **pure moves + thin wrappers** over behavior changes. +- `src/proxy.ts:createApp` is a god closure (~800 lines) assembling `AppContext` (50+ fields, `src/types/context.ts`); the four routes (`chat`/`responses`/`messages`/`interactions`, ~3200 lines combined) duplicate the preflight → stream/non-stream → retry → usage/cleanup template. Prefer **pure moves + thin wrappers** over behavior changes. - Shared unknown-guards (`asRecord`, `toErrorMessage`) live in `src/utils/guards.ts` — import from there, do not add new copies. Exception: `src/routes/system.ts` and `src/stream/collector.ts` keep their own narrower `toErrorMessage` variant (see `guards.ts` header); do not "unify" it. -- Pure layers to preserve: `src/retry/policy.ts`, `src/converters/anthropic.ts`, `src/errors/upstream.ts` (no Express/SDK deps). Route error exits must keep their wire shapes: chat non-stream JSON, responses-stream `response.failed` SSE + `[DONE]`, messages-stream `error` event without `[DONE]`. +- Pure layers to preserve: `src/retry/policy.ts`, `src/converters/anthropic.ts`, `src/errors/upstream.ts` (no Express/SDK deps). Route error exits must keep their wire shapes: chat non-stream JSON, responses-stream `response.failed` SSE + `[DONE]`, messages-stream `error` event without `[DONE]`, interactions-stream `interaction.completed` / `error` event without `[DONE]`. - High-risk no-touch list without explicit approval + regression tests: timeout/retry constants, `transformUpstreamError` mappings, `isTransientUpstreamError` matchers, parser ambiguity policy, prompt guard text/order, model alias rules, policy evaluation order, backend spawn/jail/HOME isolation, collector `finish==='tool'`/idle-exemption logic, bool-fallthrough semantics. ## 10. Commit convention & PR checklist diff --git a/CHANGELOG.md b/CHANGELOG.md index dc6d2c6..028c6cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **Interactions API (Gemini-compatible thin layer)**: new `POST /v1beta/interactions` (alias `POST /v1/interactions`) mapping text + `google_search` grounding onto the Responses pipeline, with `system_instruction` / `previous_interaction_id` / `store:false` (ephemeral) / SSE (`interaction.created` → `step.delta` → `interaction.completed`, no `[DONE]`). 新增 `src/routes/interactions.ts` 与 `tests/interactions.test.js`. Client-executed function tools are rejected with 400 (use `/v1/responses` for those). +- **Multi-key auth**: `API_KEY` merges with `OPENCODE_API_KEYS` / `API_KEYS` (any match passes via `timingSafeEqual`; `Bearer` or `x-api-key`; empty means no auth). 新增 `src/auth/keys.ts` 与 `tests/auth-multikey.test.js`. +- **Free-limit fallback proxy pool**: direct-only until a 429 free-limit error (`FreeUsageLimitError` / `GoUsageLimitError`) engages `OPENCODE_UPSTREAM_PROXIES` (`failover-rr`/`round-robin` alias / `random`, cooldown, socks-first, loopback bypass); stream paths degrade to poll while engaged; state exposed via `/health/details` + `/metrics`. 新增 `src/upstream-proxy/` 与 `tests/proxy-fallback*.test.js`. +- **Server-side web_search grounding**: `/v1/responses` `tools: [{type: "web_search"}]` (also `web_search_preview` / versioned `web_search_*` / `google_search`) drives the opencode built-in `websearch` and returns `web_search_call` + honest `url_citation` annotations only; `web_search` on chat/messages is rejected with 400 + pointer. 新增 `src/search/grounding.ts` 与 `tests/responses-websearch.test.js` / `tests/hosted-search-routing.test.js`. - **Anthropic Messages API**:新增 `POST /v1/messages`(`max_tokens` 必填,支持 `system`/`tools(input_schema)`/`tool_choice{auto,any,tool,none}`/`thinking→reasoning`/`image`;`tool_use.id` 原样往返;非流式回 `message` 对象,流式回 `message_start/content_block_*/message_delta/message_stop` 无 `[DONE]`;认证同时支持 `x-api-key`;CORS 放行 `x-api-key/anthropic-version`)。新增 `src/converters/anthropic.js` 纯函数转换层与 `tests/messages-anthropic.test.js`(9 例)。附带修复 `EXTERNAL_TOOL_PREFIX` 缺 import 的 latent `ReferenceError`。 - **官方对齐重试策略**:移植上游 `session/retry.ts`(`2s×2ⁿ⁻¹` 指数退避 +25% jitter;优先 `retry-after-ms`/`retry-after`,header 等待 clamp 30s;`5xx` 强制、`isRetryable`、`ContextOverflow` 永不)。重试次数 `n` 经 `OPENCODE_PROXY_RETRY_MAX_RETRIES` 控制(默认 3,总尝试 1+n,上限对齐官方 5)。chat 双路由与 messages 非串流改用新退避;responses 非串流新增同结构重试迴圈(仅无产出且 transient 时;responses/messages 串流保持单次尝试)。三处非串流的 prompt/poll 抛错同样走 transient 重试路由(传输层 throw 不再直接逃逸);真鉴权失败(invalid api key/unauthorized/authentication failed)立即透出不再烧退避。新增 `src/retry/policy.js` 与 `tests/retry-policy.test.js`(11 例)及接线/回归测试 14 例。 diff --git a/README.md b/README.md index 6544bf5..62b6d05 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ > > 📖 [Docs](./docs/README.md) | 🚀 [Quick Start](#quick-start) | 🐛 [Issues](https://github.com/samson910022/OpenCode2API/issues) -Turn a local [OpenCode](https://opencode.ai) runtime into an OpenAI- and Anthropic-compatible API gateway. Use free models (Big Pickle, Ling, MiMo, Muse Spark, Nemotron) from any OpenAI or Anthropic client — the free lineup rotates, so query `/v1/models` for the live list. +Turn a local [OpenCode](https://opencode.ai) runtime into an OpenAI-, Anthropic-, and Gemini-compatible API gateway. Use free models (Big Pickle, Ling, MiMo, Muse Spark, Nemotron) from any OpenAI, Anthropic, or Gemini client — the free lineup rotates, so query `/v1/models` for the live list. --- @@ -20,7 +20,11 @@ Turn a local [OpenCode](https://opencode.ai) runtime into an OpenAI- and Anthrop |:-----|:-----| | 🟢 **OpenAI compatible** | `/v1/models`, `/v1/chat/completions`, `/v1/responses` | | 🟣 **Anthropic compatible** | `/v1/messages` (with `tool_use` / `thinking` / SSE streaming) | -| 📡 **Streaming** | Full SSE streaming for Chat Completions, Responses, and Messages APIs | +| 🔁 **Gemini compatible (thin)** | `POST /v1beta/interactions` (alias `POST /v1/interactions`, text + `google_search` grounding) | +| 🌐 **Server-side web search** | `/v1/responses` `tools: [{type: "web_search"}]` drives opencode websearch, returns `web_search_call` + honest `url_citation` | +| 🔑 **Multi-key auth** | `API_KEY` + `OPENCODE_API_KEYS` / `API_KEYS` merge; `Bearer` or `x-api-key`, any match passes; empty means no auth | +| 🔀 **Free-limit fallback proxy** | Direct-only until a 429 free-limit error engages `OPENCODE_UPSTREAM_PROXIES` (`failover-rr`, cooldown, loopback bypass) | +| 📡 **Streaming** | Full SSE streaming for Chat Completions, Responses, Messages, and Interactions APIs | | 🧠 **Reasoning control** | Supports `reasoning_effort` and `reasoning: { "effort": "high" }` | | 🐳 **Docker deploy** | One-command deploy, auto-starts the OpenCode backend | | 🛡️ **Tool safety** | Tool calling disabled by default | @@ -173,6 +177,36 @@ curl -X POST http://127.0.0.1:10000/v1/messages \ > You may use `x-api-key: YOUR_API_KEY` instead of `Authorization: Bearer`; `max_tokens` is required; on stream the API returns `message_start/content_block_start/content_block_delta/content_block_stop/message_delta/message_stop` (no `[DONE]`). +### Responses API + web_search grounding + +```bash +curl -X POST http://127.0.0.1:10000/v1/responses \ + -H "Authorization: Bearer YOUR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "opencode/muse-spark-1.3-contributor-free", + "input": "What is the latest PostgreSQL release?", + "tools": [{"type": "web_search"}] + }' +``` + +> `web_search` (also `web_search_preview` / `web_search_*` / `google_search`) is an explicit grant: the proxy drives the opencode built-in `websearch` and returns `web_search_call` + `url_citation` annotations only for sources actually cited (no fabrication). `web_search` on chat/messages is rejected with 400 + a pointer to this endpoint. + +### Interactions API (Gemini-compatible thin layer) + +```bash +curl -X POST http://127.0.0.1:10000/v1beta/interactions \ + -H "Authorization: Bearer YOUR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "opencode/muse-spark-1.3-contributor-free", + "input": "What is the latest PostgreSQL release?", + "tools": [{"type": "google_search"}] + }' +``` + +> Alias `POST /v1/interactions`; supports `system_instruction`, `previous_interaction_id`, `store: false` (ephemeral), and SSE (`interaction.created` / `step.delta` / `interaction.completed`, no `[DONE]`). Client-executed function tools are rejected with 400 — use `/v1/responses` for those. + --- ## 📦 Deployment Modes @@ -193,6 +227,7 @@ curl -X POST http://127.0.0.1:10000/v1/messages \ | `PORT` / `OPENCODE_PROXY_PORT` | `10000` | Proxy listen port | | `OPENCODE_SERVER_PORT` | `10001` | OpenCode backend port | | `API_KEY` | - | Bearer token secret | +| `API_KEYS` / `OPENCODE_API_KEYS` | `(none)` | Extra client keys, comma-separated; merged with `API_KEY`, any match passes; empty falls back to no auth | | `BIND_HOST` | `0.0.0.0` | Bind address | | `DISABLE_TOOLS` | `true` | Disable OpenCode tool calling | | `OPENCODE_EXTERNAL_TOOLS_MODE` | `proxy-bridge` | External tool bridge mode; only `proxy-bridge` is supported | @@ -213,6 +248,10 @@ curl -X POST http://127.0.0.1:10000/v1/messages \ | `OPENCODE_PROXY_CLEANUP_MAX_AGE_MS` | `86400000` | Max retention (ms) | | `OPENCODE_PROXY_REQUEST_TIMEOUT_MS` | `180000` | Request timeout (ms) | | `OPENCODE_PROXY_RETRY_MAX_RETRIES` | `3` | Retries after the first attempt (0-5, total attempts 1+n; exponential backoff+jitter, honors `retry-after`) | +| `OPENCODE_UPSTREAM_PROXIES` | `(none)` | Fallback proxy URLs, comma-separated (`socks5://` preferred, `http(s)://` ok; file key `UPSTREAM_PROXIES`) | +| `OPENCODE_UPSTREAM_PROXY_STRATEGY` | `failover-rr` | `failover-rr` / `round-robin` (aliases) / `random`; rotates on consecutive free-limit 429s | +| `OPENCODE_UPSTREAM_PROXY_COOLDOWN_MS` | `300000` | How long an engaged proxy sticks before returning to direct (ms) | +| `OPENCODE_UPSTREAM_PROXY_NO_PROXY` | `localhost,127.0.0.1,::1` | Target hosts that never use the proxy (backend `127.0.0.1` always direct) | | `OPENCODE_SERVER_URL` | `http://127.0.0.1:10001` | OpenCode backend address | | `OPENCODE_SERVER_PASSWORD` | - | OpenCode backend password | | `OPENCODE_PATH` | `opencode` | OpenCode binary path | @@ -280,6 +319,8 @@ OPENCODE_PROXY_AUTO_CLEANUP_CONVERSATIONS=true | `POST` | `/v1/chat/completions` | Chat Completions API | | `POST` | `/v1/responses` | Responses API | | `POST` | `/v1/messages` | Anthropic Messages API (`max_tokens` required, supports `x-api-key`) | +| `POST` | `/v1beta/interactions` | Gemini-compatible thin layer (text + `google_search` grounding, `previous_interaction_id`, `store`, SSE without `[DONE]`) | +| `POST` | `/v1/interactions` | Alias of `/v1beta/interactions` | ### Model name formats diff --git a/README.zh-CN.md b/README.zh-CN.md index dcbbfc9..bf41337 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -10,7 +10,7 @@ > > 📖 [文档](./docs/README.zh-CN.md)([English index](./docs/README.md)) | 🚀 [快速开始](#-快速开始) | 🐛 [Issues](https://github.com/samson910022/OpenCode2API/issues) -将本地 [OpenCode](https://opencode.ai) 运行时转换为 OpenAI 与 Anthropic 兼容 API 网关。在任何 OpenAI 或 Anthropic 客户端中使用免费模型(Big Pickle、Ling、MiMo、Muse Spark、Nemotron)——免费阵容会轮换,以 `/v1/models` 实时列表为准。 +将本地 [OpenCode](https://opencode.ai) 运行时转换为 OpenAI、Anthropic 与 Gemini 兼容 API 网关。在任何 OpenAI、Anthropic 或 Gemini 客户端中使用免费模型(Big Pickle、Ling、MiMo、Muse Spark、Nemotron)——免费阵容会轮换,以 `/v1/models` 实时列表为准。 --- @@ -20,7 +20,11 @@ |:-----|:-----| | 🟢 **OpenAI 兼容** | `/v1/models`, `/v1/chat/completions`, `/v1/responses` | | 🟣 **Anthropic 兼容** | `/v1/messages`(含 `tool_use` / `thinking` / SSE 流式) | -| 📡 **流式输出** | Chat Completions、Responses 与 Messages API 的完整 SSE 流式支持 | +| 🔁 **Gemini 兼容(薄层)** | `POST /v1beta/interactions`(别名 `POST /v1/interactions`,文本 + `google_search` 联网) | +| 🌐 **服务端联网搜索** | `/v1/responses` 的 `tools: [{type: "web_search"}]` 驱动 opencode websearch,返回 `web_search_call` + 诚实 `url_citation` | +| 🔑 **多 Key 认证** | `API_KEY` 与 `OPENCODE_API_KEYS` / `API_KEYS` 合并;`Bearer` 或 `x-api-key` 任一通过;为空回退免认证 | +| 🔀 **免费限流 fallback 代理** | 默认直连,仅 429 免费限流错误时启用 `OPENCODE_UPSTREAM_PROXIES`(`failover-rr`、cooldown、回环 bypass) | +| 📡 **流式输出** | Chat Completions、Responses、Messages 与 Interactions API 的完整 SSE 流式支持 | | 🧠 **推理控制** | 支持 `reasoning_effort` 和 `reasoning: { "effort": "high" }` | | 🐳 **Docker 部署** | 一键部署,自动启动 OpenCode 后端 | | 🛡️ **工具安全** | 默认禁用工具调用 | @@ -173,6 +177,36 @@ curl -X POST http://127.0.0.1:10000/v1/messages \ > 也可用 `x-api-key: YOUR_API_KEY` 代替 `Authorization: Bearer`;`max_tokens` 必填;流式时返回 `message_start/content_block_start/content_block_delta/content_block_stop/message_delta/message_stop`(无 `[DONE]`)。 +### Responses API + 联网搜索 + +```bash +curl -X POST http://127.0.0.1:10000/v1/responses \ + -H "Authorization: Bearer YOUR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "opencode/muse-spark-1.3-contributor-free", + "input": "PostgreSQL 最新版本是多少?", + "tools": [{"type": "web_search"}] + }' +``` + +> `web_search`(亦接受 `web_search_preview` / `web_search_*` / `google_search`)是显式授权:代理驱动 opencode 内置 `websearch`,返回 `web_search_call` + 仅标注实际引用来源的 `url_citation`(无编造)。chat/messages 传入 `web_search` 会 400 并指引到此端点。 + +### Interactions API(Gemini 兼容薄层) + +```bash +curl -X POST http://127.0.0.1:10000/v1beta/interactions \ + -H "Authorization: Bearer YOUR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "opencode/muse-spark-1.3-contributor-free", + "input": "PostgreSQL 最新版本是多少?", + "tools": [{"type": "google_search"}] + }' +``` + +> 别名 `POST /v1/interactions`;支持 `system_instruction`、`previous_interaction_id`、`store: false`(不持久化)与 SSE(`interaction.created` / `step.delta` / `interaction.completed`,无 `[DONE]`)。客户端执行的 function 工具会被 400 拒绝——请改用 `/v1/responses`。 + --- ## 📦 部署方式 @@ -193,6 +227,7 @@ curl -X POST http://127.0.0.1:10000/v1/messages \ | `PORT` / `OPENCODE_PROXY_PORT` | `10000` | 代理服务端口 | | `OPENCODE_SERVER_PORT` | `10001` | OpenCode 后端服务端口 | | `API_KEY` | - | Bearer Token 认证密钥 | +| `API_KEYS` / `OPENCODE_API_KEYS` | `(none)` | 额外客户端 keys,逗号分隔;与 `API_KEY` 合并,任一通过;为空回退免认证 | | `BIND_HOST` | `0.0.0.0` | 绑定地址 | | `DISABLE_TOOLS` | `true` | 禁用 OpenCode 工具调用 | | `OPENCODE_EXTERNAL_TOOLS_MODE` | `proxy-bridge` | 外部工具桥接模式;当前仅支持 `proxy-bridge` | @@ -213,6 +248,10 @@ curl -X POST http://127.0.0.1:10000/v1/messages \ | `OPENCODE_PROXY_CLEANUP_MAX_AGE_MS` | `86400000` | 最大存储时间 (毫秒) | | `OPENCODE_PROXY_REQUEST_TIMEOUT_MS` | `180000` | 请求超时时间 (毫秒) | | `OPENCODE_PROXY_RETRY_MAX_RETRIES` | `3` | 首次失败后重试次数 (0-5,总尝试 1+n;退避指数+jitter 并优先 `retry-after`) | +| `OPENCODE_UPSTREAM_PROXIES` | `(none)` | Fallback 代理 URL,逗号分隔(`socks5://` 优先,`http(s)://` 可用;`config.json` 用短键 `UPSTREAM_PROXIES`) | +| `OPENCODE_UPSTREAM_PROXY_STRATEGY` | `failover-rr` | `failover-rr` / `round-robin`(别名)/ `random`;连续免费限流即轮换下一个 | +| `OPENCODE_UPSTREAM_PROXY_COOLDOWN_MS` | `300000` | 代理粘滞时长(毫秒),到期回直连 | +| `OPENCODE_UPSTREAM_PROXY_NO_PROXY` | `localhost,127.0.0.1,::1` | 永不走代理的目标 host(后端 `127.0.0.1` 恒直连) | | `OPENCODE_SERVER_URL` | `http://127.0.0.1:10001` | OpenCode 后端地址 | | `OPENCODE_SERVER_PASSWORD` | - | OpenCode 后端密码 | | `OPENCODE_PATH` | `opencode` | OpenCode 可执行文件路径 | @@ -281,6 +320,8 @@ OPENCODE_PROXY_AUTO_CLEANUP_CONVERSATIONS=true | `POST` | `/v1/chat/completions` | Chat Completions API | | `POST` | `/v1/responses` | Responses API | | `POST` | `/v1/messages` | Anthropic Messages API(`max_tokens` 必填,支持 `x-api-key`) | +| `POST` | `/v1beta/interactions` | Gemini 兼容薄层(文本 + `google_search` 联网、`previous_interaction_id`、`store`、SSE 无 `[DONE]`) | +| `POST` | `/v1/interactions` | `/v1beta/interactions` 的别名 | ### 模型名称格式 diff --git a/docs/api-reference.md b/docs/api-reference.md index 9c7d80b..902eccb 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -41,7 +41,7 @@ GET /health ```json { "status": "ok", - "timestamp": "2024-01-01T00:00:00.000Z" + "proxy": true } ``` @@ -53,6 +53,8 @@ GET /health GET /v1/models ``` +> 需要认证:当配置了 `API_KEY` / `API_KEYS` / `OPENCODE_API_KEYS` 任一时,需 `Authorization: Bearer` 或 `x-api-key`;均未配置时免认证。 + **响应示例:** ```json @@ -216,7 +218,7 @@ curl -X POST http://127.0.0.1:10000/v1/responses \ }' ``` -`tools: [{"type": "web_search"}]`(亦接受 `web_search_preview` / `google_search`)是显式授权:代理将其从外部 function 注册表剔除,转驱动 opencode 内置 `websearch`(需模型 provider 为 `opencode`/`opencode-go`,否则需 `OPENCODE_ENABLE_EXA=1`),并在输出中返回 `type: "web_search_call"` 项(含真实执行的 `action.query`)与 `url_citation` 引用(仅标注答案中实际出现的来源 URL,无编造)。流式模式会发送 `response.web_search_call.searching/completed` 事件,最终 `response.completed` 携带完整引用。 +`tools: [{"type": "web_search"}]`(亦接受 `web_search_preview` / `web_search_*` 版本化 / `google_search`)是显式授权:代理将其从外部 function 注册表剔除,转驱动 opencode 内置 `websearch`(需后端暴露该工具;可用性取决于模型/后端),并在输出中返回 `type: "web_search_call"` 项(含真实执行的 `action.query`)与 `url_citation` 引用(仅标注答案中实际出现的来源 URL,无编造)。流式模式会发送 `response.web_search_call.searching/completed` 事件,最终 `response.completed` 携带完整引用。注意:`/v1beta|/v1/interactions` 仅接受字面 `google_search` / `web_search` / `web_search_preview`(版本化 `web_search_YYYYMMDD` 在此会 400)。 ### 🔁 Interactions API(Gemini 兼容薄层) @@ -394,12 +396,24 @@ curl -X POST http://127.0.0.1:10000/v1/messages \ ### 401 Unauthorized +chat / models / responses / interactions(OpenAI 形状): + ```json { "error": { - "message": "Invalid API key", - "type": "invalid_request_error", - "code": "invalid_api_key" + "message": "Unauthorized" + } +} +``` + +messages(Anthropic 形状): + +```json +{ + "type": "error", + "error": { + "type": "authentication_error", + "message": "Unauthorized" } } ``` @@ -427,3 +441,5 @@ curl -X POST http://127.0.0.1:10000/v1/messages \ } } ``` + +> 后端偶发 `500 {"message":"Aborted","type":"internal_error","code":"MessageAbortedError"}` 为上游 session 瞬断(非代理超时;代理超时为 504 `Request timeout`),直接重试同一请求即可;`isTransient` 不覆盖该签名,故不自动重试。 diff --git a/docs/architecture.md b/docs/architecture.md index c9a9b5d..c20f4aa 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -20,6 +20,7 @@ index.ts (env > file > default merge + bootstrap) ├─> registerSystemRoutes [src/routes/system.ts] (models / health / metrics, no lock) ├─> registerChatRoutes [src/routes/chat.ts] (POST /v1/chat/completions, request lock) ├─> registerResponsesRoutes[src/routes/responses.ts] (POST /v1/responses, cancel race) + ├─> registerInteractionsRoutes[src/routes/interactions.ts] (POST /v1beta|/v1/interactions, Gemini thin layer) └─> registerMessagesRoutes [src/routes/messages.ts] (POST /v1/messages, Anthropic converters first) ``` @@ -38,7 +39,7 @@ Cross-cutting helpers (all routes depend on them one-way; no cycles): `retry/policy`, `converters/anthropic`, and `errors/upstream` are pure (no Express/SDK imports) — keep them that way. -## 2. Request template (all three main routes) +## 2. Request template (all four main routes) Every `POST` route repeats the same template with small per-protocol differences: @@ -53,16 +54,17 @@ differences: Known intentional differences — do not "unify" them without approval: stream `flushHeaders` timing (responses flushes before preflight, chat / messages after), `lock()` wrapping (chat + messages only), cancel via -`res 'close'`, `forbidThinkBlock` per route, and the three error-exit +`res 'close'`, `forbidThinkBlock` per route, and the four error-exit shapes (chat non-stream JSON; responses-stream `response.failed` SSE + -`[DONE]`; messages-stream `error` event without `[DONE]`). +`[DONE]`; messages-stream `error` event without `[DONE]`; +interactions-stream `interaction.completed` / `error` event without `[DONE]`). ## 3. Extension points - **New route:** add `src/routes/xxx.ts` exporting `registerXxxRoutes(app, ctx)`, wire it in `src/proxy.ts` before the 404 fallback, add request/response types under `src/types/`. Copy the - §2 template; do not refactor the existing three routes in the same + §2 template; do not refactor the existing four routes in the same change. - **New converter** (e.g. another vendor protocol): add a pure module next to `src/converters/anthropic.ts` (`toChat` / `fromChat` /