All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
分叉声明 / Fork notice:本项目自
TiaraBasori/opencode2api的v1.5.0起由samson910022/OpenCode2API独立维护。v1.5.0(含)以前见上游历史; 此后变更见本节([Unreleased])及后续版本节。
- 独立维护声明:README 致谢明确直接上游
TiaraBasori/opencode2api,LICENSE 追加上游归属,CHANGELOG 声明分叉点。 - 仓库去耦合:文档克隆/Issue 链接与 Docker 镜像指向
samson910022/OpenCode2API,package.json补全维护元数据。 - Responses 示例模型:
README、docs/api-reference.md、docs/getting-started.md的/v1/responses示例统一为opencode/muse-spark-1.3-contributor-free(解析逻辑无需改动,裸名与带前缀均兼容)。 - SDK 对齐:
@opencode-ai/sdk ^1.1.51→^1.18.29(与本地 server1.18.29对齐;v1 调用形状不变,零改码;139 tests 全绿;cross-spawn@7.0.6为 SDK 新增 prod 依赖,既有包去 dev 标记,无新版本;tool.ids仍走上游 experimental 路径,后续跟进)。 - Unified env/file/default config merge (Phase 3A):
index.tsandbuildProxyConfignow share the strict resolvers insrc/config/proxy-config.ts(resolve*Setting,parseStrictInteger/Port,parseHttpUrl,parseToolNameList); lookup stays env canonical > env legacy alias > file short key > hardcoded default with invalid values falling through. LibraryresolveDisableToolsdefault is nowtrue(wasfalse), matching prod/docs; library consumers relying on the old tools-enabled default must now setDISABLE_TOOLS=falseexplicitly. Bool tokens are1/true/yes/y/onvs0/false/no/n/offeverywhere. Docker/Compose keep empty defaults so code owns values;entrypoint.shvalidatesOPENCODE_PROXY_PORT/PORTandOPENCODE_SERVER_PORTagainst1..65535and unsets invalid proxy ports soconfig.jsonstill applies.
- GitHub AI bot (gateway + CPA dual-channel): new
github_bot/(dual-providerllm_clientwithxhigh→highCPA mapping, verbatim thinking/signature passthrough,response_idsession echo,_redact_secrets/sanitize_public_error_text;agent_orchestratortriage/review/explain/scan/fix-plan pipelines;repo_scandeterministic pre-scan;github_runnersticky-marker publish + allowlist labels + fingerprint-dedupe auto-issue + opt-in draft PRs) with role-separated primaries (triagegemini-3.8-flash-high, safetygrok-4.6, api-compatclaude-opus-4-6-thinking, docsmuse-spark-1.3-contributor-free, explainerbig-pickle, scannergrok-composer-2.5-fast) and cross-channel fallbacks; new workflowsai-review.yml/ai-scan.yml(weekly cron) /ai-fix.yml(/fixplans; draft PRs only with opt-in, human review required), each starting its own no-auth in-job gateway (API_KEYempty,OPENCODE_PROXY_MANAGE_BACKEND=true, anonymous out-of-box quota; externalGATEWAY_BASE_URLsecret overrides) with CPA as optional fallback;tests/test_ai_bot.py(34 tests);docs/AI_BOT.md. 新增 GitHub AI Bot(自託管 gateway 免費通道 + CPA 通道,定時掃描自動開 issue,/fix規劃;opt-in 才開 draft PR,需人工審查)。 - N×N protocol translator registry + route wiring: new pure
src/converters/matrix (registry/pipeline/formats/translator-types + 12 request + 12 response directed pairs for chat, responses, messages, interactions; stream + non-stream;fidelity.tsledger with 3 full forward edges out of chat and 9 text-core legs; TokenCount intentionally unregistered so counting stays in the collector). Wiring: registry initializes once increateAppand is exposed viactx.translators;src/converters/wire.tsSafe wrappers enforce error-envelope bypass (wrapped{error},response.failed,errorevents, bare{message,type}bodies, raw SSE error strings) and per-stream holder reuse;POST /v1/messagesinbound conversion now goes through the registry (thin wrapper over the sameanthropic.tspure layer, adaptive thinking collapsed to the legacy fallback for strict parity). Behavior is otherwise unchanged: wire/SSE/[DONE]/error-exit shapes stay route-owned, translated tools still flow through theexternal__*bridge + allowlist, translated usage is client-facing only. Newsrc/converters/holder.ts+wire.ts,tests/translator-wire.test.js(fail-fast, bypass, holder, parity) andtests/translator-integration.test.js(live route outputs translate cross-protocol). 新增 N×N 纯函数转换矩阵与 messages 入站接线,线形与工具政策保持不变。 - Interactions API (Gemini-compatible thin layer): new
POST /v1beta/interactions(aliasPOST /v1/interactions) mapping text +google_searchgrounding onto the Responses pipeline, withsystem_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/responsesfor those). - Multi-key auth:
API_KEYmerges withOPENCODE_API_KEYS/API_KEYS(any match passes viatimingSafeEqual;Bearerorx-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) engagesOPENCODE_UPSTREAM_PROXIES(failover-rr/round-robinalias /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/responsestools: [{type: "web_search"}](alsoweb_search_preview/ versionedweb_search_*/google_search) drives the opencode built-inwebsearchand returnsweb_search_call+ honesturl_citationannotations only;web_searchon 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 的 latentReferenceError。 - 官方对齐重试策略:移植上游
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 例。
- Backend tool-hang + per-request dispose fixes: the proxy no longer
PUT config {activeModel}on every request — nativeConfigHttpApi.updatedisposes the whole backend project instance unconditionally (no changed-check), aborting in-flight sessions into theAbortedretry-loop storm;ensureActiveModel(src/backend/active-model.ts) PUTs only on actual model change per backend client (failures never cached, existing catch semantics preserved). Separately, the headless backend can no longerask(nobody approves, so prompts hung until the 180s timeout) nor silently execute unlisted tools (the free-tier strip omits the prompt tools map, so the backend fell back to agent defaults = all on): backendopencode.jsonnow carries a generated deny-allpermissionmap (src/backend/backend-permission.ts, single source forentrypoint.shviadistwith static fallback andsrc/backend/manager.tsisolated path) allowing only explicitly configured internal tools, withexternal_directoryalways jailed to/home/node/project/**. 后端不再每请求 dispose(模型变更才 PUT activeModel);headless 后端不再ask卡死、不再静默执行未授权工具(deny-all,仅放行明确配置的 internal 工具)。Locked bytests/active-model.test.js+tests/backend-permission.test.js+tests/tool-protocol-parity.test.js. - Anonymous first-party backend posture + Zen free-tier gate handling: spawned backend pins
OPENCODE_CLIENTto a known first-party value (clidefault;desktop/acp/apppass through), strips theOPENCODE_API_KEY="public"footgun (anonymous uses empty), and git-inits the jail workspace best-effort (5s timeout, warn-and-continue) as defense-in-depth for session-dir sensitivity. Prompttoolsmaps havefalseentries stripped (true-only sent; all-false omitted) only for likely-freeopencodemodels (-freesuffix,big-pickle,union-alpha) to dodge theFreeTierError403 (anyfalsegates); stripped tools fall back to agent defaults server-side while paid/other providers keep the exact hard-disable map. The gate (...used from within OpenCode/FreeTierError) now surfaces as 403permission_deniedfail-fast (no retry, no proxy fallback) with false-positive guards for plain quota text. Jail stdout no longer prints absolute paths; interactions route gains text-level tool-markup stripping parity with chat/responses/messages. Newtests/backend-identity.test.js/tests/first-party-gate.test.js/tests/prompt-tool-overrides.test.js/tests/session-ids.test.js. No new proxy knob (no config matrix change). - Translator parity fixes (N×N matrix review): chat array
contentparts no longer silently dropped (extracttext/content);responsesincompletewithcontent_filterreason round-trips instead of collapsing tolength(non-stream + stream);is_errornow round-trips via theERROR:text prefix on chat/messages/responses tool legs (stricttrue/"true"check; success text literally starting withERROR:round-trips asis_errorby design); namedcustomtool declarations are kept socustom_tool_callinvocations always have a matching declaration; ID generation centralized onmakeId(prefix)(toolu_fallback separator canonicalized, prefix-only checks unaffected). N×N 翻译矩阵对齐修复。Newtests/translator-phase-fixes.test.js(13 cases). - 后端错误透出:
/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)。 - 同族错误路径查漏:responses 串流 idleTimeout 补 poll 失败检查(不再静默回空
completed);messages 非串流改用 transformedstatus/type/code(不再硬编码502/api_error);messages 串流检查collected.error与 poll 失败(失败即抛正規化错误走 SSE error 事件,不再静默 200);isTransientUpstreamError纳入name/code/type参与签名匹配(无 message 纯对象不再必判 false)。 - Docker six-way symmetry (Stage 1):
Dockerfileadds the missing emptyENV OPENCODE_PROXY_BIND_HOST=plus the five legacyENV EXTERNAL_TOOL_*=aliases so image defaults stay empty and never shadow a mountedconfig.jsonor legacy alias; behavior is value-identical (empty == unset). - Stream tool-error surface unification (Stage 2 + final):
/v1/chat/completions,/v1/messages, and/v1/responsesstream:truefail-closed tool errors (parallel_external_tool_calls,external_tool_choice_*,invalid_external_tool_call,external_tool_policy_blocked,duplicate_external_tool_call_id,malformed_external_tool_call) now return the same stablecodeas non-stream (chat: 502 JSON before headers / SSEdata: {error}after; messages: 502 JSON / SSEerrorevent,api_error; responses: 502 JSON /response.failed+[DONE],server_error) instead of falling through to 500internal_errorviatransformUpstreamError. - Collector ID-less tool tracking (Stage 2, review fix): counter becomes a per-tool-name
Set(noid:<tool>/noid:unknown) so two concurrent ID-less backend tool parts no longer clear each other, while repeatedmessage.part.updatedsnapshots of the same logical tool no longer inflate and pin the idle exemption untilREQUEST_TIMEOUT_MS. Replayed terminal snapshots are ignored via a settled set (they must not delete a still-running bucket); name-mismatch fuzzy clearing applies to the single-bucket case only, multi-bucket ambiguity waits for the real terminal (waiting beats truncating). Locked bytests/stream-hardening.test.js(repeat, distinct-parallel, replay, mismatch both directions, same-name limitation). - Review confirmations (no behavior change):
stream:truewith any external tool contract intentionally buffers visible text until completion to avoid leaking<function_calls>markup incrementally (pure-text turns also batch — documented in route comments); non-streamif (error != null)intentionally fails even with partial content rather than returning partial as success on chat/messages/responses (retry still covers the no-output transient case; interactions is the deliberate exception — it only throws when there is no content at all, otherwise returns partial asinteraction.completed); verified-empty tool discovery ([]) intentionally stays 503 fail-closed underDISABLE_TOOLS=true(a real backend always exposes tools; empty means unverifiable — locked bytests/tool-alias.test.js). - Explicit external-tool policy semantics (Stage 2, breaking note):
REQUIRE_CONFIRMATIONnow fires only for proxy-authoredrequiresConfirmation===trueorEXTERNAL_TOOL_REQUIRE_CONFIRMATION_FORhits (plusreport-onlybypass); the old implicitWRITE/HIGH/CRITICAL/DELETEinference is removed, so adelete_repotool with an empty allowlist now returnsALLOWinstead of being blocked. Allowlist / denylist / confirmation lists are exact-match only (no wildcard/glob support; matching is checked against original / namespaced / declared names). Operators relying on default-deny for destructive tools should list exact tool names explicitly, e.g.EXTERNAL_TOOL_REQUIRE_CONFIRMATION_FOR=delete_repo,write_file. 白名单/黑名单/确认名单仅精确匹配,不支持*通配符(按原始名/命名空间名/声明名三者任一命中为准)。Parser stays fail-closed by design (valid plus malformedstill rejects the batch withmalformed_external_tool_call, locked bytests/app.test.js). - Wire-compat confirmations (Stage 3): upstream 503 now passes through as 503
server_error(was coerced to 502;tool_discovery_unavailablestays proxy-originated only, locked bytests/backend-error-surface.test.js);/v1/messagesbare{type:tool}withoutnameis 400 viavalidateAnthropicToolChoice(direct converter calls degrade to auto, locked bytests/messages-anthropic.test.js); emptytool_resultis preserved asTOOL_RESULT content:""to keep turn alignment (prompt breaking: previously dropped, now one extra prompt line); chat/messages/responses empty-stream stays 500Upstream returned no assistant data(fail-closed, non-stream empty stays 200 — documented inconsistency, unify later); interactionsstoreResponseStateintentionally omits tool calls (text+grounding only); native discovery 30s cooldown fail-closed is by design (no secondtool.ids()probe inside the window).
DISABLE_TOOLS=false從被忽略變為真正生效:舊版只讀OPENCODE_DISABLE_TOOLS,經 compose/.env設DISABLE_TOOLS=false的用戶實際跑的是預設true(工具禁用)。本版起該別名正式生效,升級後工具會真正啟用並可執行後端操作。要保持禁用請顯式設DISABLE_TOOLS=true(或改用OPENCODE_DISABLE_TOOLS=true)或刪行回預設。- 串流錯誤語義(僅
/v1/responses改變傳輸形態):stream:true的/v1/responses在 headers 已 flush(早於 preflight)之後出錯時,改經 SSEresponse.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)。
- Docker 多架构发布:
ghcr.io/samson910022/opencode2api:latest同时推送linux/amd64+linux/arm64(此前已上线但未记入 changelog;ARM 机器可直接 pull,无需本地 build)。 - 环境变量兼容别名:
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。 - 串流强健(responses/chat/messages 三路由):串流
/v1/responses在 preflight 等待前即 flush SSE 头 + 15s heartbeat(中转不再看到零字节 stall;chat/messages 的 headers 仍在 preflight 之後送出,其 preflight 失敗維持 JSON 狀態碼);三路由的resolve/session/tool-overridespreflight 與非串流prompt全部经withTimeout限界(真错误立即透出,超时经Request timeout映射為504);客户端断开经res 'close'+!writableEnded取消并清理 session(req 'close'在 body 解析完即觸發,不可用);promptWithTimeout統一改調withTimeout(舊 inline race 洩漏 timer 且晚拒絕可致 unhandled rejection)。附tests/stream-hardening.test.js。 - 小机部署加固(可調):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「覆寫資源限制」)。
DISABLE_TOOLS=false被静默忽略:见上 Breaking(生产默认仍为true,只在显式设false/别名时行为变化)。- 断线侦测误用
req 'close':server 端该事件在请求体解析完即触发(实测 +1ms),不能用于串流断线;改用res 'close'+!writableEnded(/v1/messages旧 pattern 因监听挂得晚而等于失效,一并修复;監聽器改once避免殘留)。 withTimeout超時誤回 500:超時訊息現為Request timeout after …(label),可被transformUpstreamError正確映射為504(此前自訂訊息繞過映射);createApp補REQUEST_TIMEOUT_MS預設,非有限值回退到DEFAULT_REQUEST_TIMEOUT_MS。BIND_HOST命名分裂:startProxy後備鏈與index.js合併皆支援BIND_HOST優先、OPENCODE_PROXY_BIND_HOST後備(此前startProxy只有後者,經正常index.js路徑時後備不可達)。- 文件範例收斂(非 breaking):
docs/docker.md的docker run範例刪除-p 10001:10001(後端僅容器內部使用;運行時無變更,舊映射仍可用,建議改走代理端口)。
- External Tool Bridge: Added proxy-level bridging for external OpenAI-compatible
toolsacross/v1/chat/completionsand/v1/responses. - Streaming Tool Call Parity: Added streaming support for external tool calls in both Chat Completions and Responses APIs.
- Explicit External Tool Config: Added explicit
EXTERNAL_TOOLS_MODE=proxy-bridgeandEXTERNAL_TOOLS_CONFLICT_POLICY=namespaceconfiguration surface and documentation.
- Project Version: Bumped the repository version to
1.5.0across package metadata and documentation badges.
- Jest Test Shutdown: Removed a lingering queue rescheduling timer from the proxy request lock flow and updated the default test command to use the verified clean Jest invocation, eliminating the previous generic open-handle warning during
npm test.
- OpenAI-compatible API:
/v1/models,/v1/chat/completions,/v1/responsesendpoints - Streaming Support: Full SSE streaming for Chat Completions and Responses API
- Model Aliases: GPT-style model aliasing (e.g.,
gpt5-nano→gpt-5-nano) - Docker Deployment: Complete Docker setup with healthcheck and volume management
- Configuration: Environment variables and config.json support
- Auto Cleanup: Configurable automatic conversation/session storage cleanup
- Default Security:
DISABLE_TOOLSdefaults totruefor safer out-of-box behavior