You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
IAM Identity Center (Enterprise) accounts fail 100% of requests since #145 + #146. Every request returns:
{"error":{"message":"upstream truncated response without stop reason","type":"api_error"}}
Root cause: the IdC / Enterprise backend never emits metadataEvent at all, so stopReason is always absent. A complete, successful turn on this profile looks like:
assistantResponseEvent × N → contextUsageEvent → meteringEvent → clean EOF
stopReason is only parsed from metadataEvent (proxy/kiro.go, added in #145), and classifyStreamIntegrity (proxy/account_failover.go, added in #146) treats "content but no stopReason" as truncation. So on these accounts every successful response is classified as truncated, retried twice, then failed.
This is a regression, bisected: before #145 (2b93ffb) there was no stopReason concept in proxy/kiro.go and no stream_integrity.go / account_failover.go at all, so IdC accounts worked fine on v1.1.5 and earlier.
I believe this line in the #146 description is where the assumption entered:
既有夹具补了 metadataEvent stopReason 帧(真实上游会发,不是放宽断言)
That holds for Builder ID / social accounts, but not for profileArn-authenticated IdC / Enterprise profiles. The Kiro IDE source referenced in #146 is the IDE client's view, which doesn't cover this profile type.
Got the upstream truncated response without stop reason error; expected 200 with stop_reason: end_turn.
Fails identically for every model (sonnet-4 / 4.5 / 4.6, haiku-4.5, opus-4.8), both streaming and non-streaming, and all three OAuth endpoints (preferredEndpoint = kiro / codewhisperer / amazonq with endpointFallback: false).
Direct-to-upstream A/B, decoding the raw AWS event stream — same payload, once with agentTaskType: "vibe" + agentContinuationId + x-amzn-kiro-agent-mode: vibe, once without. Both returned byte-identical HTTP 200 streams, and neither contained a metadataEvent frame. Verified against both q.us-east-1.amazonaws.com and codewhisperer.us-east-1.amazonaws.com. Ruled out as causes: cache_control, model choice, max_tokens, endpoint, streaming mode, and the vibe agent-mode markers.
Decoded frames from a successful turn (Say exactly: ok):
Note: the built-in account test cannot detect this / 内置账号测试无法发现该问题
apiTestAccount (proxy/handler.go:4123) reports success throughout the outage, for two reasons:
It calls CallKiroAPIContext directly, bypassing runKiroWithIntegrityRetry and therefore classifyStreamIntegrity.
Its callback (proxy/handler.go:4166-4173) has no OnStopReason field at all, so it never looks at the stop reason — receiving any text is enough to report success.
This made the failure quite confusing to diagnose: the account test passes, credentials are valid, but every real request fails. Might be worth having the test exercise the same integrity path.
Relevant log output / 相关日志
WARN [StreamIntegrity] upstream truncated response without stop reason on <account>; retrying same account (1/2)
WARN [StreamIntegrity] upstream truncated response without stop reason on <account>; retrying same account (2/2)
WARN [StreamIntegrity] giving up after retries: upstream truncated response without stop reason
I have a working fix and would be happy to open a PR if you agree with the direction. Wanted to align on approach first, since #146 was a deliberate design decision and I don't want to weaken it.
Proposed approach: accept meteringEvent as an alternative terminal signal at clean EOF, synthesizing end_turn (or tool_use when a tool call was delivered), and only when upstream provided no stopReason of its own — upstream's value always wins.
Why meteringEvent specifically: it closes out billing for the turn, so upstream cannot emit it before the turn is finished. That preserves exactly the guarantee #146 exists for — a stream that dies mid-answer carries no metering frame, so it is still classified as truncated and still retried. contextUsageEvent deliberately does not count, since window occupancy isn't inherently a completion statement.
Diff is ~35 lines in parseEventStreamTracked, plus 4 regression tests (6 subtests) including negative cases: genuine truncation with no trailing frames, and "contextUsage only, no metering" — both must still be classified as truncated. gofmt / go vet / go test ./... -count=1 all green.
Verified end-to-end against a real IdC account: non-streaming, streaming, tool use, and the OpenAI-compatible path all return correct stop_reason / finish_reason.
If you'd prefer a different shape — e.g. gating it per auth method, or having classifyStreamIntegrity take the metering signal as an explicit parameter rather than synthesizing a stop reason in the parser — I'm happy to implement it that way instead.
Describe the bug / 问题描述
IAM Identity Center (Enterprise) accounts fail 100% of requests since #145 + #146. Every request returns:
{"error":{"message":"upstream truncated response without stop reason","type":"api_error"}}Root cause: the IdC / Enterprise backend never emits
metadataEventat all, sostopReasonis always absent. A complete, successful turn on this profile looks like:stopReasonis only parsed frommetadataEvent(proxy/kiro.go, added in #145), andclassifyStreamIntegrity(proxy/account_failover.go, added in #146) treats "content but no stopReason" as truncation. So on these accounts every successful response is classified as truncated, retried twice, then failed.This is a regression, bisected: before #145 (
2b93ffb) there was nostopReasonconcept inproxy/kiro.goand nostream_integrity.go/account_failover.goat all, so IdC accounts worked fine on v1.1.5 and earlier.I believe this line in the #146 description is where the assumption entered:
That holds for Builder ID / social accounts, but not for
profileArn-authenticated IdC / Enterprise profiles. The Kiro IDE source referenced in #146 is the IDE client's view, which doesn't cover this profile type.IdC / 企业版账号自 #145 + #146 起 100% 请求失败,一律返回上述错误。
根因:该 backend 从不发
metadataEvent,因此stopReason永远缺失。这类 profile 上一次完整成功的回答就是上面那个帧序列。而stopReason只从metadataEvent解析(#145 引入),classifyStreamIntegrity(#146 引入)又把"有内容但无 stopReason"判为截断,于是每个成功响应都被判成截断。已 bisect 确认为回归:#145(
2b93ffb)之前没有 stopReason 概念,也没有stream_integrity.go/account_failover.go,IdC 账号在 v1.1.5 及更早一直正常。Steps to reproduce / 复现步骤
authMethod: IdC/provider: Enterprise(hasprofileArn,startUrl).upstream truncated response without stop reasonerror; expected200withstop_reason: end_turn.Fails identically for every model (sonnet-4 / 4.5 / 4.6, haiku-4.5, opus-4.8), both streaming and non-streaming, and all three OAuth endpoints (
preferredEndpoint= kiro / codewhisperer / amazonq withendpointFallback: false).Direct-to-upstream A/B, decoding the raw AWS event stream — same payload, once with
agentTaskType: "vibe"+agentContinuationId+x-amzn-kiro-agent-mode: vibe, once without. Both returned byte-identical HTTP 200 streams, and neither contained ametadataEventframe. Verified against bothq.us-east-1.amazonaws.comandcodewhisperer.us-east-1.amazonaws.com. Ruled out as causes:cache_control, model choice,max_tokens, endpoint, streaming mode, and the vibe agent-mode markers.Decoded frames from a successful turn (
Say exactly: ok):Note: the built-in account test cannot detect this / 内置账号测试无法发现该问题
apiTestAccount(proxy/handler.go:4123) reportssuccessthroughout the outage, for two reasons:CallKiroAPIContextdirectly, bypassingrunKiroWithIntegrityRetryand thereforeclassifyStreamIntegrity.proxy/handler.go:4166-4173) has noOnStopReasonfield at all, so it never looks at the stop reason — receiving any text is enough to report success.This made the failure quite confusing to diagnose: the account test passes, credentials are valid, but every real request fails. Might be worth having the test exercise the same integrity path.
Relevant log output / 相关日志
Environment / 环境信息
ghcr.io/quorinex/kiro-go:latest(v1.1.5) and source atf8f6071(fix(stream): 识别空流/截断流,不再伪造成功 #146)go buildfrom sourceauthMethod: IdC,provider: Enterprise,region: us-east-1, withprofileArnAdditional Context / 补充信息
I have a working fix and would be happy to open a PR if you agree with the direction. Wanted to align on approach first, since #146 was a deliberate design decision and I don't want to weaken it.
Proposed approach: accept
meteringEventas an alternative terminal signal at clean EOF, synthesizingend_turn(ortool_usewhen a tool call was delivered), and only when upstream provided nostopReasonof its own — upstream's value always wins.Why
meteringEventspecifically: it closes out billing for the turn, so upstream cannot emit it before the turn is finished. That preserves exactly the guarantee #146 exists for — a stream that dies mid-answer carries no metering frame, so it is still classified as truncated and still retried.contextUsageEventdeliberately does not count, since window occupancy isn't inherently a completion statement.Diff is ~35 lines in
parseEventStreamTracked, plus 4 regression tests (6 subtests) including negative cases: genuine truncation with no trailing frames, and "contextUsage only, no metering" — both must still be classified as truncated.gofmt/go vet/go test ./... -count=1all green.Verified end-to-end against a real IdC account: non-streaming, streaming, tool use, and the OpenAI-compatible path all return correct
stop_reason/finish_reason.If you'd prefer a different shape — e.g. gating it per auth method, or having
classifyStreamIntegritytake the metering signal as an explicit parameter rather than synthesizing a stop reason in the parser — I'm happy to implement it that way instead.补丁已写好并通过验证,如方向认可我可以提 PR。先开 issue 对齐思路,因为 #146 是有意的设计决策,不想削弱它的保护。
方案:把
meteringEvent作为 clean EOF 处的备用终止信号(结账帧,上游不可能在回答结束前发),仅在上游未提供stopReason时生效。真截断收不到 metering 帧,仍会被判为截断,#146 的保护不受影响。contextUsageEvent刻意不计入。