Skip to content

Commit 453a228

Browse files
committed
fix(review): env alias compat, stream hardening, small-host guardrails, docs + 1.6.0
- Phase 1: DISABLE_TOOLS legacy alias via shared normalizeBool/resolveDisableTools (canonical > legacy > file > default, invalid falls through); tests/env-alias.test.js - Phase 2: compose RETRY/BIND_HOST/OPENCODE_DISABLE_TOOLS passthrough, mem/cpu/pids limits, log rotation, start_period 200s; Dockerfile RETRY default; entrypoint backend wait ~180s - Phase 3: responses/chat/messages preflight + non-stream prompt bounded by withTimeout (Request timeout -> 504); responses stream early SSE headers + heartbeat + res-based disconnect cancel; promptWithTimeout unified; tests/stream-hardening.test.js - Phase 4: docs (docker/configuration), CHANGELOG 1.6.0 with Breaking section, version badges 1.6.0
1 parent 440e607 commit 453a228

19 files changed

Lines changed: 549 additions & 86 deletions

‎.env.example‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ OPENCODE_PROXY_CLEANUP_MAX_AGE_MS=86400000
2525
# Optional overrides
2626
# OPENCODE_PROXY_PORT=10000
2727
# OPENCODE_SERVER_PORT=10001
28+
# BIND_HOST=0.0.0.0
2829
# OPENCODE_USE_ISOLATED_HOME=false
2930
# OPENCODE_PROXY_REQUEST_TIMEOUT_MS=180000
3031
# Retry budget after the initial attempt (0-5, default 3; total attempts = 1+n)

‎CHANGELOG.md‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2828
- **后端错误透出**:`/v1/responses` 在后端 session 失败时曾 `throw` 纯对象,经 `transformUpstreamError` 被洗成 `500 Internal server error / code Object`、真实讯息丢失。新增 `normalizeBackendError`(纯对象→Error,保留 `data.message`/`name`/状态码推断),两处 `throw polled.error` 改用它;`transformUpstreamError` 纵深加固(默认分支改读 `data.message`/`name`,code 默认 `upstream_error` 不再用 `constructor.name`)。现 Credits 类错误正确回 `402 insufficient_quota` 并携带原文。新增 `tests/backend-error-surface.test.js`(4 例复现 LiteLLM 案例,含 messages)。
2929
- **同族错误路径查漏**:responses 串流 idleTimeout 补 poll 失败检查(不再静默回空 `completed`);messages 非串流改用 transformed `status/type/code`(不再硬编码 `502/api_error`);messages 串流检查 `collected.error` 与 poll 失败(失败即抛正規化错误走 SSE error 事件,不再静默 200);`isTransientUpstreamError` 纳入 `name/code/type` 参与签名匹配(无 message 纯对象不再必判 false)。
3030

31+
## [1.6.0] - 2026-09-08
32+
33+
### ⚠️ Breaking / 行為變更
34+
35+
- **`DISABLE_TOOLS=false` 從被忽略變為真正生效**:舊版只讀 `OPENCODE_DISABLE_TOOLS`,經 compose/`.env` 設 `DISABLE_TOOLS=false` 的用戶實際跑的是預設 `true`(工具禁用)。本版起該別名正式生效,升級後工具會真正啟用並可執行後端操作。要保持禁用請顯式設 `DISABLE_TOOLS=true`(或改用 `OPENCODE_DISABLE_TOOLS=true`)或刪行回預設。
36+
- **串流錯誤語義(僅 `/v1/responses` 改變傳輸形態)**:`stream:true` 的 `/v1/responses` 在 headers 已 flush(早於 preflight)之後出錯時,改經 SSE `response.failed` 事件 + `data: [DONE]` 回報(HTTP 狀態固定為 200)。`/v1/chat/completions` 串流維持原形態(`data: {"error":…}` 後結束),`/v1/messages` 串流維持 `error` 事件後結束(無 `[DONE]`);且 chat/messages 的 preflight 失敗仍在 headers 送出前發生,故仍回 JSON 狀態碼(行為不變)。只看 HTTP 狀態碼的 responses 串流客戶端請改為同時監聽流內 `response.failed`;`stream:false` 的 JSON 狀態碼語義三路由皆不變(含超時 → `504`)。
37+
38+
### Added
39+
40+
- **Docker 多架构发布**:`ghcr.io/samson910022/opencode2api:latest` 同时推送 `linux/amd64` + `linux/arm64`(此前已上线但未记入 changelog;ARM 机器可直接 pull,无需本地 build)。
41+
- **环境变量兼容别名**:`docker-compose`/`.env` 沿用的 `DISABLE_TOOLS` 正式生效(此前只有 `OPENCODE_DISABLE_TOOLS` 被读取)。解析收敛到共用的 `normalizeBool` + `resolveDisableTools`(无效值让位:canonical env > legacy env > `config.json` > 默认;數字僅 `0/1` 有效,其餘視為未設),`index.js` 直接調用同一 helper 不再手寫 `??` 鏈,附 `tests/env-alias.test.js`。
42+
- **串流强健(responses/chat/messages 三路由)**:串流 `/v1/responses` 在 preflight 等待前即 flush SSE 头 + 15s heartbeat(中转不再看到零字节 stall;chat/messages 的 headers 仍在 preflight 之後送出,其 preflight 失敗維持 JSON 狀態碼);三路由的 `resolve/session/tool-overrides` preflight 與非串流 `prompt` 全部经 `withTimeout` 限界(真错误立即透出,超时经 `Request timeout` 映射為 `504`);客户端断开经 `res 'close'` + `!writableEnded` 取消并清理 session(`req 'close'` 在 body 解析完即觸發,不可用);`promptWithTimeout` 統一改調 `withTimeout`(舊 inline race 洩漏 timer 且晚拒絕可致 unhandled rejection)。附 `tests/stream-hardening.test.js`。
43+
- **小机部署加固(可調)**:compose 加 `mem_limit 768m`/`mem_reservation 256m`/`cpus 1.5`/`pids_limit 256`、日志轮转、`start_period 200s`,补 `OPENCODE_PROXY_RETRY_MAX_RETRIES`/`BIND_HOST`/`OPENCODE_DISABLE_TOOLS` 透传;`Dockerfile` 补 `RETRY` 默认;entrypoint 后端等待 30s→约 180s(覆盖小机冷启动)。`mem_limit` 是部署行為變更(非 API breaking):此前無上限只會變慢,現超限會被 OOM kill;大機請用 `docker-compose.override.yml` 放寬(見 `docs/docker.md`「覆寫資源限制」)。
44+
45+
### Fixed
46+
47+
- **`DISABLE_TOOLS=false` 被静默忽略**:见上 Breaking(生产默认仍为 `true`,只在显式设 `false`/别名时行为变化)。
48+
- **断线侦测误用 `req 'close'`**:server 端该事件在请求体解析完即触发(实测 +1ms),不能用于串流断线;改用 `res 'close'` + `!writableEnded`(`/v1/messages` 旧 pattern 因监听挂得晚而等于失效,一并修复;監聽器改 `once` 避免殘留)。
49+
- **`withTimeout` 超時誤回 500**:超時訊息現為 `Request timeout after …(label)`,可被 `transformUpstreamError` 正確映射為 `504`(此前自訂訊息繞過映射);`createApp` 補 `REQUEST_TIMEOUT_MS` 預設,非有限值回退到 `DEFAULT_REQUEST_TIMEOUT_MS`。
50+
- **`BIND_HOST` 命名分裂**:`startProxy` 後備鏈與 `index.js` 合併皆支援 `BIND_HOST` 優先、`OPENCODE_PROXY_BIND_HOST` 後備(此前 `startProxy` 只有後者,經正常 `index.js` 路徑時後備不可達)。
51+
- **文件範例收斂(非 breaking)**:`docs/docker.md` 的 `docker run` 範例刪除 `-p 10001:10001`(後端僅容器內部使用;運行時無變更,舊映射仍可用,建議改走代理端口)。
52+
3153
## [1.5.0] - 2026-04-18
3254

3355
### Added

‎Dockerfile‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ ENV OPENCODE_PROXY_AUTO_CLEANUP_CONVERSATIONS=false
4141
ENV OPENCODE_PROXY_CLEANUP_INTERVAL_MS=43200000
4242
ENV OPENCODE_PROXY_CLEANUP_MAX_AGE_MS=86400000
4343
ENV OPENCODE_PROXY_REQUEST_TIMEOUT_MS=180000
44+
ENV OPENCODE_PROXY_RETRY_MAX_RETRIES=3
4445

4546
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
4647
CMD ["opencode", "serve", "--hostname", "0.0.0.0", "--port", "10001"]

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# OpenCode2API
22

33
<p align="center">
4-
<img src="https://img.shields.io/badge/version-1.5.0-blue" alt="Version">
4+
<img src="https://img.shields.io/badge/version-1.6.0-blue" alt="Version">
55
<img src="https://img.shields.io/badge/license-MIT-green" alt="License">
66
<img src="https://img.shields.io/badge/Node.js-18+-orange" alt="Node">
77
</p>

‎docker-compose.yml‎

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ services:
1313
- OPENCODE_SERVER_PASSWORD=${OPENCODE_SERVER_PASSWORD:-}
1414
- OPENCODE_ZEN_API_KEY=${OPENCODE_ZEN_API_KEY:-}
1515
- API_KEY=${API_KEY:-}
16-
- BIND_HOST=0.0.0.0
16+
- BIND_HOST=${BIND_HOST:-0.0.0.0}
1717
- DISABLE_TOOLS=${DISABLE_TOOLS:-true}
18+
- OPENCODE_DISABLE_TOOLS=${OPENCODE_DISABLE_TOOLS:-}
1819
- OPENCODE_USE_ISOLATED_HOME=${OPENCODE_USE_ISOLATED_HOME:-false}
1920
- OPENCODE_PROXY_DEBUG=${OPENCODE_PROXY_DEBUG:-false}
2021
- OPENCODE_PROXY_PROMPT_MODE=${OPENCODE_PROXY_PROMPT_MODE:-standard}
@@ -23,18 +24,36 @@ services:
2324
- OPENCODE_PROXY_CLEANUP_INTERVAL_MS=${OPENCODE_PROXY_CLEANUP_INTERVAL_MS:-43200000}
2425
- OPENCODE_PROXY_CLEANUP_MAX_AGE_MS=${OPENCODE_PROXY_CLEANUP_MAX_AGE_MS:-86400000}
2526
- OPENCODE_PROXY_REQUEST_TIMEOUT_MS=${OPENCODE_PROXY_REQUEST_TIMEOUT_MS:-180000}
27+
- OPENCODE_PROXY_RETRY_MAX_RETRIES=${OPENCODE_PROXY_RETRY_MAX_RETRIES:-3}
2628
- OPENCODE_PROXY_PORT=${OPENCODE_PROXY_PORT:-10000}
2729
- OPENCODE_SERVER_PORT=${OPENCODE_SERVER_PORT:-10001}
2830
volumes:
2931
- opencode-data:/home/node/.local/share/opencode
3032
- opencode-config:/home/node/.config/opencode
3133
restart: unless-stopped
34+
# Guard small (1GB) hosts: cap memory/CPU so one slow request cannot wedge
35+
# the whole machine (see docs/host-deployments.md §9). Raw `docker run`
36+
# hosts should pass the equivalents: --memory=768m --memory-reservation=256m
37+
# --cpus=1.5 --pids-limit=256.
38+
mem_limit: 768m
39+
mem_reservation: 256m
40+
cpus: 1.5
41+
pids_limit: 256
42+
logging:
43+
driver: json-file
44+
options:
45+
max-size: "10m"
46+
max-file: "3"
3247
healthcheck:
48+
# Shallow on purpose: /health needs no auth; /health/details would 401
49+
# when HEALTH_DETAILS_REQUIRE_AUTH=true and has no credentials here.
3350
test: ["CMD", "curl", "-f", "http://localhost:${OPENCODE_PROXY_PORT:-10000}/health"]
3451
interval: 30s
3552
timeout: 10s
3653
retries: 3
37-
start_period: 60s
54+
# Must cover entrypoint backend wait (~180s on cold small hosts) plus
55+
# probe intervals; 120s flapped unhealthy during normal warmup.
56+
start_period: 200s
3857

3958
volumes:
4059
opencode-data:

‎docs/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# OpenCode2API 文档中心
22

33
<p align="center">
4-
<img src="https://img.shields.io/badge/version-1.5.0-blue" alt="Version">
4+
<img src="https://img.shields.io/badge/version-1.6.0-blue" alt="Version">
55
</p>
66

77
> 欢迎查看 OpenCode2API 文档

‎docs/api-reference.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# 🔌 API 参考
22

33
<p align="center">
4-
<img src="https://img.shields.io/badge/version-1.5.0-blue" alt="Version">
4+
<img src="https://img.shields.io/badge/version-1.6.0-blue" alt="Version">
55
</p>
66

77
---

‎docs/configuration.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# ⚙️ 配置详解
22

33
<p align="center">
4-
<img src="https://img.shields.io/badge/version-1.5.0-blue" alt="Version">
4+
<img src="https://img.shields.io/badge/version-1.6.0-blue" alt="Version">
55
</p>
66

77
---
@@ -21,15 +21,15 @@
2121
| `PORT` / `OPENCODE_PROXY_PORT` | `10000` | 代理服务端口 |
2222
| `OPENCODE_SERVER_PORT` | `10001` | OpenCode 后端服务端口 |
2323
| `API_KEY` | - | Bearer Token 认证密钥 |
24-
| `BIND_HOST` | `0.0.0.0` | 绑定地址 |
24+
| `BIND_HOST` | `0.0.0.0` | 绑定地址(`BIND_HOST` 优先,`OPENCODE_PROXY_BIND_HOST` 为后备) |
2525
| `OPENCODE_SERVER_URL` | `http://127.0.0.1:10001` | OpenCode 后端地址 |
2626
| `OPENCODE_SERVER_PASSWORD` | - | OpenCode 后端密码 |
2727

2828
### 功能配置
2929

3030
| 变量 | 默认值 | 说明 |
3131
|:-----|:-------|:-----|
32-
| `DISABLE_TOOLS` | `true` | 禁用 OpenCode 工具调用 |
32+
| `DISABLE_TOOLS` | `true` | 禁用 OpenCode 工具调用(兼容别名;`OPENCODE_DISABLE_TOOLS` 优先,二者无效值都会让位给下一顺位:canonical env > legacy env > `config.json` > 默认) |
3333
| `OPENCODE_EXTERNAL_TOOLS_MODE` | `proxy-bridge` | 外部工具桥接模式;当前仅支持 `proxy-bridge` |
3434
| `OPENCODE_EXTERNAL_TOOLS_CONFLICT_POLICY` | `namespace` | 外部工具冲突隔离策略;当前仅支持 `namespace` |
3535
| `OPENCODE_INTERNAL_WEB_FETCH_ENABLED` | `false` | 兼容旧开关;未显式配置 allowlist 时,启用后默认放行 `web_fetch` |

‎docs/development.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# 💻 开发指南
22

33
<p align="center">
4-
<img src="https://img.shields.io/badge/version-1.5.0-blue" alt="Version">
4+
<img src="https://img.shields.io/badge/version-1.6.0-blue" alt="Version">
55
</p>
66

77
---

‎docs/docker.md‎

Lines changed: 19 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# 🐳 Docker 部署
22

33
<p align="center">
4-
<img src="https://img.shields.io/badge/version-1.5.0-blue" alt="Version">
4+
<img src="https://img.shields.io/badge/version-1.6.0-blue" alt="Version">
55
</p>
66

77
---
@@ -83,53 +83,44 @@ docker build -t my-opencode2api .
8383
```bash
8484
docker run -d \
8585
-p 10000:10000 \
86-
-p 10001:10001 \
8786
-e API_KEY=your-key \
8887
-e OPENCODE_SERVER_PASSWORD=your-password \
8988
-v opencode-data:/home/node/.local/share/opencode \
9089
-v opencode-config:/home/node/.config/opencode \
9190
my-opencode2api
9291
```
9392

94-
---
93+
> 后端 `10001` 仅容器内部使用,不要对外发布(compose 默认也只映射代理端口;改端口用 `OPENCODE_PROXY_PORT`,如 `8090`)。
94+
95+
### 多架构
9596

96-
## 📝 生产部署建议
97+
`ghcr.io/samson910022/opencode2api:latest` 同时含 `linux/amd64` 与 `linux/arm64`,ARM 机器可直接 pull,无需本地 build(仅自带 `custom-bin/opencode` 二进制时才需按架构重建)。
9798

98-
### 移除源码挂载
99+
### 覆寫資源限制
99100

100-
如果不需要在容器内修改代码,可以移除项目目录的挂载:
101+
compose 預設 `mem_limit 768m`(單容器含 proxy+後端)。大機要放寬或取消,用 override 檔:
101102

102103
```yaml
103-
# docker-compose.yml
104-
volumes:
105-
- opencode-data:/home/node/.local/share/opencode
106-
- opencode-config:/home/node/.config/opencode
107-
# 移除这一行
108-
# - .:/home/node/project
104+
# docker-compose.override.yml
105+
services:
106+
opencode2api:
107+
mem_limit: 2g
108+
mem_reservation: 512m
109+
cpus: 4.0
109110
```
110111
111112
---
112113
113114
## 📊 日志管理
114115
116+
compose 已内置 json-file 轮转(`max-size: 10m`、`max-file: 3`);裸 `docker run` 请自行加上 `--log-opt max-size=10m --log-opt max-file=3`。
117+
115118
### 查看日志
116119

117120
```bash
118121
docker compose logs -f
119122
```
120123

121-
### 日志轮转
122-
123-
推荐使用 Docker 的日志驱动配置:
124-
125-
```yaml
126-
logging:
127-
driver: "json-file"
128-
options:
129-
max-size: "10m"
130-
max-file: "3"
131-
```
132-
133124
---
134125

135126
## ✅ 健康检查
@@ -138,13 +129,15 @@ logging:
138129

139130
```yaml
140131
healthcheck:
141-
test: ["CMD", "curl", "-f", "http://localhost:10000/health"]
132+
test: ["CMD", "curl", "-f", "http://localhost:${OPENCODE_PROXY_PORT:-10000}/health"]
142133
interval: 30s
143134
timeout: 10s
144135
retries: 3
145-
start_period: 60s
136+
start_period: 200s
146137
```
147138

139+
> `/health` 是浅检查(只确认代理活着,不测后端;`start_period` 200s 覆盖 entrypoint 约 180s 的小机冷启动等待)。小机(1GB)另有 `mem_limit 768m` / `pids_limit 256` 保护,裸 `docker run` 请加等价旗(见 `docker-compose.yml` 注释)。
140+
148141
---
149142

150143
## ❓ 常见问题

0 commit comments

Comments
 (0)