Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
64 changes: 41 additions & 23 deletions unstract/sdk1/src/unstract/sdk1/adapters/base1.py
Original file line number Diff line number Diff line change
Expand Up @@ -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-<date>-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-<date>-v1:0`
# - Bedrock cross-region profile `us.anthropic.claude-sonnet-5-...`,
# `eu.`, `apac.`, `global.` variants
# - Bedrock foundation-model ARN `arn:aws:bedrock:<region>::foundation-model/
# anthropic.claude-opus-4-7-...`
# anthropic.claude-sonnet-5-...`
# - Bedrock inference-profile ARN `arn:aws:bedrock:<region>:<account>:
# inference-profile/us.anthropic.claude-opus-4-7-...`
# - Vertex AI `vertex_ai/claude-opus-4-7@<date>`
# - 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@<date>`
# - 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 `@<date>`, 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
Expand All @@ -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

Expand All @@ -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).
Expand All @@ -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
Expand Down
81 changes: 80 additions & 1 deletion unstract/sdk1/tests/test_sampling_strip.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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] = [
Expand All @@ -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",
Expand Down Expand Up @@ -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,
Expand Down
Loading