Conversation
… messages have reasoning_content
…andling of free lane context windows
The oa-compat to anthropic stream converter tracked a single current tool block, so with two calls in flight the argument deltas of call 0 were emitted against call 1's block index — the client handed one tool's input to another — and the earlier block was never closed. Key the blocks by the oa-compat tool_call index and stop all of them at the finish.
fetchWithRetry drained a retryable response to free the socket, then returned that same consumed Response on the last attempt. Reading it again in the caller throws 'Body is unusable', so an upstream 429 or 503 reached the client as a generic 500 and its backoff logic never saw the real status. Return the drained text instead, and forward retry-after with it.
OPENCODE_MODEL_CACHE_TTL accepts 0, which became setInterval(refresh, 0) and re-fetched discovery plus the 4MB catalog in a tight loop.
When the client hangs up mid-stream the controller closes while the read loop is still awaiting upstream. The 30s keep-alive then enqueued onto a closed controller, and a throw inside a timer callback is uncaught — it took the whole proxy down. Route every write through a guard that latches closed.
The free backend shares the Zen base URL, so discovery returned all 61 paid ids and they replaced the 9-model snapshot — the client's picker offered models the free lane answers with a 401, since it deliberately sends no key. Filter live ids by the catalog's zero-cost marker, and stop carrying stale cache ids through the merge, which resurrected the same ids on the next run.
The helper overwrote the whole header, discarding every beta the client had just been forwarded — token-efficient tools and fine-grained tool streaming were silently disabled on any 1M model.
The estimator read a block's text and tool_use input but never descended into content, so every tool result counted as zero characters. Those results are the bulk of an agent session's context, which made the estimate for every non-anthropic model far too low.
oa-compat sends usage only after the finish chunk, so the converter emitted a second message_delta carrying stop_reason: null after the real one — a client reading the last delta sees the turn's stop reason erased.
injectAuth was never called — the router hand-rolled the same header deletes in two places, so the free backend's no-credential rule lived in three copies that could drift. Replace it with clearAuth and use it at both call sites.
The proxy authenticates nobody and forwards the configured OpenCode key on every request. With Access-Control-Allow-Origin: * and Allow-Headers: * any page open in the user's browser could POST to 127.0.0.1:8787 and spend that key. The clients are CLIs and never needed the header; spec updated to match.
Usage parsers matched only "data: "; a compliant upstream writing
"data:{...}" had its usage chunk silently ignored, so cost pings and
normalized usage came out empty.
A chunk with an empty choices array — what stream_options.include_usage emits last — threw on chunk.choices[0].delta while serializing to openai or google.
Errors were recognized only by an SSE event line, which oa-compat upstreams
do not send — they emit a bare data: {"error":…}. The proxy then appended a
synthetic message_stop after the error, telling the client the turn ended
normally.
claude-ocx-<format>--<id> resolution ignored the format and matched on the id alone, so an anthropic-shaped alias for an oa-compat model resolved happily and the request went through the wrong translator.
fetchedAt was written on every save and never read, so each restart re-ran discovery and re-downloaded the multi-megabyte catalog even when the cache was minutes old. Skip the network while the cache is younger than the configured TTL.
pumpStream's start() locks the upstream body with getReader(), so the client disconnecting made cancel() call ReadableStream.cancel() on a locked stream. That throws inside a stream callback, which is unhandled and killed the process on every hang-up. Cancel via the held reader and swallow the rejection.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Test plan
npm run typechecknpm testnpm run buildChecklist
dev, notmain