Skip to content

feat: add thinking delta - #59

Merged
bobrykov merged 4 commits into
masterfrom
feat/thinking-delta
Jul 21, 2026
Merged

feat: add thinking delta#59
bobrykov merged 4 commits into
masterfrom
feat/thinking-delta

Conversation

@bobrykov

Copy link
Copy Markdown
Contributor

No description provided.

@bobrykov
bobrykov force-pushed the feat/thinking-delta branch from d2af863 to 8ce18f3 Compare July 20, 2026 19:01
@coderabbitai

coderabbitai Bot commented Jul 20, 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 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 429ddf65-d753-40e7-85da-c13f287707f3

📥 Commits

Reviewing files that changed from the base of the PR and between 32ca0ee and fac2ce0.

📒 Files selected for processing (1)
  • CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • CHANGELOG.md

📝 Walkthrough

Walkthrough

Adds dedicated thinking deltas across Anthropic, Gemini, and OpenAI streams; exposes observer callbacks; refactors streaming into handler events; bundles API inputs in StreamRequest; and updates default handler behavior, tests, mocks, and changelog documentation.

Changes

Thinking Streaming and Request API

Layer / File(s) Summary
Stream, observer, and request contracts
src/stream.rs, src/observer.rs, src/observer/context.rs, src/api.rs, CHANGELOG.md
Adds serialized thinking deltas, observer contexts, message-accumulator exclusion, and the StreamRequest API.
Event-driven stream handling
src/stream/handler.rs, src/runtime.rs, src/capabilities.rs, Cargo.toml
Refactors retries, fallback, deadlines, and rate-limit handling into HandlerEvent streams and provides passthrough handlers by default.
Provider thinking translation
src/provider/anthropic.rs, src/provider/gemini.rs, src/provider/openai.rs
Translates provider reasoning events into dedicated lanes, handles redacted or aliased reasoning, and closes lanes around text, tools, and completion.
Bare loop dispatch
src/engine/bare.rs, src/engine/bare/stream.rs, src/engine/bare/dispatch.rs
Routes handler events through the bare loop, dispatches thinking observers, preserves text callbacks, and centralizes stream bookkeeping.
Request API migration
src/structured.rs, src/testing.rs, src/reflection/llm.rs, src/provider/grammar.rs
Updates clients, structured requests, examples, tests, and mocks to use StreamRequest.

Sequence Diagram(s)

sequenceDiagram
  participant ProviderStreamEmitter
  participant StreamHandler
  participant BareLoop
  participant ObserverHost
  participant StreamAccumulator
  ProviderStreamEmitter->>StreamHandler: emit DeltaPart::Thinking
  StreamHandler->>BareLoop: yield HandlerEvent::Stream
  BareLoop->>ObserverHost: on_thinking_delta(ThinkingDeltaContext)
  BareLoop->>StreamAccumulator: ignore thinking in assembled Message
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately captures the main change: adding a new thinking delta stream variant and related support.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/thinking-delta

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: 3

Caution

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

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

1242-1267: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

on_message_stop doesn't close an open thinking part.

The added thinking_part_open/thinking_index state (lines 940-956) is closed symmetrically with text_part_open in on_block_stop, but on_message_stop — which defensively closes any block still marked open before the terminal MessageStop — only checks text_part_open and tool_parts_open. If message_stop arrives while a thinking block was never closed via content_block_stop (malformed/truncated stream), no matching PartStop is emitted for that part index, leaving an unbalanced PartStart/PartStop pair for consumers.

🔧 Proposed fix
     fn on_message_stop(&mut self) {
         self.finished = true;
         if self.text_part_open {
             self.push(StreamEvent::PartStop);
         }
+        if self.thinking_part_open {
+            self.push(StreamEvent::PartStop);
+        }
         for _ in 0..self.tool_parts_open {
             self.push(StreamEvent::PartStop);
         }
         self.tool_parts_open = 0;
         self.text_part_open = false;
+        self.thinking_part_open = false;
+        self.thinking_index = None;
         self.push(StreamEvent::MessageStop);
     }
🤖 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/anthropic.rs` around lines 1242 - 1267, Update on_message_stop
to defensively close an open thinking block using the existing
thinking_part_open state, emitting its matching PartStop before closing tool
blocks and sending MessageStop. Reset thinking_part_open consistently with
text_part_open and preserve the documented PartStop ordering.
🤖 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/stream.rs`:
- Around line 91-102: Update StreamHandler::stream_turn and its
stream_turn_via_handler caller to preserve observer callbacks when using a
StreamHandler, routing both text deltas and thinking deltas through the
configured ObserverHost/text streamer just like the inline path. Ensure
on_text_delta and on_thinking_delta are emitted without changing the existing
inline streaming behavior.

In `@src/provider/gemini.rs`:
- Around line 510-518: Update the generationConfig construction in the Gemini
request path to add thinkingConfig with includeThoughts enabled only when the
selected model supports thinking, rather than injecting it for every model.
Preserve the existing response_format and tool suppression behavior, and omit
thinkingConfig entirely for non-thinking models.
- Around line 817-892: The Gemini extract_parts and corresponding OpenAI
reasoning/text streaming paths must prevent interleaved lanes from clobbering
the single active accumulator state. Update both src/provider/gemini.rs lines
817-892 and src/provider/openai.rs lines 1082-1099 to emit a PartStop before
opening a PartStart for the other lane, or extend the shared accumulator to
track both lanes independently; preserve correct text, thinking, and tool-part
boundaries in both providers.

---

Outside diff comments:
In `@src/provider/anthropic.rs`:
- Around line 1242-1267: Update on_message_stop to defensively close an open
thinking block using the existing thinking_part_open state, emitting its
matching PartStop before closing tool blocks and sending MessageStop. Reset
thinking_part_open consistently with text_part_open and preserve the documented
PartStop ordering.
🪄 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: 1ed809d3-71b7-44fd-b422-f49ef34e83f7

📥 Commits

Reviewing files that changed from the base of the PR and between a3c4a39 and d2af863.

📒 Files selected for processing (9)
  • CHANGELOG.md
  • src/engine/bare.rs
  • src/engine/bare/stream.rs
  • src/observer.rs
  • src/observer/context.rs
  • src/provider/anthropic.rs
  • src/provider/gemini.rs
  • src/provider/openai.rs
  • src/stream.rs

Comment thread src/engine/bare/stream.rs Outdated
Comment thread src/provider/gemini.rs Outdated
Comment thread src/provider/gemini.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.

Caution

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

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

963-1010: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Iterate all parts[] when extracting a function call

extract_function_call still hardcodes /candidates/0/content/parts/0/functionCall. If a thought:true part appears before the tool call in the same chunk, this silently misses the call and skips the new closing logic. Parse the parts array and find the functionCall entry instead of assuming index 0; there’s no test covering the mixed thought + functionCall shape.

🤖 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 963 - 1010, Update extract_function_call
to iterate candidates[0].content.parts and select the part containing
functionCall instead of reading only parts[0]. Preserve the existing name/args
extraction and lane-closing plus event-emission behavior once the function-call
part is found, while doing nothing when no such part exists.
🤖 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 963-1010: Update extract_function_call to iterate
candidates[0].content.parts and select the part containing functionCall instead
of reading only parts[0]. Preserve the existing name/args extraction and
lane-closing plus event-emission behavior once the function-call part is found,
while doing nothing when no such part exists.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 744be8e7-30e3-4c75-b4ae-f96883d9c9ae

📥 Commits

Reviewing files that changed from the base of the PR and between d2af863 and 46f63f3.

📒 Files selected for processing (19)
  • CHANGELOG.md
  • Cargo.toml
  • src/api.rs
  • src/capabilities.rs
  • src/engine/bare.rs
  • src/engine/bare/dispatch.rs
  • src/engine/bare/stream.rs
  • src/observer.rs
  • src/observer/context.rs
  • src/provider/anthropic.rs
  • src/provider/gemini.rs
  • src/provider/grammar.rs
  • src/provider/openai.rs
  • src/reflection/llm.rs
  • src/runtime.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 (3)
  • CHANGELOG.md
  • src/stream.rs
  • src/provider/openai.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.

Caution

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

⚠️ Outside diff range comments (1)
CHANGELOG.md (1)

167-170: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Require a wildcard arm for downstream #[non_exhaustive] matches. Downstream crates still need _ => when matching DeltaPart; handling Thinking explicitly is optional, but it does not make the match exhaustive on its own.

🤖 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 `@CHANGELOG.md` around lines 167 - 170, Update the CHANGELOG entry for
stream::DeltaPart to state that downstream exhaustive matches require a wildcard
(_) arm because the type is #[non_exhaustive]; clarify that explicitly handling
Thinking is optional and does not replace the wildcard requirement.
🤖 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 `@CHANGELOG.md`:
- Around line 167-170: Update the CHANGELOG entry for stream::DeltaPart to state
that downstream exhaustive matches require a wildcard (_) arm because the type
is #[non_exhaustive]; clarify that explicitly handling Thinking is optional and
does not replace the wildcard requirement.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d14a9ced-ec43-4b14-8807-4e212a72a312

📥 Commits

Reviewing files that changed from the base of the PR and between 46f63f3 and 32ca0ee.

📒 Files selected for processing (2)
  • CHANGELOG.md
  • src/provider/gemini.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/provider/gemini.rs

@bobrykov
bobrykov merged commit 27ee004 into master Jul 21, 2026
7 checks passed
This was referenced Jul 22, 2026
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