中文 | English
Transparent local retry proxy for opencode.ai/zen Responses API previous_response_id expiry (referenced response not found or expired → 400).
When the upstream returns 400 referenced response not found or expired, the proxy strips previous_response_id, merges the stored full history for that id, and retries once. On success the session continues transparently — no client changes needed.
It also self-heals context-length overflow: muse reports it as a generic 400 invalid_request_error: The request contains invalid parameters (no "context length exceeded" wording). For large requests the proxy estimates input tokens and, on such a 400, compacts the input (keeps the head task/system context + recent tail, drops the middle, keeps function_call/function_call_output pairs atomic) and retries — the turn keeps running with reduced older context instead of dying.
Reasonix / Opencode's Responses API chains turns server-side via previous_response_id. If the upstream evicts that id (idle timeout / rolling window / deploy), every subsequent turn fails with:
{
"error": {
"type": "invalid_request_error",
"message": "Error from provider (Console Go): Upstream request failed: [invalid_request_error] referenced response not found or expired"
}
}This proxy makes that error self-healing.
# run in background (nohup keeps it alive after the shell exits)
nohup node proxy.mjs >/dev/null 2>&1 &
# or with env overrides:
LISTEN_PORT=8765 UPSTREAM=https://opencode.ai/zen/go/v1 nohup node proxy.mjs >/dev/null 2>&1 &
# or use the bundled launcher:
./start.shIf port
8765is already taken, the proxy exits with an error — pick another port viaLISTEN_PORT.
Point your client at the proxy instead of the upstream:
# reasonix.toml (see reasonix.toml.example)
[[providers]]
name = "local-fixed-responses"
kind = "responses"
base_url = "http://127.0.0.1:8765/v1"
models = ["muse-spark-1.2-retry", "muse-spark-1.2-contributor"]
api_key_env = "CUSTOM_OPENCODE_AI_API_KEY"Use model muse-spark-1.2-retry — the proxy aliases it to muse-spark-1.2-contributor upstream and enables the history-merge retry path. Using the real model name directly also works, but the alias makes it obvious when the fix is active.
Verify:
tail -f /tmp/opencode-retry-proxy.log
# stored history resp_xxx session=c1 items=912 bytes=1897560
# hit expired previous_response_id=resp_xxx session=c1 -> merge retry (46453 -> 1951954 bytes)
# retry result: 200 OKTip: hitting the proxy with a bogus model (
{"model":"test",...}) returnsModelError: Model test is not supported— that is the proxy forwarding correctly to the upstream, not a failure.
| Var | Default | Notes |
|---|---|---|
LISTEN_HOST |
127.0.0.1 |
Must stay loopback. Do NOT set to 0.0.0.0 — that would expose an open proxy forwarding your Authorization to anyone. |
LISTEN_PORT |
8765 |
|
UPSTREAM |
https://opencode.ai/zen/go/v1 |
Upstream Responses API base (should end with /v1). |
LOG_FILE |
/tmp/opencode-retry-proxy.log |
|
MAX_HISTORY |
10000 |
Max response ids kept in memory (respId -> chainId refs are tiny in the shared-chain model). |
MAX_HISTORY_BYTES |
268435456 (256MB) |
Byte budget for stored inputs; oldest entries evicted first (disk file follows the trimmed state). |
MERGE_MAX_BYTES |
4194304 (4MB) |
Merged retry bodies larger than this are not sent; the upstream 400 passes through. |
CTX_MAX_TOKENS |
750000 |
Token budget for the context-overflow self-heal; compacted inputs target ≤ this (tune below the model's actual limit; 0 disables the feature). |
CTX_HEAD_TOKENS |
20000 |
Tokens always kept from the start of the input (system/task context) during compaction. |
CTX_MIN_SUSPECT_TOKENS |
400000 |
Only attempt the self-heal when the request's estimated tokens exceed this, so small genuine-error requests are never touched. |
HISTORY_FILE |
/tmp/opencode-retry-proxy-history.json |
Disk persistence of stored history (see Security & Privacy). |
- Forwards every request to
UPSTREAMverbatim (preservesAuthorization,content-type, streaming). - Tracks sessions as
previous_response_idchains: every stored response carries asession=c_Ntag in logs, and full resends / compaction resets are detected per chain by matching the input's head hash against the chain head (the chain content is replaced instead of appended). - Stores full history by sniffing the response
idfrom bothapplication/jsonandtext/event-stream(SSE) bodies. Stringinput(used by some clients) is normalized to a single user-message item for storage/merging. - On
400 referenced response (not found|expired):- with stored history — strips
previous_response_id, mergeshistory[prevId] + inputinto a full replay (injectingsummary: []on reasoning items that lack it), and retries once; - without stored history (chain predates proxy start, or evicted) — last-resort strip-and-retry so the session survives (
DEGRADED retry without contextin the log; that turn may lack older context); - merged body over
MERGE_MAX_BYTES— the upstream 400 is passed through unchanged.
- with stored history — strips
- Persists history to
HISTORY_FILE(debounced, flushed on SIGTERM/SIGINT) and restores it at startup, so restarts don't amnesia previously-alive chains. - Self-heals context overflow: muse reports context-length overflow as a generic
400 invalid_request_error: The request contains invalid parameters. When that 400 arrives on a request estimated aboveCTX_MIN_SUSPECT_TOKENS, the proxy compacts the input toCTX_MAX_TOKENS— keeping the head (task/system context,CTX_HEAD_TOKENS) and the recent tail, dropping the middle, keepingfunction_call/function_call_outputpairs atomic — and retries once, then again at 70% of the budget. Logged ascontext overflow suspected ... compacting N -> M items. Trade-off: the model no longer sees the dropped middle context (older turns); the client's stored session is unaffected. - Retries transient network failures on every upstream fetch (forward, expired-id merge retry, compaction retry): Node fetch throws
fetch failedon connection resets / stale keep-alive, and the proxy retries up to 3 times with a short backoff before surfacing an error. This keeps an otherwise-healthy turn from dying when the upstream blips while a merge retry is in flight.
No secrets are logged — only len, id, status, and truncated hints.
# copy and edit ProgramArguments / WorkingDirectory paths inside
cp launchd.plist.example ~/Library/LaunchAgents/com.example.opencode-retry-proxy.plist
# register and start (modern macOS; `launchctl load` is deprecated)
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.example.opencode-retry-proxy.plist
# verify
launchctl list | grep opencode
# unload / remove
launchctl bootout gui/$(id -u)/com.example.opencode-retry-proxyOr use start.sh:
./start.sh- Binds to
127.0.0.1only. Do not expose to the network. - Forwards
Authorizationunchanged — whoever can reach the proxy can spend your API key's quota. - Request bodies are persisted locally: stored history (full
inputarrays, up toMAX_HISTORY_BYTES) is written toHISTORY_FILEso sessions survive proxy restarts. Keep the file on a private disk; delete it to wipe stored context. - Log lines contain only lengths, ids, statuses, and truncated hints — no full bodies.
- No dependencies beyond Node.js
>=18(uses nativefetch).
MIT — see LICENSE.