Repository navigation
fix: stop dropping index:0 from Anthropic SSE content-block events - #964
Conversation
Live end-to-end verification of PR #954's x-api-key fix (a real Anthropic Python SDK, a fresh key minted through the actual developer console) found a second, pre-existing bug that PR #954 did not touch: every streamed response's first content block (content_block_start/delta/stop) omitted the "index" key entirely, because StreamEvent.Index was a plain int tagged `json:"index,omitempty"`. Go's encoder drops a zero value under omitempty, and index 0 is the common case (the first, and usually only, content block), so the wire event carried no "index" key at all. The real Anthropic Python SDK's own streaming accumulator indexes `current_snapshot.content[event.index]` and crashed with "TypeError: list indices must be integers or slices, not NoneType" on literally the first event of any streamed call. No existing test caught it: the tests decode into map[string]interface{} and read index via the comma-ok pattern, which silently yields the same zero value on a MISSING key as on a present key holding 0. Fix: Index is now `*int` with `omitempty` on the pointer, not the value, so message_start/message_delta (which never set it) still carry no "index" key at all, while every content_block_* event gets an explicit `indexPtr(n)`, including n=0. New regression test (TestSSETranslator_FirstBlockIndexIsPresentAndZero) checks key PRESENCE via json.RawMessage rather than a lenient map decode, so it cannot pass the way the existing tests already were. Also corrects docs/anthropic-sdk-integration.md, which a previous pass wrote without ever exercising a real credential end to end: switches every example from `hive-fast` to `hive-default` (hive-fast's live route currently 404s -- Groq deprecated the `llama-3.1-8b-instant` model it maps to, and the resulting upstream error leaks LiteLLM's internal fallback-group bookkeeping into the customer-visible response), and documents a separate live finding: a console user who owns more than one workspace can have their default "current" account be one with no billing mapping, so a key minted from that state answers every request with 403 account_not_provisioned with no warning at mint time. Both are filed as separate issues; neither is fixed here. Buglog entry (for the follow-up buglog-only PR to main, per .claude/rules/openwolf.md): {"date":"2026-08-18","error_message":"real Anthropic SDK client.messages.stream() raises TypeError: list indices must be integers or slices, not NoneType on the first streamed event","root_cause":"StreamEvent.Index was `int` tagged json:\"index,omitempty\"; Go's encoder drops a zero value under omitempty, and index 0 (the first content block) is the common case, so content_block_start/delta/stop wire events carried no index key at all; the real Anthropic SDK's stream accumulator indexes content[event.index] and crashes on None","fix":"apps/edge-api/internal/anthropic/types.go: Index is now *int (omitempty applies to the pointer, not the value); apps/edge-api/internal/anthropic/stream.go: every content_block_* call site now passes indexPtr(n) explicitly, including n=0; message_start/message_delta (which never set Index) still omit the key entirely","tags":["anthropic","streaming","sse","edge-api","json-omitempty"]}
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Warning Review limit reached
Next review available in: 22 minutes Limit details: You’ve used all 1 included review currently available under your plan. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Live-tested during the same pass as the rest of this PR: OpenCode's Anthropic provider is @ai-sdk/anthropic (Vercel AI SDK), which appends only "/messages" to baseURL rather than "/v1/messages" the way the official anthropic SDK does. The example here previously carried the same baseURL as the raw-SDK examples above it (no /v1), which reproduced a real 404 at https://api-hive.scubed.co/messages. Corrected to include /v1 explicitly, and the section is honest that the round trip itself timed out with no output in this pass rather than claiming a verification that didn't happen.
Summary
This PR is the output of live end-to-end verification of PR #954 (the
x-api-key auth fix): a real Anthropic Python SDK client, a fresh API key
minted through the actual developer console
(
POST /api/v1/accounts/current/api-keys, the same call the console UIbutton makes), run against the deployed gateway.
PR #954 itself is confirmed working: a bogus
x-api-keynow correctlyanswers
401 authentication_error(not the oldmissing bearer), and avalid key reaches the model on
hive-defaultandhive-autofornon-streaming completions and a full tool-use round trip.
That same live pass found a second, pre-existing bug that #954 did not
touch and did not introduce: every streamed response's first content
block omitted the
indexkey entirely.StreamEvent.Indexwas a plaininttaggedjson:"index,omitempty". Go's encoder drops a zero value underomitempty, and index 0 is the common case (the first, and usually only,content block in a plain-text or single-tool-call reply), so the wire event
carried no
"index"key at all. The real Anthropic Python SDK's ownstreaming accumulator (
anthropic/lib/streaming/_messages.py: accumulate_event) indexescurrent_snapshot.content[event.index]andcrashed with
TypeError: list indices must be integers or slices, not NoneTypeon literally the first event of any streamed call — reproducedlive 2026-08-18 against
api-hive.scubed.cowithanthropic==0.122.0.No existing test caught this. The existing assertions decode into
map[string]interface{}and readblockStarts[0]["index"].(float64)viathe comma-ok pattern, which silently yields the same zero-value
0on amissing key as it does on a present key holding
0. That is theexact "camouflaged test" shape: green whether the bug is present or not.
Fix
apps/edge-api/internal/anthropic/types.go:StreamEvent.Indexis now*intwithomitemptyon the pointer rather than the value, somessage_start/message_delta(which never setIndex, and which thereal Anthropic protocol never puts an
indexfield on) still carry no"index"key at all, while everycontent_block_*event gets anexplicit, always-present index.
apps/edge-api/internal/anthropic/stream.go: everycontent_block_start,content_block_delta, andcontent_block_stopcall site now passesindexPtr(n)explicitly, includingn=0.TestSSETranslator_FirstBlockIndexIsPresentAndZerochecks keypresence via
json.RawMessagerather than a lenient map decode, so itfails the way the existing tests should have and now cannot silently pass
the way they did before.
Docs correction
docs/anthropic-sdk-integration.md(written by a previous pass without everexercising a real credential) is corrected against what this pass actually
ran:
model="hive-fast"tomodel="hive-default".hive-fast's live route currently 404s on both/v1/messagesand/v1/chat/completions: it maps to Groq'sllama-3.1-8b-instant, whichGroq now answers with
model_not_found("does not exist or you do nothave access to it"). Confirmed live 2026-08-18 on both surfaces. Filed as
issue (see below), not fixed in this PR: it is a DB-driven routing config
change (
supabase/migrations/20260801_14_route_groq_fast_cheapest_model.sqlset this mapping), not a code change, and out of this PR's scope.
hive-fast's failure response leaks LiteLLM's internalfallback-group bookkeeping (
Fallbacks=[{'hive-fast': ['hive-fast']}, ...],retry counts) verbatim into the customer-visible error message. No literal
provider name appears, so the existing provider-name sanitizer doesn't
catch it, but it is still internal routing implementation detail that
should never reach a caller.
owns more than one workspace (a real fixture account used for this
verification did) can have their session's default "current" account be
one with no
tenant_billing_accountsmapping. A key minted from thatdefault state looks completely normal (
200, a realhk_...secret) butanswers every
/v1/messagesrequest with403 permission_error/account_not_provisioned, with no warning anywhere in the mint flow. Theconsole does support selecting a different workspace via an
hive_account_idcookie (forwarded asX-Hive-Account-ID), but nothingin the UI surfaces which account is "current" before a key is minted.
Two issues filed from this verification pass, not fixed here
hive-fastroutes to a deprecated/unavailable Groq model, and theresulting error leaks internal routing bookkeeping. Needs a DB routing
update (out of this PR's scope) plus a look at whether the provider-blind
sanitizer should also scrub LiteLLM fallback-group text, not just
provider names.
their default account with no warning. Needs either a provisioning
backfill for orphaned accounts, a mint-time check that surfaces
account_not_provisionedbefore the key is created, or a UI affordanceshowing which workspace is "current."
PR #962 (README refresh, open, not yet merged) also uses
model="hive-fast"in both its OpenAI-SDK and Anthropic-SDK examples; flagging there directly
since it's about to publish the same broken example.
Test plan
TestSSETranslator_FirstBlockIndexIsPresentAndZero— RED before thefix (confirmed: all three content_block_* events showed
"index"absent from the wire JSON), GREEN after
apps/edge-api/internal/anthropic/...suite passesunchanged (
go test ./apps/edge-api/internal/anthropic/... -count=1 -v)apps/edge-api/...suite green(
go test ./apps/edge-api/... -count=1 -short)go build ./apps/edge-api/...andgo vet ./apps/edge-api/...cleanclient.messages.stream()againsthive-defaulton the deployed box,full event sequence checked in order, will be posted as a PR comment
once
deploy-demo-box.ymlfor the merge commit succeeds.Boundary honored
No billing, reservation, ledger, or metering code touched. No API key,
token, or credential value printed anywhere in this PR, its tests, or its
commit message.