Skip to content

fix: stop dropping index:0 from Anthropic SSE content-block events - #964

Merged
sakibsadmanshajib merged 2 commits into
mainfrom
fix/anthropic-stream-index-omitempty
Aug 18, 2026
Merged

sakibsadmanshajib merged 2 commits into
mainfrom
fix/anthropic-stream-index-omitempty

Conversation

@sakibsadmanshajib

Copy link
Copy Markdown
Owner

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 UI
button makes), run against the deployed gateway.

PR #954 itself is confirmed working: a bogus x-api-key now correctly
answers 401 authentication_error (not the old missing bearer), and a
valid key reaches the model on hive-default and hive-auto for
non-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 index key entirely.
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 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 own
streaming accumulator (anthropic/lib/streaming/_messages.py: accumulate_event) 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 — reproduced
live 2026-08-18 against api-hive.scubed.co with anthropic==0.122.0.

No existing test caught this. The existing assertions decode into
map[string]interface{} and read blockStarts[0]["index"].(float64) via
the comma-ok pattern, which silently yields the same zero-value 0 on a
missing key as it does on a present key holding 0. That is the
exact "camouflaged test" shape: green whether the bug is present or not.

Fix

  • apps/edge-api/internal/anthropic/types.go: StreamEvent.Index is now
    *int with omitempty on the pointer rather than the value, so
    message_start/message_delta (which never set Index, and which the
    real Anthropic protocol never puts an index field on) still carry no
    "index" key at all, while every content_block_* event gets an
    explicit, always-present index.
  • apps/edge-api/internal/anthropic/stream.go: every content_block_start,
    content_block_delta, and content_block_stop call site now passes
    indexPtr(n) explicitly, including n=0.
  • New regression test
    TestSSETranslator_FirstBlockIndexIsPresentAndZero checks key
    presence via json.RawMessage rather than a lenient map decode, so it
    fails 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 ever
exercising a real credential) is corrected against what this pass actually
ran:

  • Every example switched from model="hive-fast" to model="hive-default".
    hive-fast's live route currently 404s on both /v1/messages and
    /v1/chat/completions
    : it maps to Groq's llama-3.1-8b-instant, which
    Groq now answers with model_not_found ("does not exist or you do not
    have 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.sql
    set this mapping), not a code change, and out of this PR's scope.
  • Documents that hive-fast's failure response leaks LiteLLM's internal
    fallback-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.
  • Documents a separate live finding, unrelated to fix: normalize x-api-key before the JWT/API-key auth selector #954: a console user who
    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_accounts mapping. A key minted from that
    default state looks completely normal (200, a real hk_... secret) but
    answers every /v1/messages request with 403 permission_error /
    account_not_provisioned, with no warning anywhere in the mint flow. The
    console does support selecting a different workspace via an
    hive_account_id cookie (forwarded as X-Hive-Account-ID), but nothing
    in the UI surfaces which account is "current" before a key is minted.

Two issues filed from this verification pass, not fixed here

  1. hive-fast routes to a deprecated/unavailable Groq model, and the
    resulting 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.
  2. A multi-workspace console user can mint a dead-on-arrival API key from
    their default account with no warning. Needs either a provisioning
    backfill for orphaned accounts, a mint-time check that surfaces
    account_not_provisioned before the key is created, or a UI affordance
    showing 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 the
    fix (confirmed: all three content_block_* events showed "index"
    absent from the wire JSON), GREEN after
  • Full existing apps/edge-api/internal/anthropic/... suite passes
    unchanged (go test ./apps/edge-api/internal/anthropic/... -count=1 -v)
  • Full apps/edge-api/... suite green
    (go test ./apps/edge-api/... -count=1 -short)
  • go build ./apps/edge-api/... and go vet ./apps/edge-api/... clean
  • Live re-verification pending this PR's deploy: real SDK
    client.messages.stream() against hive-default on the deployed box,
    full event sequence checked in order, will be posted as a PR comment
    once deploy-demo-box.yml for 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.

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"]}
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Aug 18, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

@sakibsadmanshajib, you've reached your PR review limit, so we couldn't start this review.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e8e33116-c44b-47d0-a79e-a11fa504294c

📥 Commits

Reviewing files that changed from the base of the PR and between 5c00df1 and f68316b.

📒 Files selected for processing (4)
  • apps/edge-api/internal/anthropic/stream.go
  • apps/edge-api/internal/anthropic/stream_test.go
  • apps/edge-api/internal/anthropic/types.go
  • docs/anthropic-sdk-integration.md

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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.
@sakibsadmanshajib
sakibsadmanshajib merged commit 5e903c4 into main Aug 18, 2026
45 of 47 checks passed
@github-actions
github-actions Bot deleted the fix/anthropic-stream-index-omitempty branch August 18, 2026 15:28
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