Skip to content

Fix/aws non empty text#3080

Merged
Calcium-Ion merged 1 commit intoQuantumNous:mainfrom
seefs001:fix/aws-non-empty-text
Mar 2, 2026
Merged

Fix/aws non empty text#3080
Calcium-Ion merged 1 commit intoQuantumNous:mainfrom
seefs001:fix/aws-non-empty-text

Conversation

@seefs001
Copy link
Collaborator

@seefs001 seefs001 commented Mar 2, 2026

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling of function/tool calls in Claude streaming to avoid duplicate or spurious emissions.
    • Empty JSON deltas are ignored in the streaming flow unless explicit stream context indicates a real tool invocation.
    • Better lifecycle and cleanup of tool-call state to ensure correct emission timing and final finalization.
  • Tests

    • Updated and added tests verifying ignored empty deltas, correct no-argument tool emission at stream end, and ID/name preservation for argumented tool calls.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 2, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1b33df3 and 032a3ec.

📒 Files selected for processing (2)
  • relay/channel/claude/relay-claude.go
  • relay/channel/claude/relay_claude_test.go

Walkthrough

Adds per-tool streaming state tracking to Claude→OpenAI conversion: StreamResponseClaude2OpenAI now accepts a ClaudeResponseInfo pointer, records and tracks ToolCallStreamStates across content_block_start/delta/stop, emits function tool_call events only for non-empty PartialJson deltas, and emits fallback empty-args tool calls at stop when needed.

Changes

Cohort / File(s) Summary
Claude streaming logic
relay/channel/claude/relay-claude.go
Expanded StreamResponseClaude2OpenAI signature to accept claudeInfo *ClaudeResponseInfo. Added ToolCallStreamStates map[int]*ToolCallStreamState and ToolCallStreamState type. Initialize and record tool-call state on content_block_start; require non-empty PartialJson on content_block_delta to emit function tool_call (marking state.Emitted and preserving ID/Name when available); on content_block_stop emit a final empty-args tool_call if a state existed but was not emitted, then remove the state. HandleStreamResponseData now forwards claudeInfo and handles nil returns.
Tests
relay/channel/claude/relay_claude_test.go
Updated tests to call the new StreamResponseClaude2OpenAI(resp, info) signature. Adjusted expectation for empty input_json_delta (ignored when ClaudeResponseInfo is supplied). Added tests for no-arg tool emitting empty object at stop and arg-tool retaining ID/Name and Arguments on delta.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Suggested reviewers

  • Calcium-Ion

Poem

"I nibble code and track each call,
I mark the starts and tidy the fall.
When deltas whisper arguments clear,
I emit their names for all to hear.
A rabbit's hop keeps streams near." 🐇✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The PR title 'Fix/aws non empty text' is vague and does not clearly describe the actual changes, which involve refactoring Claude API streaming response handling with tool call state management. Revise the title to be more descriptive, such as 'Refactor Claude streaming tool call state management' or 'Fix Claude API tool call streaming with state tracking' to better reflect the actual changes.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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 and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
relay/channel/claude/relay-claude.go (1)

785-792: ⚠️ Potential issue | 🟠 Major

Guard nil chunks before writing SSE data

At Line 785, StreamResponseClaude2OpenAI can legitimately return nil (for example, empty input_json_delta on Line 463-469), but Line 791 still writes response. Please short-circuit when response == nil to avoid emitting invalid/no-op payloads.

Suggested patch
 		response := StreamResponseClaude2OpenAI(&claudeResponse, claudeInfo)

 		if !FormatClaudeResponseInfo(&claudeResponse, response, claudeInfo) {
 			return nil
 		}
+		if response == nil {
+			return nil
+		}

 		err = helper.ObjectData(c, response)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@relay/channel/claude/relay-claude.go` around lines 785 - 792,
StreamResponseClaude2OpenAI may return nil (e.g., empty input_json_delta), so
after calling response := StreamResponseClaude2OpenAI(&claudeResponse,
claudeInfo) short-circuit if response == nil to avoid passing a nil payload into
FormatClaudeResponseInfo and helper.ObjectData; add a nil check (if response ==
nil { return nil }) immediately after that call so the function returns early
and does not emit invalid SSE data.
🧹 Nitpick comments (1)
relay/channel/claude/relay_claude_test.go (1)

219-232: Add handler-level regression for ignored empty JSON deltas

This test verifies converter output (nil), but not stream write behavior. Please add a HandleStreamResponseData-level test to assert no chunk is written when converter returns nil.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@relay/channel/claude/relay_claude_test.go` around lines 219 - 232, Add a
handler-level test that ensures HandleStreamResponseData does not write any
chunks when the converter (StreamResponseClaude2OpenAI) returns nil for an
input_json_delta with empty PartialJson: create a ClaudeResponse like in
TestStreamResponseClaude2OpenAI_EmptyInputJSONDeltaIgnored, pass it into
HandleStreamResponseData using a mock/io.Writer or an in-memory buffer that
records writes, and assert the writer received no data; reference the existing
helper StreamResponseClaude2OpenAI and call the top-level handler
HandleStreamResponseData (or the exported handler function used in production)
to validate end-to-end write behavior rather than just converter return value.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@relay/channel/claude/relay-claude.go`:
- Around line 439-444: The current keying into claudeInfo.ToolCallStreamStates
uses the normalized fcIdx (derived via max(0, Index-1)), which collapses
zero/one indexes and causes state collisions; update the code to use the raw
Anthropic index (e.g., claudeResponse.ContentBlock.Index) as the map key instead
of fcIdx, or alternatively add a field (e.g., RawIndex) to ToolCallStreamState
and store the raw index there while mapping by the raw index, ensuring
ToolCallStreamStates, ToolCallStreamState, claudeInfo, and
claudeResponse.ContentBlock.Id/Name are updated accordingly so each tool_use
block keeps a unique state.

---

Outside diff comments:
In `@relay/channel/claude/relay-claude.go`:
- Around line 785-792: StreamResponseClaude2OpenAI may return nil (e.g., empty
input_json_delta), so after calling response :=
StreamResponseClaude2OpenAI(&claudeResponse, claudeInfo) short-circuit if
response == nil to avoid passing a nil payload into FormatClaudeResponseInfo and
helper.ObjectData; add a nil check (if response == nil { return nil })
immediately after that call so the function returns early and does not emit
invalid SSE data.

---

Nitpick comments:
In `@relay/channel/claude/relay_claude_test.go`:
- Around line 219-232: Add a handler-level test that ensures
HandleStreamResponseData does not write any chunks when the converter
(StreamResponseClaude2OpenAI) returns nil for an input_json_delta with empty
PartialJson: create a ClaudeResponse like in
TestStreamResponseClaude2OpenAI_EmptyInputJSONDeltaIgnored, pass it into
HandleStreamResponseData using a mock/io.Writer or an in-memory buffer that
records writes, and assert the writer received no data; reference the existing
helper StreamResponseClaude2OpenAI and call the top-level handler
HandleStreamResponseData (or the exported handler function used in production)
to validate end-to-end write behavior rather than just converter return value.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0689600 and f101198.

📒 Files selected for processing (2)
  • relay/channel/claude/relay-claude.go
  • relay/channel/claude/relay_claude_test.go

Comment on lines +439 to +444
if claudeInfo != nil {
claudeInfo.ToolCallStreamStates[fcIdx] = &ToolCallStreamState{
ID: claudeResponse.ContentBlock.Id,
Name: claudeResponse.ContentBlock.Name,
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Key tool-call index and state keying collision on normalized index

ToolCallStreamStates is keyed by fcIdx, which is derived from the upstream Index field using floor-to-zero normalization (line 418-421: approximately fcIdx = max(0, Index - 1)). Since Anthropic's content_block.index is zero-based and can emit sequential indexes (0, 1, 2...) for multiple tool_use blocks in a single response, the normalization causes a collision: both Index 0 and Index 1 map to fcIdx 0, causing the state for the first block to be overwritten. This breaks stop emission, which may use incorrect tool metadata.

Use raw Index directly as the key for ToolCallStreamStates, or if normalization is required for another reason, store the raw index separately in ToolCallStreamState for use during finalization.

Affects: lines 439-444 and 494-499

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@relay/channel/claude/relay-claude.go` around lines 439 - 444, The current
keying into claudeInfo.ToolCallStreamStates uses the normalized fcIdx (derived
via max(0, Index-1)), which collapses zero/one indexes and causes state
collisions; update the code to use the raw Anthropic index (e.g.,
claudeResponse.ContentBlock.Index) as the map key instead of fcIdx, or
alternatively add a field (e.g., RawIndex) to ToolCallStreamState and store the
raw index there while mapping by the raw index, ensuring ToolCallStreamStates,
ToolCallStreamState, claudeInfo, and claudeResponse.ContentBlock.Id/Name are
updated accordingly so each tool_use block keeps a unique state.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
relay/channel/claude/relay-claude.go (1)

439-443: ⚠️ Potential issue | 🟠 Major

Use a non-colliding key for ToolCallStreamStates.

ToolCallStreamStates is keyed by fcIdx, but fcIdx is normalized (see Line 418+), so different upstream indexes can collapse to the same key and overwrite tool state. That can attach wrong ID/Name or break stop emission/cleanup.

🛠️ Minimal fix (separate state key from output index)
 func StreamResponseClaude2OpenAI(claudeResponse *dto.ClaudeResponse, claudeInfo *ClaudeResponseInfo) *dto.ChatCompletionsStreamResponse {
@@
 	fcIdx := 0
+	stateKey := 0
 	if claudeResponse.Index != nil {
+		stateKey = *claudeResponse.Index
 		fcIdx = *claudeResponse.Index - 1
 		if fcIdx < 0 {
 			fcIdx = 0
 		}
 	}
@@
 				if claudeInfo != nil {
-					claudeInfo.ToolCallStreamStates[fcIdx] = &ToolCallStreamState{
+					claudeInfo.ToolCallStreamStates[stateKey] = &ToolCallStreamState{
 						ID:   claudeResponse.ContentBlock.Id,
 						Name: claudeResponse.ContentBlock.Name,
 					}
 					return nil
 				}
@@
 				if claudeInfo != nil {
-					if state, ok := claudeInfo.ToolCallStreamStates[fcIdx]; ok {
+					if state, ok := claudeInfo.ToolCallStreamStates[stateKey]; ok {
 						state.Emitted = true
 						toolCall.ID = state.ID
 						toolCall.Function.Name = state.Name
 					}
 				}
@@
-		state, ok := claudeInfo.ToolCallStreamStates[fcIdx]
+		state, ok := claudeInfo.ToolCallStreamStates[stateKey]
 		if !ok {
 			return nil
 		}
-		delete(claudeInfo.ToolCallStreamStates, fcIdx)
+		delete(claudeInfo.ToolCallStreamStates, stateKey)

Also applies to: 479-483, 498-503

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@relay/channel/claude/relay-claude.go` around lines 439 - 443,
ToolCallStreamStates is currently keyed by the normalized output index variable
fcIdx which can collide; change to use a non-colliding key (for example a
composite key that includes the original/upstream tool call index plus the
normalized fcIdx or an explicit unique ID) when setting and reading entries in
ToolCallStreamStates so state entries cannot be overwritten by different
upstream indexes. Update the assignment sites that write ToolCallStreamStates
(the block that assigns &ToolCallStreamState using
claudeResponse.ContentBlock.Id/Name near the current fcIdx usage) and the
corresponding read/cleanup sites (also the similar blocks around the other
occurrences you noted) to use the new unique key variable instead of fcIdx.
Ensure all places that delete or check ToolCallStreamStates use the same
composite key logic so lookups and removals stay consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@relay/channel/claude/relay-claude.go`:
- Around line 439-443: ToolCallStreamStates is currently keyed by the normalized
output index variable fcIdx which can collide; change to use a non-colliding key
(for example a composite key that includes the original/upstream tool call index
plus the normalized fcIdx or an explicit unique ID) when setting and reading
entries in ToolCallStreamStates so state entries cannot be overwritten by
different upstream indexes. Update the assignment sites that write
ToolCallStreamStates (the block that assigns &ToolCallStreamState using
claudeResponse.ContentBlock.Id/Name near the current fcIdx usage) and the
corresponding read/cleanup sites (also the similar blocks around the other
occurrences you noted) to use the new unique key variable instead of fcIdx.
Ensure all places that delete or check ToolCallStreamStates use the same
composite key logic so lookups and removals stay consistent.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 11f4256 and 1b33df3.

📒 Files selected for processing (2)
  • relay/channel/claude/relay-claude.go
  • relay/channel/claude/relay_claude_test.go

@seefs001 seefs001 force-pushed the fix/aws-non-empty-text branch from 1b33df3 to 032a3ec Compare March 2, 2026 11:22
@Calcium-Ion Calcium-Ion merged commit 5dcbcd9 into QuantumNous:main Mar 2, 2026
1 check was pending
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.

2 participants