diff --git a/unstract/sdk1/src/unstract/sdk1/adapters/base1.py b/unstract/sdk1/src/unstract/sdk1/adapters/base1.py index ebd93e3dbd..9bcc7591f7 100644 --- a/unstract/sdk1/src/unstract/sdk1/adapters/base1.py +++ b/unstract/sdk1/src/unstract/sdk1/adapters/base1.py @@ -18,11 +18,15 @@ logger = logging.getLogger(__name__) # Anthropic models that have deprecated sampling parameters (`temperature`, -# `top_p`, `top_k`). The patterns are regex-searched against the model id -# after lowercasing and normalizing `.` / `_` to `-`. The match is anchored at -# the trailing edge so that unrelated future ids (`claude-opus-4-70`, -# `claude-opus-4-75`, `claude-opus-4-7verbose`) do not match. A single entry -# covers every encoding of the id we have observed: +# `top_p`, `top_k`) — sending any of them yields a 400. Anthropic began this +# with Claude Opus 4.7 and continued it across the Claude 5 family (Opus 4.8, +# Sonnet 5, Fable 5, Mythos 5); adjacent older families (Opus 4.6, Sonnet 4.6, +# and earlier) still ACCEPT these params and must NOT be listed here. +# The patterns are regex-searched against the model id after lowercasing and +# normalizing `.` / `_` to `-`. Each match is anchored at the trailing edge so +# that unrelated ids (`claude-opus-4-70`, `claude-sonnet-50`, +# `claude-sonnet-5verbose`) do not match. One entry per model covers every +# encoding of that id we have observed (shown here for `claude-opus-4-7`): # - Native Anthropic `claude-opus-4-7`, `anthropic/claude-opus-4-7` # - Bedrock foundation model `anthropic.claude-opus-4-7--v1:0` # - Bedrock cross-region profile `us.anthropic.claude-opus-4-7-...`, @@ -44,6 +48,10 @@ # See https://docs.claude.com/en/about-claude/models/whats-new-claude-4-7 _SAMPLING_DEPRECATED_MODEL_PATTERNS: tuple[re.Pattern[str], ...] = ( re.compile(r"claude-opus-4-7(?=$|[-:@/]|v\d)"), + re.compile(r"claude-opus-4-8(?=$|[-:@/]|v\d)"), + re.compile(r"claude-sonnet-5(?=$|[-:@/]|v\d)"), + re.compile(r"claude-fable-5(?=$|[-:@/]|v\d)"), + re.compile(r"claude-mythos-5(?=$|[-:@/]|v\d)"), ) _DEPRECATED_SAMPLING_PARAMS: tuple[str, ...] = ("temperature", "top_p", "top_k") # Fields whose value can carry a model id. `model` is universal; `model_id` is diff --git a/unstract/sdk1/tests/test_sampling_strip.py b/unstract/sdk1/tests/test_sampling_strip.py index 3e3ce8b405..3839d23312 100644 --- a/unstract/sdk1/tests/test_sampling_strip.py +++ b/unstract/sdk1/tests/test_sampling_strip.py @@ -1,8 +1,11 @@ -"""Tests for Claude Opus 4.7 sampling-parameter strip. +"""Tests for the Anthropic sampling-parameter strip. + +Covers Claude Opus 4.7 and the Claude 5 family (Opus 4.8, Sonnet 5, Fable 5, +Mythos 5) — every model that rejects `temperature`/`top_p`/`top_k` with a 400. Pins the detection regex and the four-adapter wiring against the failure modes that surfaced in PR #1934 review: -- prefix collisions (`claude-opus-4-70`, `-75`, `4-7verbose`) +- prefix collisions (`claude-opus-4-70`, `-75`, `4-7verbose`, `claude-sonnet-50`) - Bedrock Application Inference Profile ARN fallback via `model_id` - mutate-and-return regression (input dict must be preserved) - silent skip with sampling params present must emit a debug breadcrumb @@ -68,6 +71,44 @@ def test_has_deprecated_sampling_params_positive(model: str) -> None: assert _has_deprecated_sampling_params(model) +# Claude 5 family — the deprecation continued from Opus 4.7 to Opus 4.8, +# Sonnet 5, Fable 5, and Mythos 5. Sonnet 5 on Azure AI Foundry is the +# reported case that motivated adding these entries. +CLAUDE_5_POSITIVES: list[str] = [ + # Opus 4.8 + "claude-opus-4-8", + "anthropic/claude-opus-4-8", + "anthropic.claude-opus-4-8-20260101-v1:0", + "us.anthropic.claude-opus-4-8-20260101-v1:0", + "vertex_ai/claude-opus-4-8@20260101", + "azure_ai/claude-opus-4-8", + "claude-opus-4-8v1", + # Sonnet 5 (reported: Azure AI Foundry) + "claude-sonnet-5", + "anthropic/claude-sonnet-5", + "anthropic.claude-sonnet-5", + "us.anthropic.claude-sonnet-5-20260101-v1:0", + "vertex_ai/claude-sonnet-5", + "azure_ai/claude-sonnet-5", + "azure_ai/my-claude-sonnet-5-deployment", + "claude.sonnet.5", + "claude_sonnet_5", + "claude-sonnet-5v1", + # Fable 5 + "claude-fable-5", + "anthropic/claude-fable-5", + "azure_ai/claude-fable-5", + # Mythos 5 + "claude-mythos-5", + "vertex_ai/claude-mythos-5", +] + + +@pytest.mark.parametrize("model", CLAUDE_5_POSITIVES) +def test_has_deprecated_sampling_params_claude5_positive(model: str) -> None: + assert _has_deprecated_sampling_params(model) + + # ── detection: negatives ──────────────────────────────────────────────────── NEGATIVES: list[str | None] = [ @@ -92,6 +133,17 @@ def test_has_deprecated_sampling_params_positive(model: str) -> None: "claude-opus-4-7verbose", "claude-opus-4-7vnext", "claude-opus-4-7variant", + "claude-sonnet-5verbose", + # Claude 5 prefix collisions — trailing digit is not a boundary. + "claude-opus-4-80", + "claude-sonnet-50", + "claude-fable-50", + "claude-mythos-50", + # Adjacent Claude families that still accept sampling params. + "claude-sonnet-4-6", + "claude-sonnet-4-5", + "claude-haiku-4-6", + "claude-fable-4", # Opaque Bedrock Application Inference Profile ARN — model id is not # recoverable from the string. Strip-detection is expected to skip; # callers must keep the standard id in `model` or `model_id`. @@ -277,6 +329,31 @@ def test_vertex_validate_strips_temperature_for_opus_4_7() -> None: assert param not in result, f"vertex: {param} should be stripped" +@pytest.mark.parametrize( + "name,cls,extra", + ADAPTER_CASES, + ids=lambda v: v if isinstance(v, str) else "", +) +def test_validate_strips_temperature_for_sonnet_5( + name: str, cls: type, extra: dict[str, Any] +) -> None: + """Reported case: Claude Sonnet 5 on Azure AI Foundry (and its peers).""" + model = { + "anthropic": "claude-sonnet-5", + "bedrock": "anthropic.claude-sonnet-5", + "azure_ai_foundry": "claude-sonnet-5", + }[name] + result = cls.validate({"model": model, "temperature": 0.5, **extra}) + for param in _DEPRECATED_SAMPLING_PARAMS: + assert param not in result, f"{name}: {param} should be stripped" + + +def test_vertex_validate_strips_temperature_for_sonnet_5() -> None: + result = VertexAILLMParameters.validate(_vertex_metadata("claude-sonnet-5")) + for param in _DEPRECATED_SAMPLING_PARAMS: + assert param not in result, f"vertex: {param} should be stripped" + + @pytest.mark.parametrize( "name,cls,extra", ADAPTER_CASES,