Skip to content

Chore/stream handler cleanup - #64

Merged
bobrykov merged 20 commits into
masterfrom
chore/stream-handler-cleanup
Aug 1, 2026
Merged

Chore/stream handler cleanup#64
bobrykov merged 20 commits into
masterfrom
chore/stream-handler-cleanup

Conversation

@bobrykov

Copy link
Copy Markdown
Contributor

No description provided.

@bobrykov

Copy link
Copy Markdown
Contributor Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Changes

The PR adds typed provider responses, object-safe loop memory, configurable token counting, revised stream retries and timeouts, bounded provider reads, terminal-state reporting, and non-consuming tool availability checks.

Changes

Typed provider and streaming flow

Layer / File(s) Summary
Typed response contracts and provider mapping
src/api.rs, src/message.rs, src/provider/*.rs, src/structured.rs
Providers return typed messages, stop reasons, and optional usage. Structured extraction consumes Message values.
Streaming control and provider safety
src/stream/handler.rs, src/provider.rs, src/provider/openai.rs, src/provider/anthropic.rs
Timeout and retry configuration are independent. Retry jitter, typed fallback usage, bounded body reads, SSE errors, and provider stream metadata are handled explicitly.

Loop memory and context accounting

Layer / File(s) Summary
Object-safe memory integration
src/memory.rs, src/memory/builtin.rs, src/managers.rs, src/capabilities.rs
LoopMemory uses boxed futures and supports Arc<dyn LoopMemory> configuration.
Memory lifecycle and token accounting
src/engine/bare.rs, src/engine/bare/dispatch.rs, src/compact.rs, src/compact/types.rs, src/compact/truncating.rs, src/engine/core/machine.rs
The loop retrieves memory before turns, stores successful tool trajectories, consolidates successful runs, and uses configurable token estimates.

Loop terminal-state reporting

Layer / File(s) Summary
Terminal outcomes and stop reasons
src/engine/bare.rs, src/engine/bare/emission.rs, src/engine/core/lifecycle.rs
Terminal errors are stored on Run. Stop reasons and hook outcomes derive from terminal loop results.

Tool health availability inspection

Layer / File(s) Summary
Non-consuming circuit-breaker availability
src/tool/health.rs
Availability checks inspect breaker state without consuming a half-open recovery probe.

Supporting updates

Layer / File(s) Summary
Examples, mocks, dependency, and documentation
src/testing.rs, src/reflection.rs, src/reflection/llm.rs, src/tool/registry.rs, Cargo.toml, CHANGELOG.md
Examples, mocks, dependency declarations, and changelog entries use the revised contracts and behavior.

Possibly related PRs

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately identifies the substantial stream-handler cleanup, although the pull request also includes broader API, memory, compaction, and provider changes.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 50.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/stream-handler-cleanup

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/stream/handler.rs`:
- Around line 285-291: Update jittered_base_delay to incorporate per-stream or
per-request entropy when calculating the jitter factor, rather than deriving it
solely from attempt. Preserve the zero-jitter fast path and ensure concurrent
streams with the same attempt can produce different retry delays.
- Around line 1727-1729: Update the transport retry path around
retry_config.jittered_base_delay and sleep_cancellable to pass the jittered
delay through clamp_delay_to_deadline using total_deadline before sleeping.
Preserve the existing attempt increment and cancellation/error propagation while
ensuring the backoff cannot extend beyond the total stream deadline.
- Around line 1456-1460: Update the invalid-configuration branches in
src/stream/handler.rs:1456-1460 and src/stream/handler.rs:1484-1488 so they
assign StreamTimeoutConfig::default() and StreamRetryConfig::default(),
respectively, when validation fails. Preserve the existing warning logs and
valid-configuration assignments.

In `@src/tool/health.rs`:
- Around line 614-631: Update would_be_half_open so it reports whether the next
allow_request call will transition into or operate as a HalfOpen probe: return
true only for an Open breaker whose recovery duration has elapsed, and false
when already HalfOpen or Closed. Revise the method’s rustdoc to describe this
next-request behavior and clarify that an existing HalfOpen state has a probe in
flight.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c40d2150-878c-4d35-a98f-a3a1accc591c

📥 Commits

Reviewing files that changed from the base of the PR and between a64839a and ac80a1e.

📒 Files selected for processing (14)
  • CHANGELOG.md
  • src/compact.rs
  • src/engine/bare.rs
  • src/engine/bare/emission.rs
  • src/engine/core/lifecycle.rs
  • src/managers.rs
  • src/provider.rs
  • src/provider/anthropic.rs
  • src/provider/gemini.rs
  • src/provider/openai.rs
  • src/reflection.rs
  • src/stream/handler.rs
  • src/tool/health.rs
  • src/tool/registry.rs

Comment thread src/stream/handler.rs
Comment thread src/stream/handler.rs
Comment thread src/stream/handler.rs Outdated
Comment thread src/tool/health.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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)
src/engine/bare.rs (1)

1510-1555: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Memory retrieval query is empty on every turn after a tool call.

turn_input (used as the memory.retrieve(&turn_input, 3) query) is derived by filtering the previous history message's parts for Text — but on any turn following a tool call, that message is a ToolResult-only message, so turn_input is "". In InMemoryStore::retrieve, an empty query makes query_words empty, so the word-overlap bonus is always 0.0, and retrieval degenerates to "top base-relevance entries" instead of anything actually relevant to the current turn — defeating the documented purpose ("retrieves relevant entries as context before each turn").

💡 Suggested fix: fall back to the run's original input when turn_input is empty
-        if let Some(memory) = self.managers.memory() {
-            match memory.retrieve(&turn_input, 3).await {
+        if let Some(memory) = self.managers.memory() {
+            let query = if turn_input.is_empty() {
+                self.current_run().map_or(String::new(), |r| r.input.clone())
+            } else {
+                turn_input.clone()
+            };
+            match memory.retrieve(&query, 3).await {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/engine/bare.rs` around lines 1510 - 1555, Update turn_input construction
in handle_call_llm so that when the history-based input after a tool call is
empty, it falls back to the current run’s original input from
current_run().input. Preserve the existing first-turn and non-empty history
behavior, ensuring memory.retrieve receives a meaningful query on every turn.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/engine/bare/dispatch.rs`:
- Around line 786-812: Update record_tool_memory to bound the persisted
trajectory content before constructing MemoryEntry, truncating both tc.input and
tool_result.output to an explicit maximum length while preserving the existing
tool name, category, and store behavior. Use a clear shared cap and ensure
truncation cannot produce unbounded prompt content when entries are later
reused.

---

Outside diff comments:
In `@src/engine/bare.rs`:
- Around line 1510-1555: Update turn_input construction in handle_call_llm so
that when the history-based input after a tool call is empty, it falls back to
the current run’s original input from current_run().input. Preserve the existing
first-turn and non-empty history behavior, ensuring memory.retrieve receives a
meaningful query on every turn.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a9fd9531-20b7-414d-836f-0d5c3ad088b9

📥 Commits

Reviewing files that changed from the base of the PR and between ac80a1e and 42b39a5.

📒 Files selected for processing (10)
  • CHANGELOG.md
  • Cargo.toml
  • src/capabilities.rs
  • src/engine/bare.rs
  • src/engine/bare/dispatch.rs
  • src/managers.rs
  • src/memory.rs
  • src/memory/builtin.rs
  • src/stream/handler.rs
  • src/tool/health.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • CHANGELOG.md

Comment thread src/engine/bare/dispatch.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

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

⚠️ Outside diff range comments (4)
src/stream/handler.rs (3)

1531-1540: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the leftover doc fragment.

Line 1536 is a remnant of the removed with_config documentation. It has an unmatched parenthesis and contradicts the new independent setters.

📝 Proposed doc fix
     /// Read-only access to the [`StreamRetryConfig`] stored on the handler.
     /// Mutate via
     /// [`with_retry_config`](Self::with_retry_config).
-    /// both retry and timeout together); there is no per-field setter.
     #[must_use]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/stream/handler.rs` around lines 1531 - 1540, Remove the stray “both retry
and timeout together” documentation fragment, including its unmatched
parenthesis, from the `retry_config` method docs. Keep the remaining description
of read-only access and mutation via `with_retry_config` intact.

1765-1790: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Move the rate_limit_retry doc block back to its function.

Lines 1765-1777 document rate_limit_retry (they describe RateLimitRetry::HardStop, Escalate, and Retry). They are now contiguous with the decide_rate_limit_error doc block that starts at line 1778, so both blocks attach to decide_rate_limit_error. rate_limit_retry at line 1850 is left undocumented, and the merged rustdoc describes two different decisions.

Move lines 1765-1777 directly above fn rate_limit_retry.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/stream/handler.rs` around lines 1765 - 1790, Move the rustdoc paragraph
describing count increments and the RateLimitRetry::HardStop, Escalate, and
Retry outcomes from above decide_rate_limit_error to directly above
rate_limit_retry. Keep the decide_rate_limit_error documentation focused only on
mapping rate-limit stream errors to ErrorAction.

2221-2233: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Propagate fallback usage in drive_turn so the test asserts the real contract.

drive_turn discards the new usage field and hardcodes usage: None. The test at lines 3503-3506 then asserts result.usage.is_none(), and its comment states that the non-streaming path does not carry token counts. The mock returns Some(Usage::default()), and the production consumer BareLoop::stream_turn in src/engine/bare/stream.rs returns fallback_usage. The assertion now hides a regression in usage propagation.

💚 Proposed test fix
                     HandlerEvent::Fallback {
                         message,
                         stop_reason: fallback_stop_reason,
-                        ..
+                        usage: fallback_usage,
                     } => {
                         from_fallback = true;
                         return Ok(DriveResult {
                             message,
-                            usage: None,
+                            usage: fallback_usage,
                             stop_reason: fallback_stop_reason,
                             from_fallback,
                         });
                     }

And at lines 3503-3506:

-        // Usage is None on the fallback path (non-streaming JSON doesn't
-        // reliably carry token counts).
-        assert!(result.usage.is_none());
+        // Usage comes from the typed non-streaming response.
+        assert_eq!(result.usage, Some(Usage::default()));

Also applies to: 3503-3506

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/stream/handler.rs` around lines 2221 - 2233, Update the
HandlerEvent::Fallback branch in drive_turn to propagate the fallback event’s
usage value into DriveResult instead of hardcoding usage: None, preserving the
existing message, stop_reason, and from_fallback fields. Update the related test
assertion and comment to expect the mock’s Some(Usage::default()) result,
validating real usage propagation.
src/engine/bare.rs (1)

1596-1601: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not inject tool-derived memory as system instructions.

Lines 1596-1601 add MemoryEntry.memory as Role::System. record_tool_memory stores successful tool input and output, so untrusted tool content can become high-priority instructions on a later model call.

Encode retrieved memory as lower-trust data and explicitly delimit it as untrusted reference content.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/engine/bare.rs` around lines 1596 - 1601, Update the contributor_messages
construction around record_tool_memory retrieval so the memory is not emitted
with Role::System. Encode the retrieved summary as lower-trust reference content
using the appropriate non-instruction role, and explicitly delimit it as
untrusted data while preserving the existing “Relevant memory” context.
🧹 Nitpick comments (1)
src/testing.rs (1)

736-748: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Align the non-streaming mock with the streaming mock shape.

stream_messages emits a text part and a tool-call part, and reports Usage::new(50, 25). create_message drops the text when tool_call is set, and reports Usage::default() (zeros). Tests that exercise the streaming-to-non-streaming fallback then observe a different message shape and different token counts depending on which path ran. That weakens coverage for the fallback behavior this PR changes.

♻️ Proposed fix
         let response = self.pop_response();
         Box::pin(async move {
             let stop_reason = crate::stream::StreamStopReason::from_api_str(&response.stop_reason)
                 .unwrap_or(crate::stream::StreamStopReason::EndTurn);
-            let parts = if let Some(tc) = response.tool_call {
-                vec![MessagePart::tool_call(tc.id, tc.name, tc.input)]
-            } else {
-                vec![MessagePart::text(response.text)]
-            };
+            let mut parts = vec![MessagePart::text(response.text)];
+            if let Some(tc) = response.tool_call {
+                parts.push(MessagePart::tool_call(tc.id, tc.name, tc.input));
+            }
             Ok(crate::api::NonStreamingResponse {
                 message: Message::new(Role::Assistant, parts),
                 stop_reason,
-                usage: Some(crate::stream::Usage::default()),
+                usage: Some(Usage::new(50, 25)),
             })
         })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/testing.rs` around lines 736 - 748, Update the non-streaming mock in
create_message to match stream_messages: always include the response text part,
append the tool-call part when response.tool_call is present, and report
Usage::new(50, 25) instead of Usage::default(). Preserve the existing
stop-reason conversion and response construction.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/compact.rs`:
- Around line 113-132: Clean up the HeuristicTokenCounter documentation by
removing the duplicated introductory block and the misleading “Presets” section.
Keep one accurate description that refers to the implemented CHARS_PER_TOKEN
constant rather than the nonexistent bytes_per_token parameter.

In `@src/engine/bare.rs`:
- Around line 677-680: Update set_token_counter so the newly installed counter
is also propagated to the configured ContextManager, preserving synchronization
regardless of setter order; alternatively reject calls made after
context-manager configuration. Add tests covering set_token_counter before and
after set_context_manager.
- Around line 1679-1680: Update the context token calculation in the
model-response handling flow around model_response so it includes the current
model message before estimating context size. Ensure the message that
model_response appends to pending history is represented in the counted history,
allowing compaction to trigger before the context budget is exceeded.

In `@src/engine/bare/dispatch.rs`:
- Around line 7-15: Update truncate_to to determine whether truncation is needed
using character indexing rather than s.len() byte length. Check for a character
at max_len and return s unchanged when none exists; otherwise retain only the
first max_len characters and append the ellipsis.

In `@src/message.rs`:
- Around line 412-419: Add the serde default attribute to the name field in
MessagePart::ToolResult so deserialization of older serialized tool-result data
missing this field succeeds. Preserve the existing String type and current
behavior for newly serialized results.

In `@src/provider/gemini.rs`:
- Around line 169-181: Update the function-call id sentence in the
NonStreamingResponse documentation to state that the parsed functionCall.id is
preserved, matching the implementation and
build_response_parses_function_call_id assertion; remove the claim that Gemini
calls have no id or that it is left empty.

---

Outside diff comments:
In `@src/engine/bare.rs`:
- Around line 1596-1601: Update the contributor_messages construction around
record_tool_memory retrieval so the memory is not emitted with Role::System.
Encode the retrieved summary as lower-trust reference content using the
appropriate non-instruction role, and explicitly delimit it as untrusted data
while preserving the existing “Relevant memory” context.

In `@src/stream/handler.rs`:
- Around line 1531-1540: Remove the stray “both retry and timeout together”
documentation fragment, including its unmatched parenthesis, from the
`retry_config` method docs. Keep the remaining description of read-only access
and mutation via `with_retry_config` intact.
- Around line 1765-1790: Move the rustdoc paragraph describing count increments
and the RateLimitRetry::HardStop, Escalate, and Retry outcomes from above
decide_rate_limit_error to directly above rate_limit_retry. Keep the
decide_rate_limit_error documentation focused only on mapping rate-limit stream
errors to ErrorAction.
- Around line 2221-2233: Update the HandlerEvent::Fallback branch in drive_turn
to propagate the fallback event’s usage value into DriveResult instead of
hardcoding usage: None, preserving the existing message, stop_reason, and
from_fallback fields. Update the related test assertion and comment to expect
the mock’s Some(Usage::default()) result, validating real usage propagation.

---

Nitpick comments:
In `@src/testing.rs`:
- Around line 736-748: Update the non-streaming mock in create_message to match
stream_messages: always include the response text part, append the tool-call
part when response.tool_call is present, and report Usage::new(50, 25) instead
of Usage::default(). Preserve the existing stop-reason conversion and response
construction.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0f0b8d47-c94e-4506-a0a3-404a890e2e4b

📥 Commits

Reviewing files that changed from the base of the PR and between 42b39a5 and 27c9c32.

📒 Files selected for processing (21)
  • CHANGELOG.md
  • src/api.rs
  • src/compact.rs
  • src/compact/truncating.rs
  • src/compact/types.rs
  • src/engine/bare.rs
  • src/engine/bare/dispatch.rs
  • src/engine/bare/message.rs
  • src/engine/bare/stream.rs
  • src/engine/core/lifecycle.rs
  • src/engine/core/machine.rs
  • src/message.rs
  • src/provider.rs
  • src/provider/anthropic.rs
  • src/provider/gemini.rs
  • src/provider/openai.rs
  • src/reflection/llm.rs
  • src/stream.rs
  • src/stream/handler.rs
  • src/structured.rs
  • src/testing.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/provider.rs

Comment thread src/compact.rs Outdated
Comment thread src/engine/bare.rs
Comment thread src/engine/bare.rs Outdated
Comment thread src/engine/bare/dispatch.rs
Comment thread src/message.rs
Comment thread src/provider/gemini.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (3)
src/provider/gemini.rs (1)

715-740: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Validate legacy ToolResult names before building Gemini requests.

Old tool results deserialize with name == "", and this serializer copies that value unconditionally into functionResponse.name. Recover the missing function name from a matching ToolCall by call_id, or reject the request before sending it. Add a round-trip test that deserializes legacy tool-result JSON and passes it through build_request_body; the existing test only checks deserialization.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/provider/gemini.rs` around lines 715 - 740, Update the Gemini
request-building flow around MessagePart::ToolResult to validate empty result
names before constructing functionResponse. Resolve a missing name by matching
call_id against the corresponding MessagePart::ToolCall, or reject the request
if no match exists; preserve existing names. Extend the relevant round-trip test
to deserialize legacy tool-result JSON and pass it through build_request_body,
asserting the recovered name or expected rejection.
src/provider/openai.rs (2)

190-195: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Reject invalid OpenAI tool argument JSON.

build_response currently turns any function.arguments parse failure into ToolCall { input: {} }, so malformed JSON such as "hi", {, or {"q"becomes the same no-argument call as"". In non-streaming responses the argument parsing takes place inside build_response, so return an ApiResponseInvalidfor non-empty invalid JSON; only missing or empty arguments should default to{}`. Update the OpenAI argument tests to cover both malformed and omitted arguments.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/provider/openai.rs` around lines 190 - 195, Update build_response’s
function.arguments handling to default to an empty object only when arguments
are missing or empty, and return ApiResponseInvalid when non-empty JSON fails to
parse. Ensure this behavior applies to non-streaming responses, and extend the
OpenAI argument tests to cover malformed and omitted arguments.

208-211: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Normalize zero OpenAI usage to None in both response paths.

Map’s usage field is missing by OpenAI default, but the Usage struct is zeroed by default when a provider sends empty or absent usage values. The streaming path still carries that zeroed value from the empty choices usage-only chunk. If a downstream model supplies zero input and output counts, return None instead of Some(Usage::default()) so it is not treated as a final token count.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/provider/openai.rs` around lines 208 - 211, Normalize zero-valued OpenAI
usage to None in both response paths, including the raw response mapping around
OpenAiUsage::deserialize and the streaming usage-only chunk handling. After
constructing Usage, return Some only when input and output token counts are not
both zero; otherwise return None, preserving existing behavior for nonzero
usage.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/provider/gemini.rs`:
- Around line 715-740: Update the Gemini request-building flow around
MessagePart::ToolResult to validate empty result names before constructing
functionResponse. Resolve a missing name by matching call_id against the
corresponding MessagePart::ToolCall, or reject the request if no match exists;
preserve existing names. Extend the relevant round-trip test to deserialize
legacy tool-result JSON and pass it through build_request_body, asserting the
recovered name or expected rejection.

In `@src/provider/openai.rs`:
- Around line 190-195: Update build_response’s function.arguments handling to
default to an empty object only when arguments are missing or empty, and return
ApiResponseInvalid when non-empty JSON fails to parse. Ensure this behavior
applies to non-streaming responses, and extend the OpenAI argument tests to
cover malformed and omitted arguments.
- Around line 208-211: Normalize zero-valued OpenAI usage to None in both
response paths, including the raw response mapping around
OpenAiUsage::deserialize and the streaming usage-only chunk handling. After
constructing Usage, return Some only when input and output token counts are not
both zero; otherwise return None, preserving existing behavior for nonzero
usage.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a47bf00f-5768-4064-bc90-cafb7dd24af8

📥 Commits

Reviewing files that changed from the base of the PR and between 27c9c32 and dc748cb.

📒 Files selected for processing (12)
  • CHANGELOG.md
  • src/compact.rs
  • src/engine/bare.rs
  • src/engine/bare/dispatch.rs
  • src/engine/core/machine.rs
  • src/message.rs
  • src/provider.rs
  • src/provider/anthropic.rs
  • src/provider/gemini.rs
  • src/provider/openai.rs
  • src/provider/sse.rs
  • src/stream/handler.rs
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/provider.rs
  • src/engine/bare/dispatch.rs
  • src/compact.rs
  • src/engine/core/machine.rs
  • src/stream/handler.rs
  • src/engine/bare.rs

@bobrykov
bobrykov merged commit a7c0ef6 into master Aug 1, 2026
7 checks passed
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