fix(vertex,azure): model-aware mid-conversation system for Claude /v1/messages - #33807
Conversation
Greptile SummaryThis PR closes the Vertex AI and Azure AI Foundry gap from the Kraken Tech RCA: both providers serve Claude on the native Anthropic
Confidence Score: 5/5Safe to merge — the change fixes a live 400 error on older Claude models on Azure/Vertex and prevents prompt-cache collapse on 4.8+/5, with no behavioral change to the first-party Anthropic path or Bedrock. The refactoring correctly relies on the polymorphic No files require special attention.
|
| Filename | Overview |
|---|---|
| litellm/llms/anthropic/experimental_pass_through/messages/transformation.py | Extracted _normalize_system_role_messages, _as_system_content_blocks, and _is_system_role_message from Bedrock into the shared AnthropicMessagesConfig base; base class never calls the method itself (first-party path unchanged), subclasses opt in. |
| litellm/llms/azure_ai/anthropic/messages_transformation.py | Added _normalize_system_role_messages call in transform_anthropic_messages_request before _remove_scope_from_cache_control; custom_llm_provider returns "azure_ai" so the cost-map lookup resolves correctly. |
| litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/experimental_pass_through/transformation.py | Added _normalize_system_role_messages call in transform_anthropic_messages_request; custom_llm_provider returns "vertex_ai" so the cost-map lookup resolves correctly. |
| litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py | Removed the now-redundant Bedrock-local _normalize_system_role_messages_for_bedrock and replaced the call site with the shared _normalize_system_role_messages; Bedrock behavior unchanged because its custom_llm_provider still returns "bedrock". |
| model_prices_and_context_window.json | Added supports_mid_conversation_system: true to azure_ai/claude-opus-4-8, azure_ai/claude-sonnet-5, azure_ai/claude-fable-5, and the matching vertex_ai/* and vertex_ai/*@default entries; no new models added without the flag. |
| tests/test_litellm/llms/azure_ai/claude/test_azure_anthropic_messages_transformation.py | Added unit tests covering leading-run hoist, mid-conversation preservation on flagged models, hoist-all on unflagged models for Azure; the cost-map flag-coverage test correctly uses next(..., None) with an explicit assertion, addressing the previous StopIteration comment. |
| tests/test_litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/test_vertex_ai_partner_models_anthropic_messages_config.py | Parallel unit tests for Vertex: same three hoist scenarios plus the cost-map flag-coverage guard with next(..., None) + assertion; uses str(info.get("litellm_provider", "")).startswith("vertex_ai") to cover vertex_ai, vertex_ai_beta, etc. |
| tests/e2e/llm_translation/test_messages_mid_conversation_system_native_providers_e2e.py | New e2e tests for Azure Foundry and Vertex: flagged-model cache-preservation test (asserts cache_read >= primed prefix) and unflagged-model hoist-and-succeed test; cache priming loop avoids warming a mutated prefix by generating a fresh user-turn text on each retry. |
Reviews (3): Last reviewed commit: "test: give cost-map guard next() a defau..." | Re-trigger Greptile
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Merging this PR will improve performance by 54.77%
|
| Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|
| ⚡ | test_completion_streaming |
58.8 ms | 38 ms | +54.77% |
Tip
Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.
Comparing litellm_vertex_azure_midsys (8b1a19f) with litellm_internal_staging (214945a)1
Footnotes
…/merge-skew flake The model_info / get_model_info_with_id endpoint tests drove refactored endpoints with bare, unspec'd MagicMock routers and models. Because the mocks were unspec'd, any attribute or method the (refactored) endpoints newly read auto-materialized a child MagicMock, and whether that child was reached depended on process-global state (premium_user, and the real get_available_models_for_user chain reading litellm globals) that sibling tests in the same xdist worker mutate. When reached, the MagicMock either unpacked to empty (a, b = mock.method() -> 'not enough values to unpack (expected 2, got 0)') or leaked into RouterModelInfo(**model_info) and failed Pydantic str validation. Pass in isolation, fail under xdist. The original TestModelInfoEndpoint failure (BerriAI#33807 CI) was the same class surfaced by merge skew: BerriAI#33721 added a get_configured_token_limits unpack to create_model_info_response, and CI's merge commit ran that against the un-updated bare-mock test before the BerriAI#33742 band-aid landed. Fix (test-only, no product change): - TestModelInfoEndpoint: mock the real seam (get_available_models_for_user), configure the router methods the endpoint actually calls, return a real Deployment, and drop the dead proxy_server.get_key_models/get_team_models/ get_complete_model_list patches the refactor had stranded. - TestGetModelInfoWithIdBlocked: spec the model mock so unset enterprise columns read as None instead of child MagicMocks. - test_ProxyConfig_get_model_info_with_id_missing_model_id_raises: pin premium_user so the asserted AttributeError no longer flips with the ambient license global.
…/messages
Azure AI Foundry and Vertex AI serve Claude on the first-party Anthropic
Messages contract, which was verified live to be byte-identical to
api.anthropic.com: a leading role:"system" entry in messages is rejected on
every model ("messages.0: use the top-level 'system' parameter"), and a
mid-conversation role:"system" reminder is accepted in place on Claude 4.8+/5
but 400s on Claude 4.7 and older ("role 'system' is not supported on this
model"). This is the same contract Bedrock Invoke already handles model-aware
(PRs #32578/#32831/#32882); Vertex and Azure did no hoisting at all, so a Claude
Code session on an older Vertex/Azure Claude model hard-400s on its reminder
turns, and the only thing sparing 4.8+/5 was that nothing was hoisted
Extract Bedrock's model-gated normalization into the shared
AnthropicMessagesConfig base as _normalize_system_role_messages and call it from
the Vertex and Azure messages configs. Flagged models (4.8+/5) hoist only the
leading run of system entries and keep mid-conversation reminders in place so
the top-level system prefix stays byte-identical and the prompt cache is
preserved; unflagged models hoist every system entry so the request returns a
completion instead of a 400
Add supports_mid_conversation_system to the azure_ai and vertex_ai Claude 4.8+/5
cost-map entries. Exact cost-map hits win over the claude-mid-conversation-system
fallback rule, so without the explicit flag those models would be treated as
unsupported and hoist every reminder, collapsing the prompt cache (the exact
customer regression). A per-provider test guards this so future 4.8+/5 entries
cannot silently miss the flag
Closes the Vertex/Azure gap from the customer RCA
Ran the before/after proof live against Vertex Claude (global endpoint, project vertex-check-481318): base transform 400s an unflagged model (claude-opus-4-7) on a mid-conversation role:system reminder, the fix hoists it to a 200, and a flagged model (claude-opus-4-8) keeps the reminder in messages with cache_read held at 15615 across the reminder turn. Flip both vertex.mid_conversation_system rows to fail_before_fix: proven.
cc49913 to
335b79d
Compare
…th a clear assertion
Relevant issues
Closes the Vertex/Azure gap called out in the customer RCA (High Spend on Claude Code via Bedrock Invoke), item 3 under "Gaps still there": test Vertex and Azure and hoist mid-conversation system messages where needed
Linear ticket
Resolves LIT-4563
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
@greptileaito re-request a review after pushing changes)Screenshots / Proof of Fix
All calls hit real Claude on Azure AI Foundry (
litellm-e2e-suite-resource.services.ai.azure.com), no mocks, real spend.azure-opus-4-8is flaggedsupports_mid_conversation_system;azure-opus-4-7is not. The request is a mid-session Claude Code shape: a top-levelsystem, a user turn, arole: "system"reminder, an assistant turn, a fresh user turnBEFORE (base
40e914cfa7, no fix) — proxy on the base commit,role: "system"forwarded in place:Every Claude Code session on an older Azure/Vertex Claude model 400s the moment it appends a reminder. The supported model works only because nothing is hoisted, which is also why a naive fix that hoisted everything would collapse its prompt cache (the regression from the customer RCA)
AFTER (this PR,
23b5b7d199) — same proxy, same requests:Wire-level (detailed_debug), the request LiteLLM sends upstream:
Cache preserved on the flagged model — two turns against
azure-opus-4-8, second turn carries the mid-conversation reminder:The reminder does not mutate the
systemprefix, socache_readstays at the full 6013 instead of collapsing to a fresh writeVertex AI (live, real spend) — same proof over
vertex_ai/claude-opus-4-8(flagged) andvertex_ai/claude-opus-4-7(unflagged) on the Vertex global endpoint (projectvertex-check-481318):Wire-level (detailed_debug), the request LiteLLM sends upstream to Vertex:
Cache preserved on the flagged Vertex model — two turns against
vertex-opus-4-8, second turn carries the reminder:Vertex's rejection wording differs from Azure's ("Unexpected role system / use the top-level system parameter" vs "role 'system' is not supported on this model"), but the behavior class is identical: the older/unflagged Claude rejects a mid-conversation
role: "system"while the flagged 4.8+/5 model accepts it in place, so the same model-aware hoist closes both. Bothvertex.mid_conversation_systemregistry rows are nowfail_before_fix: provenType
🐛 Bug Fix
✅ Test
Changes
Azure AI Foundry and Vertex AI serve Claude on the first-party Anthropic
/v1/messagescontract. Probingapi.anthropic.comand Azure Foundry live returned byte-identical validation: a leadingrole: "system"entry insidemessagesis rejected on every model ("messages.0: use the top-level 'system' parameter for the initial system prompt"), a mid-conversationrole: "system"reminder is accepted in place on Claude 4.8+/5 but rejected on Claude 4.7 and older ("role 'system' is not supported on this model"). Bedrock Invoke already handles this model-aware (#32578/#32831/#32882); Vertex and Azure did no hoisting at all_normalize_system_role_messagesinto the sharedAnthropicMessagesConfigbase and call it from the Vertex and Azure messages configs. Flagged models (supports_mid_conversation_system: Claude 4.8+/5) hoist only the leading run of system entries and keep mid-conversation reminders in place so the top-levelsystemprefix stays byte-identical and the prompt cache survives; unflagged models hoist every system entry so the request returns a completion instead of a 400. Bedrock keeps its exact behavior through the shared methodsupports_mid_conversation_systemto theazure_aiandvertex_aiClaude 4.8+/5 cost-map entries in bothmodel_prices_and_context_window.jsonand the bundled backup. Exact cost-map hits win over theclaude-mid-conversation-systemfallback rule, so without the explicit flag these models would be treated as unsupported and hoist every reminder, collapsing the cache. A per-provider test guards this so future 4.8+/5 entries cannot silently miss the flaganthropic/path is intentionally left untouched: it forwardsmessagesas before and keeps billing-header attribution (should_strip_billing_metadatastays False there)Unit tests cover leading-run hoisting, mid-conversation preservation on flagged models, hoist-all on unflagged models, and the flag-coverage guard, for both Azure and Vertex. Mutation-checked: disabling the hoist kills the leading-run and unflagged tests; removing the flag kills the keep-in-place, leading-run, and flag-coverage tests (the exact cache-collapse mutant)
QA runbook
Prerequisites: a proxy with an Azure AI Foundry Anthropic deployment for a flagged model (
azure_ai/claude-opus-4-8) and an unflagged one (azure_ai/claude-opus-4-7) viaapi_base: os.environ/AZURE_AI_API_BASE,api_key: os.environ/AZURE_AI_API_KEY; and Vertex deployments (vertex_ai/claude-opus-4-8,vertex_ai/claude-sonnet-4-6) viavertex_project: os.environ/VERTEXAI_PROJECT,vertex_location: global. The Vertex legs need working Vertex credentials in the runnermessagesso the prompt cache written on turn one is read back in full on turn twoazure_ai/claude-opus-4-8; prime a >1024-token cachedsystem+ a cache-marked user turn untilcache_read>0andcache_creation>0system, the primed user turn (cached), arole: "system"reminder, an assistant turn, a fresh cached user turncache_read_input_tokens >= (turn-one system prefix + first user turn)systemazure_ai/claude-opus-4-7; POST/v1/messageswith a top-levelsystem, a user turn, arole: "system"reminder, an assistant turn, a user turnrole: "assistant"and non-empty text (base code returns 400 "role 'system' is not supported on this model")vertex_ai/claude-opus-4-8vertex_ai/claude-opus-4-8; prime and assertcache_readis preserved across the reminder turn as abovevertex_ai/claude-sonnet-4-6vertex_ai/claude-sonnet-4-6; POST the reminder conversation and expect HTTP 200 with non-empty assistant textFinal Attestation