From 0de0373ea40d94cf12e0bf62f7af5dbebc3fefd9 Mon Sep 17 00:00:00 2001 From: kamilbenkirane Date: Fri, 3 Jul 2026 17:05:47 +0200 Subject: [PATCH] feat(anthropic): stream thinking summaries, fix haiku thinking budget, dynamic-filtering web search - ThinkingLevelMapper sets display="summarized" so adaptive-thinking summaries stream; the API default "omitted" returns empty thinking blocks on Opus 4.8 / Sonnet 5 / 4.7. - claude-haiku-4-5 THINKING_BUDGET floor -1 -> 1024: haiku has no dynamic thinking mode, and the -1 -> "auto" translation raised ConstraintViolationError against the numeric Range. - Select web_search_20260209 (dynamic filtering) for models that support it, basic web_search_20250305 otherwise; the supported-model list lives in the anthropic catalog. --- .../text/providers/anthropic/models.py | 14 +++++++++++++- .../text/providers/anthropic/parameters.py | 17 ++++++++++++++++- .../providers/anthropic/messages/parameters.py | 8 +++----- 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/src/celeste/modalities/text/providers/anthropic/models.py b/src/celeste/modalities/text/providers/anthropic/models.py index a78a378..cc6c427 100644 --- a/src/celeste/modalities/text/providers/anthropic/models.py +++ b/src/celeste/modalities/text/providers/anthropic/models.py @@ -94,7 +94,7 @@ streaming=True, parameter_constraints={ Parameter.MAX_TOKENS: Range(min=1, max=64000), - TextParameter.THINKING_BUDGET: Range(min=-1, max=32000), + TextParameter.THINKING_BUDGET: Range(min=1024, max=32000), TextParameter.OUTPUT_SCHEMA: Schema(), TextParameter.TOOLS: ToolSupport(tools=[WebSearch]), TextParameter.TOOL_CHOICE: ToolChoiceSupport(), @@ -217,3 +217,15 @@ }, ), ] + +# Models that support web_search dynamic filtering (web_search_20260209); others use basic. +DYNAMIC_FILTERING_MODELS = frozenset( + { + "claude-fable-5", + "claude-opus-4-8", + "claude-opus-4-7", + "claude-opus-4-6", + "claude-sonnet-5", + "claude-sonnet-4-6", + } +) diff --git a/src/celeste/modalities/text/providers/anthropic/parameters.py b/src/celeste/modalities/text/providers/anthropic/parameters.py index e8d370d..3992f72 100644 --- a/src/celeste/modalities/text/providers/anthropic/parameters.py +++ b/src/celeste/modalities/text/providers/anthropic/parameters.py @@ -28,6 +28,7 @@ from celeste.types import TextContent from ...parameters import TextParameter +from .models import DYNAMIC_FILTERING_MODELS class TemperatureMapper(_TemperatureMapper): @@ -80,10 +81,24 @@ class OutputSchemaMapper(_OutputFormatMapper): class ToolsMapper(_ToolsMapper): - """Map tools to Anthropic's tools parameter.""" + """Map tools to Anthropic's tools parameter (web_search version per model).""" name = TextParameter.TOOLS + def map( + self, + request: dict[str, Any], + value: object, + model: Model, + ) -> dict[str, Any]: + """Upgrade web_search to dynamic filtering (web_search_20260209) where the model supports it.""" + request = super().map(request, value, model) + if model.id in DYNAMIC_FILTERING_MODELS: + for tool in request.get("tools", []): + if tool.get("name") == "web_search": + tool["type"] = "web_search_20260209" + return request + class ToolChoiceMapper(_ToolChoiceMapper): """Map tool_choice to Anthropic's tool_choice parameter.""" diff --git a/src/celeste/providers/anthropic/messages/parameters.py b/src/celeste/providers/anthropic/messages/parameters.py index 96f0d7f..d2cbc67 100644 --- a/src/celeste/providers/anthropic/messages/parameters.py +++ b/src/celeste/providers/anthropic/messages/parameters.py @@ -74,10 +74,8 @@ def map( class ThinkingLevelMapper(ParameterMapper[TextContent]): """Map thinking_level to adaptive thinking plus output_config.effort. - Emits {"thinking": {"type": "adaptive"}, "output_config": {"effort": }} - where is one of "low" | "medium" | "high" | "xhigh" | "max" — the - Messages API rejects an "effort" key inside the thinking object - ("thinking.adaptive.effort: Extra inputs are not permitted"). + display="summarized" so thinking summaries stream (API default "omitted" is empty). + effort goes in output_config, not the thinking object (the API rejects it there). """ def map( @@ -90,7 +88,7 @@ def map( validated_value = self._validate_value(value, model) if validated_value is None: return request - request["thinking"] = {"type": "adaptive"} + request["thinking"] = {"type": "adaptive", "display": "summarized"} request.setdefault("output_config", {})["effort"] = validated_value return request