Skip to content

fix(llm): OpenAI-compatible streaming tool calls (#475) + api-key Responses 401 diagnostics (#476) - #481

Draft
TYRMars wants to merge 2 commits into
mainfrom
claude/vibrant-dijkstra-1kkn2d
Draft

fix(llm): OpenAI-compatible streaming tool calls (#475) + api-key Responses 401 diagnostics (#476)#481
TYRMars wants to merge 2 commits into
mainfrom
claude/vibrant-dijkstra-1kkn2d

Conversation

@TYRMars

@TYRMars TYRMars commented Jul 20, 2026

Copy link
Copy Markdown
Owner

Summary

Two independent @jarvis/llm provider fixes, both surfaced as freshly-opened bug reports.

Closes #476 (the issue that triggered this change) and #475.

#476responses.ts: api-key 401s discard the real upstream body

completeStream checked resp.status === 401 before the branch that reads the upstream body, and refreshIfUnchanged's api_key arm unconditionally threw "API-key auth cannot refresh". So for JARVIS_PROVIDER=openai-responses, any 401 from api.openai.com/v1/responses surfaced as that fixed string — the real {"error":{"message":…}} body and status never reached the operator. The 401 body was also never consumed (socket pinned until GC), and the message wasn't classified as an auth failure by fallback.ts.

Fix: guard the refresh branch on auth.kind === "chatgpt_oauth" so api-key 401s fall through to the !resp.ok branch, which reports status 401: <body>, consumes the body, and yields a fallback-recognisable message.

#475openai.ts: id-less streamed tool calls dropped (Kimi/Ollama)

StreamAccumulator.#finalise required both id and name to keep a tool call. OpenAI-compatible backends (Kimi/Ollama) stream {index, function:{name, arguments}} with no id, so the call was silently dropped — leaving finish_reason: "tool_calls" on an empty assistant message and the tool never running over SSE/WS.

Fix: skip only true placeholder slots (no name) and synthesise call_<index> when the stream omits an id, mirroring the Google provider's gem_<index>.

Tests

  • openai.test.ts: id-less deltas get a synthesised call_0 and survive; padding slots still skipped.
  • responses.test.ts: api-key 401 surfaces status 401: …Incorrect API key provided, no refresh retry.

All 158 @jarvis/llm tests pass; changed files lint clean. (Pre-existing repo-wide typecheck errors about DOM globals are environmental — the sandbox pnpm install can't fetch the electron/DOM lib types — unrelated to this diff.)

🤖 Generated with Claude Code

https://claude.ai/code/session_01AfgFeWNPZaPcWc8CSDL3H6

claude added 2 commits July 20, 2026 01:24
StreamAccumulator.#finalise dropped any streamed tool call whose deltas
never carried an `id`, because the guard required both `id` and `name`.
OpenAI-compatible backends (Kimi/Ollama) commonly stream
`{index, function:{name, arguments}}` with no `id`, so the real call was
silently discarded while finish_reason stayed "tool_calls" — leaving an
empty assistant message and the tool never running over SSE/WS (the
non-streaming path kept the call, so the same backend worked via
/v1/chat/completions).

Skip only true placeholder slots (no `name`) and synthesise `call_<index>`
when the stream omits an id, matching how the Google provider synthesises
`gem_<index>`. Adds regression tests for the id-less and padding cases.

Fixes #475

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AfgFeWNPZaPcWc8CSDL3H6
completeStream checked `resp.status === 401` before the branch that reads
the upstream body, and refreshIfUnchanged's api_key arm unconditionally
threw "API-key auth cannot refresh". So for JARVIS_PROVIDER=openai-responses
any 401 from api.openai.com/v1/responses (revoked key, wrong project/org,
disabled account) surfaced as that one fixed string — the real
`{"error":{"message":…}}` body and status were discarded. The 401 Response
body was also never consumed (undici kept the socket pinned until GC), and
the message contained none of the tokens fallback.ts scans for
(401/unauthorized/authentication), so it wasn't classified as an auth error.

Guard the refresh branch on `auth.kind === "chatgpt_oauth"` so api-key 401s
fall through to the `!resp.ok` branch, which reports `status 401: <body>`,
consumes the body, and yields a fallback-recognisable message. Updates the
test to assert the surfaced status + body.

Fixes #476

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AfgFeWNPZaPcWc8CSDL3H6
@TYRMars TYRMars changed the title fix(llm): keep id-less streamed tool calls (Kimi/Ollama backends) fix(llm): OpenAI-compatible streaming tool calls (#475) + api-key Responses 401 diagnostics (#476) Jul 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants