Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d6d75ad
refactor(llm): extract stream content parsing into a named seam
dsapandora Jul 26, 2026
ec1a7df
feat(llm): add normalized provider Adapter interface
dsapandora Jul 26, 2026
a2e18f0
refactor(llm): move stream content parser to the adapter module
dsapandora Jul 26, 2026
b820391
feat(llm): add LangChainAdapter
dsapandora Jul 26, 2026
6ae4301
feat(llm): add AnthropicAdapter
dsapandora Jul 26, 2026
1d8ebcc
feat(llm): add OpenAIAdapter
dsapandora Jul 26, 2026
4f19762
feat(llm): add drive_adapter Event→callback shim
dsapandora Jul 26, 2026
4faf8b3
feat(llm): drive the LangChain streaming path through the adapter
dsapandora Jul 26, 2026
9396a33
fix(llm): flatten non-streaming _chat content to a string
dsapandora Jul 27, 2026
1cb1465
feat(llm): route the native Anthropic path through an adapter
dsapandora Jul 27, 2026
7203958
feat(llm): route the OpenAI Responses path through an adapter
dsapandora Jul 27, 2026
fba74bb
refactor(llm): unify the non-streaming path through the adapter
dsapandora Jul 28, 2026
d0cd813
feat(llm_anthropic): per-node extended-thinking control with context …
dsapandora Jul 28, 2026
5be6e36
test(agent_crewai): drive NativeAnthropicAdapter for native stop-thre…
dsapandora Jul 28, 2026
afce1ff
fix(sync-models): make the extendedThinking toggle capabilities-aware
dsapandora Jul 28, 2026
1d35fac
refactor(llm): address CodeRabbit — dead adapters, store=False, Respo…
dsapandora Jul 28, 2026
b556080
fix(sync-models): scope the extendedThinking toggle to nodes that def…
dsapandora Jul 29, 2026
8314681
docs(llm_anthropic): soften the extendedThinking field description
dsapandora Jul 29, 2026
6196feb
fix(llm): keep the non-streaming path on invoke via adapter.collect()
dsapandora Jul 30, 2026
8c5dacb
fix(llm): address review — parse_bool, opus-5 toggle, custom, Respons…
dsapandora Jul 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 15 additions & 22 deletions nodes/src/nodes/llm_anthropic/anthropic.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,10 @@
from ai.common.chat import ChatBase
from ai.common.config import Config
from ai.common.llm_native_stream import build_anthropic_thinking_kwargs, gate_model_name
from ai.common.utils import parse_bool
from langchain_anthropic import ChatAnthropic


# Interim: extended thinking is disabled at the node until the normalized provider adapter
# lands (RFC #1679). With thinking on, Anthropic returns typed content blocks (thinking + text)
# that the agent / expectJson path cannot consume (incident #1658; flatten defense-in-depth in
# #1659). We gate activation here rather than flipping capabilities.reasoning, because that flag
# is stamped by tools/sync_models and re-stamps to true on the next sync. Flip back to re-enable.
_EXTENDED_THINKING_ENABLED = False


def _estimate_token_ids(text: str) -> list:
"""Estimate token ids at ~4 chars/token."""
return [0] * max(1, (len(text) + 3) // 4)
Expand Down Expand Up @@ -78,23 +71,23 @@ def __init__(self, provider: str, connConfig: Dict[str, Any], bag: Dict[str, Any
# Init the chat base
super().__init__(provider, connConfig, bag)

# Get the LLM
kwargs: Dict[str, Any] = {
'model': model,
'api_key': apikey,
'max_tokens': self._modelOutputTokens,
'custom_get_token_ids': _estimate_token_ids,
}
if self._is_reasoning and _EXTENDED_THINKING_ENABLED:
kwargs.update(build_anthropic_thinking_kwargs(model_gate, self._modelOutputTokens))

self._extended_thinking = bool(kwargs.get('thinking'))
# Only route through the native handler when thinking is actually on;
# non-reasoning models stay on the default LangChain path.
# Extended thinking is opt-in per node (config extendedThinking, default off) and only
# for reasoning models. It is NOT baked into the client: the native streaming adapter adds
# it per call, so thinking activates on the interactive streaming path only — never on the
# agent / expectJson path (which has no streaming and stays on the plain LangChain client).
self._thinking_mode_kwargs: Dict[str, Any] = {}
if self._is_reasoning and parse_bool(config.get('extendedThinking')):
self._thinking_mode_kwargs = build_anthropic_thinking_kwargs(model_gate, self._modelOutputTokens)
self._extended_thinking = bool(self._thinking_mode_kwargs)
if self._extended_thinking:
self._native_stream_provider = 'anthropic'

self._llm = ChatAnthropic(**kwargs)
self._llm = ChatAnthropic(
model=model,
api_key=apikey,
max_tokens=self._modelOutputTokens,
custom_get_token_ids=_estimate_token_ids,
)

# Save our chat class into the bag
bag['chat'] = self
26 changes: 26 additions & 0 deletions nodes/src/nodes/llm_anthropic/services.json
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,12 @@
"title": "Tokens",
"description": "Total Tokens"
},
"extendedThinking": {
"type": "boolean",
"title": "Extended thinking",
"default": false,
"description": "Enable Anthropic extended thinking (reasoning) for this node. Off by default. Applies to reasoning-capable models on the interactive chat path."
},
"anthropic.custom": {
"object": "custom",
"properties": [
Expand All @@ -345,34 +351,39 @@
"object": "claude-sonnet-4-6",
"properties": [
"llm.cloud.apikey",
"extendedThinking",
"llm.cloud.modelSource"
]
},
"anthropic.claude-opus-4-6": {
"object": "claude-opus-4-6",
"properties": [
"llm.cloud.apikey",
"extendedThinking",
"llm.cloud.modelSource"
]
},
"anthropic.claude-haiku-4-5": {
"object": "claude-haiku-4-5",
"properties": [
"llm.cloud.apikey",
"extendedThinking",
"llm.cloud.modelSource"
]
Comment on lines 366 to 372

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not expose an inert extended-thinking toggle for Haiku.

The supplied Anthropic node documentation states Haiku models receive no thinking parameters. The runtime only selects native thinking when kwargs are produced, so these controls cannot take effect. Add an extended-thinking-capability gate (rather than using general reasoning capability alone), or exclude Haiku profiles from this field wiring.

  • nodes/src/nodes/llm_anthropic/services.json#L367-L373: remove or gate extendedThinking for Claude Haiku 4.5.
  • nodes/src/nodes/llm_anthropic/services.json#L565-L571: remove or gate extendedThinking for Claude Haiku Latest.
📍 Affects 1 file
  • nodes/src/nodes/llm_anthropic/services.json#L367-L373 (this comment)
  • nodes/src/nodes/llm_anthropic/services.json#L565-L571
🤖 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 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.

},
"anthropic.claude-sonnet-4-5": {
"object": "claude-sonnet-4-5",
"properties": [
"llm.cloud.apikey",
"extendedThinking",
"llm.cloud.modelSource"
]
},
"anthropic.claude-opus-4-5": {
"object": "claude-opus-4-5",
"properties": [
"llm.cloud.apikey",
"extendedThinking",
"llm.cloud.modelSource"
]
},
Expand Down Expand Up @@ -523,6 +534,7 @@
"object": "claude-opus-4-7",
"properties": [
"llm.cloud.apikey",
"extendedThinking",
"llm.cloud.modelSource"
]
},
Expand All @@ -537,97 +549,111 @@
"object": "claude-fable-5",
"properties": [
"llm.cloud.apikey",
"extendedThinking",
"llm.cloud.modelSource"
]
},
"anthropic.claude-fable-latest": {
"object": "claude-fable-latest",
"properties": [
"llm.cloud.apikey",
"extendedThinking",
"llm.cloud.modelSource"
]
},
"anthropic.claude-haiku-latest": {
"object": "claude-haiku-latest",
"properties": [
"llm.cloud.apikey",
"extendedThinking",
"llm.cloud.modelSource"
]
},
"anthropic.claude-opus-4": {
"object": "claude-opus-4",
"properties": [
"llm.cloud.apikey",
"extendedThinking",
"llm.cloud.modelSource"
]
},
"anthropic.claude-opus-4-1": {
"object": "claude-opus-4-1",
"properties": [
"llm.cloud.apikey",
"extendedThinking",
"llm.cloud.modelSource"
]
},
"anthropic.claude-opus-4-7-fast": {
"object": "claude-opus-4-7-fast",
"properties": [
"llm.cloud.apikey",
"extendedThinking",
"llm.cloud.modelSource"
]
},
"anthropic.claude-opus-4-8": {
"object": "claude-opus-4-8",
"properties": [
"llm.cloud.apikey",
"extendedThinking",
"llm.cloud.modelSource"
]
},
"anthropic.claude-opus-4-8-fast": {
"object": "claude-opus-4-8-fast",
"properties": [
"llm.cloud.apikey",
"extendedThinking",
"llm.cloud.modelSource"
]
},
"anthropic.claude-opus-latest": {
"object": "claude-opus-latest",
"properties": [
"llm.cloud.apikey",
"extendedThinking",
"llm.cloud.modelSource"
]
},
"anthropic.claude-sonnet-4": {
"object": "claude-sonnet-4",
"properties": [
"llm.cloud.apikey",
"extendedThinking",
"llm.cloud.modelSource"
]
},
"anthropic.claude-sonnet-5": {
"object": "claude-sonnet-5",
"properties": [
"llm.cloud.apikey",
"extendedThinking",
"llm.cloud.modelSource"
]
},
"anthropic.claude-sonnet-latest": {
"object": "claude-sonnet-latest",
"properties": [
"llm.cloud.apikey",
"extendedThinking",
"llm.cloud.modelSource"
]
},
"anthropic.claude-opus-5": {

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.

should fix — claude-opus-5 and claude-opus-5-fast are the only reasoning models that did not get the toggle.

I checked every field object in this file against its profile's capabilities.reasoning:

object capabilities.reasoning has extendedThinking
18 other Claude profiles true yes
claude-3-haiku false no (correct)
claude-opus-5 true no
claude-opus-5-fast true no

These two are the newest and most capable profiles in the node, and they are the only reasoning models where the new toggle is missing. A user selecting Claude Opus 5 cannot turn extended thinking on at all.

This also breaks the invariant the PR itself adds in tools/sync_models/src/core/patcher.py: _repair_field_objects now asserts "toggle present iff capabilities.reasoning". The next model sync will silently add these two, producing an unrelated diff inside a chore(models): commit. Better to add them here.

Suggested change
"anthropic.claude-opus-5": {
"anthropic.claude-opus-5": {
"object": "claude-opus-5",
"properties": [
"llm.cloud.apikey",
"llm.cloud.modelSource",
"extendedThinking"
]
},
"anthropic.claude-opus-5-fast": {
"object": "claude-opus-5-fast",
"properties": [
"llm.cloud.apikey",
"llm.cloud.modelSource",
"extendedThinking"
]
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 8c5dacb5 — added the toggle to claude-opus-5 and claude-opus-5-fast, and reordered extendedThinking before modelSource across all profiles to respect the invariant.

"object": "claude-opus-5",
"properties": [
"llm.cloud.apikey",
"extendedThinking",
"llm.cloud.modelSource"
]
},
"anthropic.claude-opus-5-fast": {
"object": "claude-opus-5-fast",
"properties": [
"llm.cloud.apikey",
"extendedThinking",
"llm.cloud.modelSource"
]
}
Expand Down
9 changes: 3 additions & 6 deletions nodes/test/agent_crewai/test_stop_words.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ def _load_native_stream():


def _run_native_capture(ns, stop_value):
"""Drive _stream_anthropic_messages_api with fakes; return the stop the payload got."""
"""Drive the native Anthropic adapter with fakes; return the stop the payload got."""
captured: dict = {}

class _FakeLLM:
Expand All @@ -255,11 +255,8 @@ class _FakeChat:
ns._open_raw_message_stream = lambda client, payload: iter(()) # no events -> no text
token = ns.STOP_SEQUENCES_VAR.set(stop_value)
try:
# Produces no text, so the function raises after building the payload — we only
# assert the payload wiring, which happens before any streaming.
ns._stream_anthropic_messages_api(_FakeChat(), 'prompt', lambda t: None, None, None)
except RuntimeError:
pass
# Drain the adapter; the payload (with the stop) is built before any streaming.
list(ns.NativeAnthropicAdapter(_FakeChat()).stream('prompt'))
finally:
ns.STOP_SEQUENCES_VAR.reset(token)
return captured.get('stop', 'UNSET')
Expand Down
Loading
Loading