Skip to content

feat(llm): virtualize provider interface behind a normalized Adapter - #1683

Merged
dsapandora merged 25 commits into
developfrom
feat/llm-provider-adapter
Jul 31, 2026
Merged

feat(llm): virtualize provider interface behind a normalized Adapter#1683
dsapandora merged 25 commits into
developfrom
feat/llm-provider-adapter

Conversation

@dsapandora

@dsapandora dsapandora commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator

Summary

Virtualizes the LLM provider interface behind a normalized Event/Adapter layer so ChatBase never touches provider-native content shapes (RFC — discussion #1679). Single-turn.

  • Normalized interfaceEvent(thinking|text|done) + an Adapter protocol that owns provider-native history with opaque items. A drive_adapter shim fans the Event stream into the existing on_chunk / on_reasoning_chunk callbacks, so the node / SSE thinking surface is unchanged.
  • Three adaptersLangChainAdapter (the ~18 non-reasoning providers, no raw-SDK rewrite), NativeAnthropicAdapter (Messages API), NativeOpenAIResponsesAdapter (Responses API). Every provider path now flows through the same interface.
  • Crash fix (Anthropic typed-block content not flattened on the non-streaming path — breaks every agent node #1658) — the non-streaming / expectJson (agent) path returned Anthropic's typed-block list verbatim and crashed on .strip(). _chat now drains the adapter and returns a plain string; content handling is one mechanism on every path (the "unify" — _chat_with_retries's invoke branch is gone).
  • Extended-thinking control — replaces the interim hard-off (fix(llm_anthropic): disable extended thinking at the node (interim stop-gap) #1681) with a per-node extendedThinking toggle (default off). Thinking is no longer baked into the client; the native adapter adds it per call, so it rides the interactive streaming path only — agent / expectJson calls never get it (context gate). Model-gated to reasoning-capable models.
  • Base cleanup — the provider-shape branches (isinstance(content, list), thinking/text/signature block parsing, betas, reasoning_content, Responses vs Messages) move into the adapters; the stream content parser is a shared, tested seam.

Type

refactor + feat + fix

Testing

  • Unit + integration tests added: adapters, drive_adapter shim, chat_string wiring, native adapters, per-call thinking injection, non-streaming drain.
  • packages/ai/tests/ai/common suite green (605 passed), no regressions.
  • Updated the crewai native stop-threading test to drive the adapter (same payload wiring).
  • Live run of the real Anthropic / OpenAI reasoning paths — pending (no API keys in CI).

Checklist

Notes

Linked Issue

Fixes #1658

Summary by CodeRabbit

  • New Features
    • Added an opt-in Extended Thinking setting for supported Anthropic reasoning models (default off).
  • Improved Streaming
    • Streaming is now normalized so visible text and reasoning are handled separately with consistent finish-reason reporting.
    • Stop-sequence handling is preserved during streaming.
  • Bug Fixes
    • Improved completion/finish-reason behavior when streaming isn’t available.
  • Tests
    • Added/expanded coverage for output parsing, reasoning handling, completion states, stop-sequence wiring, and Extended Thinking configuration.

@coderabbitai

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

The PR adds normalized streaming adapters and content parsing, routes ChatBase execution through them, enables opt-in Anthropic extended thinking for reasoning models, exposes the setting in service profiles, and synchronizes the field based on model capabilities.

Changes

Unified LLM streaming and Anthropic thinking

Layer / File(s) Summary
Normalized adapter contract and content parsing
packages/ai/src/ai/common/llm_adapter.py, packages/ai/tests/ai/common/test_chat_content.py, packages/ai/tests/ai/common/test_llm_adapter*.py
Adds normalized events, adapter driving, reasoning/text parsing, think-tag splitting, content flattening, and unit tests.
Provider streaming adapters
packages/ai/src/ai/common/llm_adapter.py, packages/ai/src/ai/common/llm_native_stream.py, packages/ai/tests/ai/common/test_*adapter.py, nodes/test/agent_crewai/test_stop_words.py
Adds LangChain, Anthropic, and OpenAI Responses adapters with history and finish-reason handling.
ChatBase adapter routing
packages/ai/src/ai/common/chat.py, packages/ai/tests/ai/common/test_chat_string_wiring.py
Replaces inline streaming parsing with adapters and adds conditional non-streaming fallback behavior.
Anthropic thinking configuration
nodes/src/nodes/llm_anthropic/anthropic.py, nodes/src/nodes/llm_anthropic/services.json
Adds the extendedThinking option and enables thinking only for configured reasoning models.
Extended-thinking model synchronization
tools/sync_models/src/core/patcher.py, tools/sync_models/src/providers/base.py, tools/sync_models/test/test_extended_thinking_field.py
Adds or removes the UI field during profile repair and creation according to reasoning capabilities, with tests.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant ChatBase
  participant Adapter
  participant Provider
  Caller->>ChatBase: chat_string(prompt)
  ChatBase->>Adapter: stream(prompt)
  Adapter->>Provider: consume provider stream
  Provider-->>Adapter: thinking and text events
  Adapter-->>ChatBase: normalized events
  ChatBase-->>Caller: callbacks and final text
Loading

Possibly related PRs

Suggested reviewers: jmaionchi, stepmikhaylov, rod-christensen

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.91% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: moving provider interactions behind a normalized Adapter interface.
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 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/llm-provider-adapter

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.

@github-actions github-actions Bot added the module:ai AI/ML modules label Jul 26, 2026
@github-actions

Copy link
Copy Markdown
Contributor
🤖 Internal: Discord sync marker

Auto-managed by the Discord notification workflow. Stores the linked Discord message ID and forum thread ID. Do not edit or delete.

@dsapandora
dsapandora force-pushed the feat/llm-provider-adapter branch from 2f016e1 to 46d6a16 Compare July 28, 2026 08:56
@github-actions github-actions Bot added the module:nodes Python pipeline nodes label Jul 28, 2026
@dsapandora
dsapandora marked this pull request as ready for review July 28, 2026 15:05

@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 `@nodes/src/nodes/llm_anthropic/services.json`:
- Around line 508-515: Remove "extendedThinking" from the properties array for
the anthropic.claude-3-haiku entry, leaving its API key and modelSource
properties unchanged.

In `@packages/ai/src/ai/common/chat.py`:
- Around line 406-411: Update the native OpenAI Responses path around
NativeOpenAIResponsesAdapter and drive_adapter to raise an exception when no
text is produced, matching the Anthropic native stream behavior. Ensure this
occurs before on_finish and return, so failed streams with finish_reason='error'
enter the existing fallback logic, while successful responses containing text
retain the current flow.

In `@packages/ai/src/ai/common/llm_adapter.py`:
- Around line 173-278: Remove the unused AnthropicAdapter and OpenAIAdapter
classes from this module, since production routing uses NativeAnthropicAdapter
and NativeOpenAIResponsesAdapter instead. Preserve LangChainAdapter and any
shared functionality that is actively referenced.
- Around line 288-322: Update NativeOpenAIResponsesAdapter.stream() at the
responses.create(...) call to pass store=False explicitly, matching
OpenAIAdapter’s stateless behavior. Add or synchronize the nearby
rationale/documentation so it states that Responses API requests and responses
should not be stored.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1739171e-5c99-4817-aea9-b8ba208604b8

📥 Commits

Reviewing files that changed from the base of the PR and between 1483794 and 877c29d.

📒 Files selected for processing (15)
  • nodes/src/nodes/llm_anthropic/anthropic.py
  • nodes/src/nodes/llm_anthropic/services.json
  • nodes/test/agent_crewai/test_stop_words.py
  • packages/ai/src/ai/common/chat.py
  • packages/ai/src/ai/common/llm_adapter.py
  • packages/ai/src/ai/common/llm_native_stream.py
  • packages/ai/tests/ai/common/test_chat_content.py
  • packages/ai/tests/ai/common/test_chat_string_wiring.py
  • packages/ai/tests/ai/common/test_llm_adapter.py
  • packages/ai/tests/ai/common/test_llm_adapter_anthropic.py
  • packages/ai/tests/ai/common/test_llm_adapter_drive.py
  • packages/ai/tests/ai/common/test_llm_adapter_langchain.py
  • packages/ai/tests/ai/common/test_llm_adapter_openai.py
  • packages/ai/tests/ai/common/test_native_anthropic_adapter.py
  • packages/ai/tests/ai/common/test_native_openai_responses_adapter.py

Comment thread nodes/src/nodes/llm_anthropic/services.json
Comment thread packages/ai/src/ai/common/chat.py
Comment thread packages/ai/src/ai/common/llm_adapter.py
Comment thread packages/ai/src/ai/common/llm_adapter.py
@dsapandora
dsapandora marked this pull request as draft July 28, 2026 15:42
@dsapandora
dsapandora marked this pull request as ready for review July 28, 2026 16:28

@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 (2)
nodes/src/nodes/llm_anthropic/services.json (1)

319-366: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Place extendedThinking before llm.cloud.modelSource.

The affected arrays currently append extendedThinking after llm.cloud.modelSource, but tools/sync_models/src/core/patcher.py Lines 338-345 and tools/sync_models/test/test_extended_thinking_field.py Lines 26-28 require llm.cloud.modelSource to be last. Reorder every affected array so the checked-in schema matches the synchronization contract.

Proposed fix
-				"llm.cloud.modelSource",
-				"extendedThinking"
+				"extendedThinking",
+				"llm.cloud.modelSource"

Also applies to: 500-505, 519-608

🤖 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 `@nodes/src/nodes/llm_anthropic/services.json` around lines 319 - 366, Reorder
the properties arrays for every affected Anthropic model entry, including
anthropic.custom and the listed Claude models, so extendedThinking appears
before llm.cloud.modelSource and llm.cloud.modelSource remains the final
property. Apply this consistently across all occurrences covered by the review.
tools/sync_models/src/core/patcher.py (1)

280-305: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document the new profiles argument.

_repair_field_objects now accepts profiles and uses it to add or remove extendedThinking, but the docstring only documents fields. Add the argument and its capability-aware behavior.

Proposed documentation update
     Args:
         fields: The ``"fields"`` dict to mutate in-place
+        profiles: Optional complete profile mapping used to determine whether
+                  each model supports reasoning
🤖 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 `@tools/sync_models/src/core/patcher.py` around lines 280 - 305, Update the
_repair_field_objects docstring to document the profiles argument, including
that it is used to determine capability-aware additions or removals of the
extendedThinking field. Also describe the expected optional mapping shape or
absence behavior consistently with the function signature, without changing
implementation logic.
🤖 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 `@nodes/src/nodes/llm_anthropic/services.json`:
- Around line 319-366: Reorder the properties arrays for every affected
Anthropic model entry, including anthropic.custom and the listed Claude models,
so extendedThinking appears before llm.cloud.modelSource and
llm.cloud.modelSource remains the final property. Apply this consistently across
all occurrences covered by the review.

In `@tools/sync_models/src/core/patcher.py`:
- Around line 280-305: Update the _repair_field_objects docstring to document
the profiles argument, including that it is used to determine capability-aware
additions or removals of the extendedThinking field. Also describe the expected
optional mapping shape or absence behavior consistently with the function
signature, without changing implementation logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: dc76eb4a-7050-44b9-b7c4-842bcc34a8f3

📥 Commits

Reviewing files that changed from the base of the PR and between 877c29d and c2f97bd.

📒 Files selected for processing (6)
  • nodes/src/nodes/llm_anthropic/services.json
  • packages/ai/src/ai/common/chat.py
  • packages/ai/src/ai/common/llm_adapter.py
  • tools/sync_models/src/core/patcher.py
  • tools/sync_models/src/providers/base.py
  • tools/sync_models/test/test_extended_thinking_field.py

@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)
tools/sync_models/src/core/patcher.py (1)

283-305: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document the new toggle behavior in both helper docstrings.

_repair_field_objects now accepts profiles, but its Args section omits that parameter. _update_fields_for_added also conditionally adds extendedThinking, while its docstring still describes only the original three entries. Document the profile shape, None behavior, and conditional property ordering.

Also applies to: 357-390

🤖 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 `@tools/sync_models/src/core/patcher.py` around lines 283 - 305, Update the
docstrings for _repair_field_objects and _update_fields_for_added to document
the profiles parameter, including its expected shape and behavior when it is
None. In _update_fields_for_added, describe that extendedThinking is
conditionally added and specify the resulting property ordering, while
preserving documentation of the existing entries.
🤖 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 `@tools/sync_models/test/test_extended_thinking_field.py`:
- Around line 45-54: Update test_repair_skips_custom to include a custom object
whose properties omit extendedThinking, then run _repair_field_objects and
assert the property remains absent. Preserve the existing custom-object coverage
while ensuring the test fails if repair incorrectly adds the toggle to custom
objects.

---

Outside diff comments:
In `@tools/sync_models/src/core/patcher.py`:
- Around line 283-305: Update the docstrings for _repair_field_objects and
_update_fields_for_added to document the profiles parameter, including its
expected shape and behavior when it is None. In _update_fields_for_added,
describe that extendedThinking is conditionally added and specify the resulting
property ordering, while preserving documentation of the existing entries.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2f0b6ef9-caac-40ab-92b9-09dd682dfa17

📥 Commits

Reviewing files that changed from the base of the PR and between c2f97bd and 2c91ecb.

📒 Files selected for processing (2)
  • tools/sync_models/src/core/patcher.py
  • tools/sync_models/test/test_extended_thinking_field.py

Comment thread tools/sync_models/test/test_extended_thinking_field.py 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 (1)
nodes/src/nodes/llm_anthropic/services.json (1)

319-367: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep llm.cloud.modelSource as the final property.

The synchronization contract in tools/sync_models/src/core/patcher.py:283-354 explicitly keeps llm.cloud.modelSource last, but these new arrays append extendedThinking after it. Reorder the entries consistently to avoid schema/UI ordering being repaired only later.

Proposed fix
 "properties": [
     "llm.cloud.apikey",
-    "llm.cloud.modelSource",
-    "extendedThinking"
+    "extendedThinking",
+    "llm.cloud.modelSource"
 ]

Apply this ordering to every affected profile.

Also applies to: 500-506, 515-609

🤖 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 `@nodes/src/nodes/llm_anthropic/services.json` around lines 319 - 367, Reorder
the properties arrays for every affected Anthropic profile, including
anthropic.custom and the listed Claude model profiles, so extendedThinking
appears before llm.cloud.modelSource and llm.cloud.modelSource remains the final
entry. Preserve all existing properties and apply the same ordering consistently
across the affected profiles.
🤖 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 `@nodes/src/nodes/llm_anthropic/services.json`:
- Around line 319-367: Reorder the properties arrays for every affected
Anthropic profile, including anthropic.custom and the listed Claude model
profiles, so extendedThinking appears before llm.cloud.modelSource and
llm.cloud.modelSource remains the final entry. Preserve all existing properties
and apply the same ordering consistently across the affected profiles.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e8289913-f116-420b-8a5c-dde9ee339616

📥 Commits

Reviewing files that changed from the base of the PR and between 2c91ecb and f673c95.

📒 Files selected for processing (1)
  • nodes/src/nodes/llm_anthropic/services.json

@dsapandora
dsapandora force-pushed the feat/llm-provider-adapter branch from f673c95 to 8314681 Compare July 29, 2026 22:59

@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: 2

Caution

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

⚠️ Outside diff range comments (1)
nodes/src/nodes/llm_anthropic/services.json (1)

645-657: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Expose the setting for both new reasoning profiles.

claude-opus-5 and claude-opus-5-fast are reasoning-capable, but their field objects omit extendedThinking; users cannot opt in until a later sync repair. Insert it before llm.cloud.modelSource in both lists.

Proposed fix
 "properties": [
     "llm.cloud.apikey",
+    "extendedThinking",
     "llm.cloud.modelSource"
 ]
🤖 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 `@nodes/src/nodes/llm_anthropic/services.json` around lines 645 - 657, Update
the field lists for anthropic.claude-opus-5 and anthropic.claude-opus-5-fast to
include extendedThinking immediately before llm.cloud.modelSource, preserving
the existing properties in both profiles.
🤖 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 `@nodes/src/nodes/llm_anthropic/services.json`:
- Around line 367-373: Remove or gate the extendedThinking property for the
Claude Haiku 4.5 service profile at
nodes/src/nodes/llm_anthropic/services.json:367-373 and the Claude Haiku Latest
profile at nodes/src/nodes/llm_anthropic/services.json:565-571. Use an
extended-thinking capability check rather than the general reasoning capability,
ensuring Haiku profiles do not expose an inert toggle.

In `@packages/ai/src/ai/common/llm_adapter.py`:
- Around line 210-253: Update NativeOpenAIResponsesAdapter.stream by wrapping
the event-processing loop over stream in a try/finally block, and call
stream.close() in finally. Preserve all existing event handling, history
updates, and completion behavior while ensuring the OpenAI stream closes on
normal, partial, and exception paths.

---

Outside diff comments:
In `@nodes/src/nodes/llm_anthropic/services.json`:
- Around line 645-657: Update the field lists for anthropic.claude-opus-5 and
anthropic.claude-opus-5-fast to include extendedThinking immediately before
llm.cloud.modelSource, preserving the existing properties in both profiles.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 970a0f54-66f4-401a-ade9-437eda7462c5

📥 Commits

Reviewing files that changed from the base of the PR and between f673c95 and 8314681.

📒 Files selected for processing (16)
  • nodes/src/nodes/llm_anthropic/anthropic.py
  • nodes/src/nodes/llm_anthropic/services.json
  • nodes/test/agent_crewai/test_stop_words.py
  • packages/ai/src/ai/common/chat.py
  • packages/ai/src/ai/common/llm_adapter.py
  • packages/ai/src/ai/common/llm_native_stream.py
  • packages/ai/tests/ai/common/test_chat_content.py
  • packages/ai/tests/ai/common/test_chat_string_wiring.py
  • packages/ai/tests/ai/common/test_llm_adapter.py
  • packages/ai/tests/ai/common/test_llm_adapter_drive.py
  • packages/ai/tests/ai/common/test_llm_adapter_langchain.py
  • packages/ai/tests/ai/common/test_native_anthropic_adapter.py
  • packages/ai/tests/ai/common/test_native_openai_responses_adapter.py
  • tools/sync_models/src/core/patcher.py
  • tools/sync_models/src/providers/base.py
  • tools/sync_models/test/test_extended_thinking_field.py

Comment thread nodes/src/nodes/llm_anthropic/services.json
Comment thread packages/ai/src/ai/common/llm_adapter.py

@asclearuc asclearuc left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks @dsapandora — this is a solid piece of work. The Event/Adapter split is the right shape for this problem, the RFC-first approach in #1679 paid off, and moving thinking to a per-call payload makes the "never on the agent path" rule structural instead of a comment.

Requesting changes on four points:

  1. _chat now streams, which silently disables the existing "falling back to non-streaming response" recovery in chat_string — and removes the only non-streaming route the agent / expectJson path had. Inline on chat.py.
  2. CodeRabbit's still-current Major on the unclosed OpenAI Responses stream: #1683 (comment) — worth matching the try/finally + close() that NativeAnthropicAdapter already has in the same PR.
  3. Missing tests for the new behaviour: the no-text guard in _chat_string_responses, response.failederror, store=False, and the extendedThinking config wiring in the node. Inline.
  4. nodes/src/nodes/llm_anthropic/README.md is stale. The PR adds a public config field but the doc is not updated in the same change (co-located documentation rule). The Fields table has no extendedThinking row, and both the intro and the "Extended thinking" section still say the node "enables extended thinking automatically" from capabilities.reasoning. That is no longer true: it is opt-in, default off, and only on the interactive streaming path.

Rest of the detail is in the inline comments. CodeRabbit's Haiku toggle note (#1683 (comment)) is also worth a look — I found the same inert-toggle problem on anthropic.custom.

Non-blocking follow-up: now that LangChainAdapter strips <think> centrally, the _chat override in nodes/src/nodes/llm_minimax/minimax.py:87 is a redundant second implementation of the same strip (and it drops the stop-sequence kwarg) — a good candidate to delete, here or separately.

Comment thread packages/ai/src/ai/common/chat.py Outdated
Comment thread nodes/src/nodes/llm_anthropic/anthropic.py Outdated
Comment thread nodes/src/nodes/llm_anthropic/services.json
Comment thread nodes/src/nodes/llm_anthropic/services.json Outdated
Comment thread packages/ai/tests/ai/common/test_native_openai_responses_adapter.py
Comment thread packages/ai/src/ai/common/llm_adapter.py Outdated

@asclearuc asclearuc left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks @dsapandora — this is a clean, well-tested refactor, and every finding from my earlier review is addressed at 8c5dacb5 (the non-streaming fallback now drains through LangChainAdapter.collect()/invoke(), parse_bool gates the toggle, claude-opus-5/-fast got the toggle, the custom profile is patcher-managed, and the OpenAI Responses path is covered by tests).

Requesting changes for one still-valid item: CodeRabbit's Major finding holds at head — NativeOpenAIResponsesAdapter.stream() never close()s the OpenAI Stream on the exception / partial-consumption path, while the sibling NativeAnthropicAdapter already wraps its loop in try/finally + close(): #1683 (comment)

Non-blocking: the inert Extended Thinking toggle on Haiku (CodeRabbit Minor #1683 (comment)) traces back to the model patcher gating on capabilities.reasoning alone, so it re-adds the toggle to reasoning-capable-but-thinking-incapable Haiku on every sync — worth an extended-thinking-specific gate rather than the general reasoning flag. CodeRabbit's earlier Major on the missing "no text" guard (#1683 (comment)) is already fixed at head (chat.py:404), so it does not block.

dsapandora added a commit that referenced this pull request Jul 31, 2026
Address review on #1683:
- NativeOpenAIResponsesAdapter.stream() now wraps the loop in try/finally
  and close()s the OpenAI Stream on raise / early GeneratorExit, matching
  NativeAnthropicAdapter. Prevents leaking the HTTP stream on partial reads.
- build_anthropic_thinking_kwargs only rejects legacy Claude 3/3.5 Haiku;
  Haiku 4.5+ now gets extended thinking, so the extendedThinking toggle is
  no longer inert on those profiles.

Tests: stream close-on-exception / close-on-early-break; Haiku 4.5 + latest
get thinking, Claude 3 Haiku does not.
@dsapandora

Copy link
Copy Markdown
Collaborator Author

@asclearuc thanks for the thorough pass — both items are addressed in ada8e56e:

  • Stream close (blocking / CodeRabbit Major): NativeOpenAIResponsesAdapter.stream() now closes the OpenAI Stream on raise / early GeneratorExit via try/finally, matching the Anthropic adapter.
  • Haiku toggle (non-blocking): rather than gating the toggle off, I fixed the root cause. The runtime blanket-rejected all Haiku even though Haiku 4.5 supports extended thinking, so the toggle was inert. build_anthropic_thinking_kwargs now only rejects legacy Claude 3/3.5 Haiku — the toggle is functional on the 4.5 profiles that carry it, and capabilities.reasoning + the patcher stay as-is.

Both covered by new unit tests. Ready for another look.

asclearuc
asclearuc previously approved these changes Jul 31, 2026

@asclearuc asclearuc left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

One small comment

Comment thread packages/ai/src/ai/common/chat.py Outdated
dsapandora added a commit that referenced this pull request Jul 31, 2026
…back

Address review nit on #1683: the non-streaming fallback in
_chat_string_responses did invoke()+flatten_content() by hand, a second copy
of what LangChainAdapter.collect() already does. Route it through collect()
so the "invoke and normalize" step lives in one place. Behaviour identical.
Pull the provider-shape → (text, reasoning) walker out of chat_string into
_make_stream_content_parser, pinned by a characterization test. No behavior change.
Event (thinking/text/done) + Adapter Protocol with opaque history items —
the seam ChatBase will consume. Not wired yet. RFC: discussion #1679.
Relocate _make_stream_content_parser / _make_think_tag_splitter into llm_adapter
so adapters can reuse them without a chat↔adapter import cycle.
Wraps a LangChain chat model as an Event stream (reusing the shared parser);
covers the non-reasoning providers. done.items is the assistant text turn.
Streams the Messages API (thinking/text deltas → Events); done.items is the
assembled content with signatures intact — appended to history verbatim.
Streams the Responses API (reasoning/text deltas → Events); done.items are the
output items with encrypted reasoning, extended into history verbatim.
Consumes an adapter's Event stream into the existing on_chunk/on_reasoning_chunk
callbacks and returns (answer_text, opaque done.items). Not wired yet.
chat_string now streams via LangChainAdapter + drive_adapter instead of the inline
walker; stop sequences and finish_reason preserved. Native/Responses paths unchanged.
_chat returned Anthropic's typed-block list verbatim, crashing agents/expectJson
on .strip(); flatten_content keeps text blocks, drops thinking. Fixes the #1658 class.
NativeAnthropicAdapter yields Events over the same payload + create(stream=True) path;
try_anthropic_native_chat_stream now drives it. Behavior preserved.
NativeOpenAIResponsesAdapter yields Events over responses.create(stream=True);
_chat_string_responses drives it, keeping the non-streaming fallback. Behavior preserved.
_chat now drains the adapter (same iterator/normalization as streaming) instead of
invoke+flatten, so content handling is one mechanism. Fallback kept for stream-less backends.
…gate

Replace the interim hard-off (#1681) with an opt-in `extendedThinking` node toggle
(default off). Thinking is no longer baked into the client; the native adapter adds it
per call, so it rides the interactive streaming path only — agent/expectJson never gets it.
…ading

_stream_anthropic_messages_api folded into NativeAnthropicAdapter on this branch;
update the stop-threading test to drive the adapter. Same payload wiring, no behavior change.
The model sync now adds the extendedThinking UI toggle to reasoning-capable profiles
(incl. models added on future syncs) and removes it from non-reasoning ones — the UI
stays self-maintaining. Also drops the inert toggle from claude-3-haiku (non-reasoning).
…nses fallback

- Remove unused AnthropicAdapter/OpenAIAdapter (.stream() wrappers) + their tests;
  production routes through the Native* adapters.
- NativeOpenAIResponsesAdapter now passes store=False (no server-side retention).
- _chat_string_responses raises on empty output so it falls back (matching the Anthropic
  path) instead of returning '' silently.
…ine the field

The capabilities-aware repair only adds/manages extendedThinking on nodes whose fields
define it (today: llm_anthropic), so it never leaks into other providers (openai, gemini,
…) that neither define nor read the flag.
_chat drains through LangChainAdapter.collect() (invoke) instead of stream(), so the
streaming fallback in chat_string recovers with a genuinely different mechanism again —
while content normalization stays unified through the adapter. Addresses review #1.
…es tests

- llm_anthropic: parse_bool for the extendedThinking config (handles 'false'/'0'/'no').
- services.json: add the toggle to claude-opus-5/-fast, drop it from the inert custom
  profile, and order extendedThinking before modelSource (the sync invariant).
- patcher: manage 'custom' too (non-reasoning → toggle removed), so it self-heals.
- NativeAnthropic/NativeOpenAIResponses adapters drop the unused history constructor arg.
- Add Responses tests: failed→error, store=False, and the no-text → invoke fallback.
Address review on #1683:
- NativeOpenAIResponsesAdapter.stream() now wraps the loop in try/finally
  and close()s the OpenAI Stream on raise / early GeneratorExit, matching
  NativeAnthropicAdapter. Prevents leaking the HTTP stream on partial reads.
- build_anthropic_thinking_kwargs only rejects legacy Claude 3/3.5 Haiku;
  Haiku 4.5+ now gets extended thinking, so the extendedThinking toggle is
  no longer inert on those profiles.

Tests: stream close-on-exception / close-on-early-break; Haiku 4.5 + latest
get thinking, Claude 3 Haiku does not.
…back

Address review nit on #1683: the non-streaming fallback in
_chat_string_responses did invoke()+flatten_content() by hand, a second copy
of what LangChainAdapter.collect() already does. Route it through collect()
so the "invoke and normalize" step lives in one place. Behaviour identical.
The non-streaming fallback dropped the reasoning text after routing through
LangChainAdapter.collect(), which returned visible text only. collect() now
drains both parts and chat_string() forwards the reasoning to its own sink,
keeping the block vocabulary in the adapter rather than a second flattener.
@dsapandora
dsapandora force-pushed the feat/llm-provider-adapter branch from 43cf4a5 to 989a6e7 Compare July 31, 2026 20:19

@asclearuc asclearuc left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks @dsapandora — and thanks for taking the collect() suggestion further than I asked. 989a6e78 routing adapter.reasoning to on_reasoning_chunk is the right instinct: reasoning reaching the visible text is exactly the failure this PR exists to prevent.

Re-reviewed at 989a6e78. The collect() reuse is done and I have resolved that thread.

The rebase onto the newer develop is what changes the picture. While this PR was open, develop merged ai/common/utils/content_blocks.py — a shared flatten_content_blocks helper that centralised exactly the block vocabulary this PR reimplements. The rebase resolved that collision in favour of this branch's copy, which I do not think is what anyone intended. Two blockers come out of it, both inline:

  1. The block vocabulary is duplicated again. chat.py no longer imports flatten_content_blocks; llm_adapter.py carries a private copy. agent/_internal/utils.py still uses the shared one, so the repo ends up with two implementations of the same thing — the state develop had just finished removing.
  2. A develop test breaks. test_chat_return_contract.py::TestStringContent::test_plain_string_passes_through fails against this head. I ran it to confirm. CI's build/test jobs are still pending on this commit, so it has not surfaced yet.

Still open from the last round, unchanged:

  1. nodes/src/nodes/llm_anthropic/README.md is still not updated. Repeating the table since the thread was lost in a dismissed review:
Line Currently says Should say
17-19 the node "enables extended thinking automatically" opt-in per node, default off, interactive streaming path only
33-39 Fields table needs an extendedThinking row
64-66 "driven by the model's capabilities.reasoning flag" capabilities.reasoning and the new toggle
71 "Any Haiku model → None" wrong since the Haiku fix — only legacy Claude 3/3.5 Haiku
  1. The node-init test — still outstanding, inline again below.

Everything from my first review remains fixed; I re-ran the capability-vs-toggle audit after the rebase and it is still 0 mismatches across 22 profiles. Once the two rebase items and the two carried-over items land, this is ready.

Comment thread packages/ai/src/ai/common/llm_adapter.py
Comment thread packages/ai/src/ai/common/llm_adapter.py Outdated
Comment thread nodes/src/nodes/llm_anthropic/anthropic.py
The rebase onto develop left the adapter with a private copy of the block
vocabulary that content_blocks.py had just centralised, and the two had already
diverged: an unexpected content type was stringified there and dropped here.
The parser now delegates the list case and keeps only the stateful <think>
splitter, which is the split the shared helper was designed for.

collect() also handed invoke() a one-element message list on a single turn,
breaking the contract test that pins _chat passing the prompt string through.
It now sends the string on a first turn and the history only once there is one.
test(llm_anthropic): cover the toggle from node config to native handler
@github-actions github-actions Bot added the docs Documentation label Jul 31, 2026
@dsapandora

Copy link
Copy Markdown
Collaborator Author

Thanks for re-reviewing at 989a6e78 and for the table — all four items are in. Replies are inline on the three threads; this covers item 3, which had no line anchor.

3. nodes/src/nodes/llm_anthropic/README.md — updated in 9c318a08, following your table:

Line Now says
17-19 opt-in via extendedThinking, off by default, interactive streaming path only — and that the agent / expectJson path never requests it
33-39 added the extendedThinking row (boolean, false)
64-66 capabilities.reasoning and the toggle, both required
71 "Legacy Claude 3 / 3.5 Haiku", noting 4.5+ follows the row below

I left the generated ## Schema block alone, since nodes:docs-generate owns it.

Summary of the two rebase items: fe436378 puts the block vocabulary back in flatten_content_blocks and fixes the invoke() call shape; 9c318a08 covers the docs and the node-init test.

Verified with the local engine: packages/ai 659 passed / 3 skipped (PIL), nodes/test/llm_anthropic + nodes/test/agent_crewai 88 passed, ruff clean.

On the rebase itself — you read it right, and the duplicate was my resolution, not an intent. Worth flagging for whoever rebases next: content_blocks.py arrived on develop inside #1687, a tool_pipedrive CRM node PR, so a branch touching chat.py collides with it without any signal from the PR title.

@asclearuc asclearuc left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks @dsapandora — everything from the previous round is fixed, and I checked each one against the code rather than the commit message. This reads as ready.

The block vocabulary is shared again (fe436378). _make_stream_content_parser now delegates the list case to flatten_content_blocks and keeps only the <think> splitter, which is exactly the split the helper's docstring describes. The divergence I flagged is gone with it — flatten_content(42) now returns '42' like the shared helper, instead of silently dropping the content.

The develop contract test is fixed (fe436378). collect() hands the backend the prompt string on a single turn and the message list only once there is prior history. Worth recording that this one was real: the previous head 989a6e78 went red on all three platforms with 1 failed, 1863 passed, and the single failure was exactly this test:

packages/ai/tests/ai/common/test_chat_return_contract.py:59: in test_plain_string_passes_through
assert llm.prompts == ['what is 2+2?']
E   AssertionError: assert [[{'role': 'user', 'content': 'what is 2+2?'}, ...]] == ['what is 2+2?']

I re-ran that case against this head and it passes, along with the rest of the contract file. test_collect_sends_the_history_once_the_turn_is_not_the_first pins the multi-turn half, which is the right place to put it.

The README is accurate now (9c318a08). All four rows I listed are corrected, including the Haiku row that the earlier gate change had invalidated, and the "Both must be on" wording removes the ambiguity the old text had.

The node-init test landed (9c318a08). nodes/test/llm_anthropic/test_extended_thinking_init.py drives the real chain from config through to _native_stream_provider, and parametrising the falsy strings ('false', 'False', '0', '') is a better guard than the single case I asked for — that is the one that fails the moment parse_bool is swapped back for bool().

No new findings. The only thing outstanding is mechanical: Build / Ubuntu 22.04, macOS, and Windows are still running on 9c318a08. I verified the previously failing test locally against this head, but worth letting the three builds go green before merging, given the last head was red.

@dsapandora
dsapandora enabled auto-merge (squash) July 31, 2026 22:51
@dsapandora
dsapandora merged commit 83e1387 into develop Jul 31, 2026
24 checks passed
@dsapandora
dsapandora deleted the feat/llm-provider-adapter branch July 31, 2026 22:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Documentation module:ai AI/ML modules module:nodes Python pipeline nodes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Anthropic typed-block content not flattened on the non-streaming path — breaks every agent node

2 participants