diff --git a/unstract/sdk1/src/unstract/sdk1/adapters/base1.py b/unstract/sdk1/src/unstract/sdk1/adapters/base1.py index ebd93e3dbd..bc8035e021 100644 --- a/unstract/sdk1/src/unstract/sdk1/adapters/base1.py +++ b/unstract/sdk1/src/unstract/sdk1/adapters/base1.py @@ -18,32 +18,47 @@ 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: -# - 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-...`, +# `top_p`, `top_k`). Anthropic removed the sampling params starting with Claude +# Opus 4.7, and every model released since — Opus 4.8, Sonnet 5, Fable 5, +# Mythos 5 — rejects them with a 400 (directly, and through the Bedrock / Azure +# AI Foundry / Vertex proxies). Opus 4.6 / Sonnet 4.6 and older still accept +# them, so detection is a narrow allowlist rather than a broad `claude-*` match. +# +# Each stem is compiled into a pattern that is regex-searched against the model +# id after lowercasing and normalizing `.` / `_` to `-`. The match is anchored +# at the trailing edge so unrelated future ids (`claude-sonnet-50`, +# `claude-opus-4-70`, `claude-sonnet-5verbose`) do not match. One stem covers +# every encoding of the id we have observed, e.g. for `claude-sonnet-5`: +# - Native Anthropic `claude-sonnet-5`, `anthropic/claude-sonnet-5` +# - Bedrock foundation model `anthropic.claude-sonnet-5--v1:0` +# - Bedrock cross-region profile `us.anthropic.claude-sonnet-5-...`, # `eu.`, `apac.`, `global.` variants # - Bedrock foundation-model ARN `arn:aws:bedrock:::foundation-model/ -# anthropic.claude-opus-4-7-...` +# anthropic.claude-sonnet-5-...` # - Bedrock inference-profile ARN `arn:aws:bedrock::: -# inference-profile/us.anthropic.claude-opus-4-7-...` -# - Vertex AI `vertex_ai/claude-opus-4-7@` -# - Azure AI Foundry deployments whose name embeds `claude-opus-4-7` +# inference-profile/us.anthropic.claude-sonnet-5-...` +# - Vertex AI `vertex_ai/claude-sonnet-5@` +# - Azure AI Foundry deployments whose name embeds `claude-sonnet-5` # Leading text (route prefixes like `converse/`, `invoke/`, `bedrock/`) passes # through because the regex is anchored only at the trailing edge. -# Add new entries here when Anthropic deprecates sampling on more models. +# Keep this list current — add a stem here when Anthropic deprecates sampling on +# a new model. # Trailing anchor allows: end-of-string, or one of `-`/`:`/`@`/`/` (the # delimiters used in date suffixes, ARN paths, Vertex `@`, and the # `v1:0` tag), or `v` followed by a digit (the version-tag start). A bare -# `v` is intentionally rejected so alpha continuations like `4-7verbose` do -# not silently match. -# 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)"), +# `v` is intentionally rejected so alpha continuations like `sonnet-5verbose` +# do not silently match. +# See https://docs.claude.com/en/about-claude/models/overview +_SAMPLING_DEPRECATED_MODEL_STEMS: tuple[str, ...] = ( + "claude-opus-4-7", + "claude-opus-4-8", + "claude-sonnet-5", + "claude-fable-5", + "claude-mythos-5", +) +_SAMPLING_DEPRECATED_MODEL_PATTERNS: tuple[re.Pattern[str], ...] = tuple( + re.compile(rf"{re.escape(stem)}(?=$|[-:@/]|v\d)") + for stem in _SAMPLING_DEPRECATED_MODEL_STEMS ) _DEPRECATED_SAMPLING_PARAMS: tuple[str, ...] = ("temperature", "top_p", "top_k") # Fields whose value can carry a model id. `model` is universal; `model_id` is @@ -63,7 +78,7 @@ def _looks_like_opaque_aip_arn(value: str | None) -> bool: Bedrock AIP ARNs do not carry the underlying foundation-model id in the string, so the sampling-strip detector cannot decide whether the call is - bound for Claude Opus 4.7. + bound for a sampling-deprecated Claude model (Opus 4.7 and later). """ return bool(value) and _OPAQUE_AIP_ARN_MARKER in value @@ -72,12 +87,14 @@ def _has_deprecated_sampling_params(model: str | None) -> bool: """Return True when the model rejects sampling parameters. Anthropic deprecated `temperature`, `top_p`, and `top_k` starting with - Claude Opus 4.7; sending any of them yields a 400 from Anthropic and from - the providers that proxy it (Bedrock, Azure AI Foundry, Vertex AI). + Claude Opus 4.7, and every model released since (Opus 4.8, Sonnet 5, + Fable 5, Mythos 5) rejects them too; sending any of them yields a 400 from + Anthropic and from the providers that proxy it (Bedrock, Azure AI Foundry, + Vertex AI). See `_SAMPLING_DEPRECATED_MODEL_STEMS` for the covered set. The check normalizes case and `.`/`_` separators to `-`, then regex- searches against the patterns with a trailing-edge boundary, so - `claude-opus-4-70` and `claude-opus-4-7verbose` do not match. This + `claude-sonnet-50` and `claude-sonnet-5verbose` do not match. This catches every format that embeds the model id (foundation model ids, cross-region profiles, foundation-model ARNs, inference-profile ARNs, Vertex `@`-suffixed ids). @@ -89,7 +106,8 @@ def _has_deprecated_sampling_params(model: str | None) -> bool: from the string. Pass the AIP ARN in `model_id` and keep the standard model id in `model`, or the strip won't fire. - Azure AI Foundry deployment names that omit the model id; rename the - deployment to include `claude-opus-4-7` so detection works. + deployment to include the model id (e.g. `claude-sonnet-5`) so detection + works. """ if not model: return False diff --git a/unstract/sdk1/tests/test_sampling_strip.py b/unstract/sdk1/tests/test_sampling_strip.py index 3e3ce8b405..d6d792a3d7 100644 --- a/unstract/sdk1/tests/test_sampling_strip.py +++ b/unstract/sdk1/tests/test_sampling_strip.py @@ -1,4 +1,9 @@ -"""Tests for Claude Opus 4.7 sampling-parameter strip. +"""Tests for the Claude sampling-parameter strip. + +Covers Claude Opus 4.7 and every model released since (Opus 4.8, Sonnet 5, +Fable 5, Mythos 5), all of which reject `temperature`/`top_p`/`top_k`. Sonnet 5 +is the model behind the reported Azure AI Foundry `temperature is deprecated` +failure. Pins the detection regex and the four-adapter wiring against the failure modes that surfaced in PR #1934 review: @@ -68,6 +73,39 @@ def test_has_deprecated_sampling_params_positive(model: str) -> None: assert _has_deprecated_sampling_params(model) +# Every Claude model released after Opus 4.7 also rejects sampling params +# (Opus 4.8, Sonnet 5, Fable 5, Mythos 5). Sonnet 5 is the model behind the +# reported Azure AI Foundry `temperature is deprecated` failure. +POST_47_POSITIVES: list[str] = [ + # Sonnet 5 — native, Azure AI Foundry (prefixed by validate_model), case, + # deployment names embedding the id, separator normalization, version tag + "claude-sonnet-5", + "anthropic/claude-sonnet-5", + "azure_ai/claude-sonnet-5", + "azure_ai/claude-sonnet-5-prod", + "azure_ai/my-claude-sonnet-5-deployment", + "Claude-Sonnet-5", # case + "claude.sonnet.5", # dot separators + "claude_sonnet_5", # underscore separators + "claude-sonnet-5v1", # version tag + "anthropic.claude-sonnet-5-20260101-v1:0", # Bedrock foundation model id + "vertex_ai/claude-sonnet-5@20260101", # Vertex AI + # Opus 4.8 + "claude-opus-4-8", + "anthropic.claude-opus-4-8-20260101-v1:0", + "azure_ai/claude-opus-4-8", + # Fable 5 / Mythos 5 + "claude-fable-5", + "vertex_ai/claude-fable-5@20260101", + "claude-mythos-5", +] + + +@pytest.mark.parametrize("model", POST_47_POSITIVES) +def test_has_deprecated_sampling_params_positive_post_opus_47(model: str) -> None: + assert _has_deprecated_sampling_params(model) + + # ── detection: negatives ──────────────────────────────────────────────────── NEGATIVES: list[str | None] = [ @@ -77,6 +115,18 @@ def test_has_deprecated_sampling_params_positive(model: str) -> None: "claude-sonnet-4-7", "claude-haiku-4-5", "anthropic.claude-3-5-sonnet-20241022-v2:0", + # Sonnet families that still accept sampling params must NOT match the + # `claude-sonnet-5` stem + "claude-sonnet-4-5", + "claude-sonnet-4-6", + # Prefix collisions / alpha continuations for the new stems — lock the + # trailing-edge anchor the same way it is locked for opus-4-7 + "claude-sonnet-50", + "claude-sonnet-59", + "claude-sonnet-5verbose", + "claude-sonnet-5variant", + "claude-opus-4-80", + "claude-fable-50", # Non-Anthropic providers "gpt-4o", "gemini-2.0-flash", @@ -277,6 +327,35 @@ 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: + """Sonnet 5 rejects temperature just like Opus 4.7. + + The `azure_ai_foundry` case is the exact scenario reported: configuring + Claude Sonnet 5 on Azure AI Foundry 400'd with `temperature is deprecated`. + """ + model = { + "anthropic": "claude-sonnet-5", + "bedrock": "anthropic.claude-sonnet-5-20260101-v1:0", + "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@20260101")) + 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,