Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 例。

Expand Down
45 changes: 43 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---

Expand All @@ -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 |
Expand Down Expand Up @@ -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
Expand All @@ -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 |
Expand All @@ -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 |
Expand Down Expand Up @@ -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

Expand Down
45 changes: 43 additions & 2 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` 实时列表为准。

---

Expand All @@ -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 后端 |
| 🛡️ **工具安全** | 默认禁用工具调用 |
Expand Down Expand Up @@ -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`。

---

## 📦 部署方式
Expand All @@ -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` |
Expand All @@ -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 可执行文件路径 |
Expand Down Expand Up @@ -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` 的别名 |

### 模型名称格式

Expand Down
Loading
Loading