Skip to content

Dev - #3

Merged
schiz0x00 merged 18 commits into
mainfrom
dev
Aug 8, 2026
Merged

Dev#3
schiz0x00 merged 18 commits into
mainfrom
dev

Conversation

@schiz0x00

Copy link
Copy Markdown
Owner

Summary

Test plan

  • npm run typecheck
  • npm test
  • npm run build

Checklist

  • PR targets dev, not main
  • No secrets / API keys in diff

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.
@schiz0x00
schiz0x00 merged commit 6d3a3c0 into main Aug 8, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant