Repository navigation
Conversation
…rompt Claude Code sends SessionStart hook context as inline system messages per the updated Anthropic Messages API. Gateway previously rejected these with 422 before conversion. Extract system-role messages and append them to the top-level system field instead of treating them as user turns. Co-authored-by: Cursor <cursoragent@cursor.com>
Tool calls are buffered until Kiro finishes the whole response, so a large tool input (e.g. Claude Code writing a big file) leaves the client without a single byte for minutes. Clients with an idle-stream watchdog treat that as a dead connection and abort: Claude Code shows "Waiting for API response ... check your network" and retries the whole turn. Add with_keepalive() in streaming_core and wrap both streaming paths: - Anthropic: `event: ping` (the API's own keepalive, skipped by SDKs) - OpenAI: an SSE comment line (`: keepalive`), ignored per the SSE spec Keepalives start only after the first real chunk, so errors raised before the stream starts are unchanged. A client disconnect cancels the pending read and closes the upstream stream. New STREAM_KEEPALIVE_INTERVAL setting (default 10s, 0 disables). Non-streaming responses are not affected.
Kiro can block a response and send only a refusal event, e.g.
{"stopDetails":{"refusal":{"category":"REASONING_EXTRACTION",...}},
"stopReason":"CONTENT_FILTERED"}. The parser had no pattern for it, so the
client got an empty end_turn. Claude Code then shows "No response requested."
or nudges the model for "no visible output", which loops and looks like a
hang. REASONING_EXTRACTION is triggered by the gateway's own fake reasoning
prompt in any conversation with history.
- parsers: parse stopDetails.refusal into a refusal event
- streaming_core: KiroEvent/StreamResult carry the refusal; the event's
content is a user-facing message (with a FAKE_REASONING=false hint when
the gateway caused it); logged as a warning
- Anthropic: stop_reason "refusal" with the message as a text block
- OpenAI: finish_reason "content_filter" with the message as content
- both streaming and non-streaming
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.
Everything this gateway needed to run Claude Code (2.1.304) reliably against Kiro, collected on one branch. Based on upstream
main@a5292ca.Commits
1.
fix(anthropic): accept messages[].role system and merge into system prompt(upstream jwadow#197, by @W0n9)Cherry-picked unchanged, author kept. Newer Claude Code sends inline
role: "system"messages; without this the gateway answers 422.2.
fix(streaming): send keepalive while the client stream is silent(upstream jwadow#288 / issue jwadow#287)Symptom: a large tool call (e.g.
Writeof a big file) stalls with "Waiting for API response … check your network", then Claude Code retries the whole turn.Why: tool calls are buffered until Kiro finishes the response, so the client gets zero bytes for minutes. Claude Code's byte-level idle watchdog (
CLAUDE_BYTE_STREAM_IDLE_TIMEOUT_MS, default 180-300 s) then aborts.Fix: a
pingevent (Anthropic) or: keepaliveSSE comment (OpenAI) everySTREAM_KEEPALIVE_INTERVALseconds (default 10,0disables) while the stream is silent.3.
fix(streaming): surface Kiro refusals instead of empty responses(new)Symptom: replies come back empty. Claude Code shows "No response requested." or sends "Your previous response had no visible output", and sessions feel stuck or very slow.
Why: Kiro now refuses the gateway's fake-reasoning prompt in any conversation with history:
The parser had no pattern for
stopDetails, so this became an emptyend_turn. Reproduced on bothclaude-sonnet-5.5andclaude-opus-5.5. A one-message request works; user → assistant → user is refused every time; the same withthinking: disabledworks.Fix: parse the refusal and report it the way each API reports a refusal:
stop_reason: "refusal", with the explanation as a text block.finish_reason: "content_filter", with the explanation as content.REASONING_EXTRACTIONand fake reasoning is on, the message and a WARNING log line say to setFAKE_REASONING=false.Deliberately not an error event: Claude Code retries
api_error, which would turn this into a retry loop.Required configuration
FAKE_REASONING=false. With it, multi-turn requests answer normally; the refusal fix only makes the failure visible.Testing
1726 passed. New tests: 15 for keepalive, 17 for refusals (parser, core, Anthropic and OpenAI, streaming and non-streaming). 7 of the refusal tests fail on the unpatched code; the other 3 check that normal behavior is unchanged.CLAUDE_BYTE_STREAM_IDLE_TIMEOUT_MS=15000finished a ~54 sWrite(662 lines) without a retry.FAKE_REASONING=true: the client now receivesstop_reason=refusalwith Kiro's explanation and the fix hint, instead of an empty reply.Note on prompt caching
Kiro applies prompt caching automatically; no gateway change is needed. Measured from Kiro's
meteringEventon a growing conversation with a ~45k-token prefix:Kiro's
cachePointfield had no effect. Thecache_read_input_tokens: 0that clients see is only the gateway's own token estimate.