Skip to content

fix(anthropic): strip all remaining output_format schema keywords rejected by Anthropic - #34319

Merged
mateo-berri merged 2 commits into
litellm_internal_stagingfrom
litellm_anthropic_output_format_remaining_keywords
Jul 23, 2026
Merged

fix(anthropic): strip all remaining output_format schema keywords rejected by Anthropic#34319
mateo-berri merged 2 commits into
litellm_internal_stagingfrom
litellm_anthropic_output_format_remaining_keywords

Conversation

@mateo-berri

@mateo-berri mateo-berri commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Relevant issues

Follow-up to #34313 (the staging port of #33981). #33981 completed the array/object constraint keywords that were reported failing, but live probing of api.anthropic.com showed the strip list was still not exhaustive. This PR strips every remaining schema keyword the API rejects, so the whack-a-mole ends: probing confirmed unknown keywords are silently ignored by the API (a made-up key passes through fine), meaning the only 400 sources are the specific keywords Anthropic validates against, and all of them are now covered

Linear ticket

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • My PR passes all CI/CD checks (e.g., lint, format, unit tests)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Screenshots / Proof of Fix

Every keyword below was first probed directly against api.anthropic.com (claude-opus-4-8, structured-outputs-2025-11-13 beta header) to establish exactly what the API rejects: multipleOf, patternProperties, propertyNames, dependentRequired, dependentSchemas, unevaluatedProperties, if, then, else, not, and prefixItems all 400 with property '<keyword>' is not supported, and oneOf 400s with Schema type 'oneOf' is not supported. Accepted and left untouched: pattern, format, default, examples, readOnly, contentMediaType, unevaluatedItems, and unknown keys generally

The e2e runs are fully live with zero mocks: a LiteLLM proxy in front of a real Azure AI Foundry Claude deployment (azure_ai/claude-sonnet-4-6), costing real $. The before run is at 46a0809a51, the head of #34313 as it went into staging via 46440e2df4, so it is the exact pre-fix behavior shipping today. The after run is at this PR's head 16dad256d4, the merge of the latest litellm_internal_staging into the fix. Same nine requests both times, each a response_format: json_schema call whose schema carries the named keyword(s):

Schema keyword(s) Before 46a0809a51 After 16dad256d4
none (control) 200 200
multipleOf 400 200
patternProperties 400 200
propertyNames 400 200
dependentRequired+dependentSchemas 400 200
if+then+else 400 200
not 400 200
prefixItems 400 200
oneOf 400 200

Example request (the multipleOf case; the other payloads differ only in which keyword they set):

curl -sS "http://localhost:$PORT/v1/chat/completions" \
  -H "Authorization: Bearer sk-1234" -H 'Content-Type: application/json' -d '{
    "model": "claude-sonnet-4-6",
    "messages": [{"role": "user", "content": "Give me a number divisible by 5."}],
    "response_format": {"type": "json_schema", "json_schema": {"name": "num", "schema": {
      "type": "object", "properties": {"n": {"type": "integer", "multipleOf": 5}},
      "required": ["n"], "additionalProperties": false}}}
  }'

Raw results, before (46a0809a51):

clean             | HTTP 200 | {"colors":["red","blue","green"]}
multipleOf        | HTTP 400 | output_format.schema: For 'integer' type, property 'multipleOf' is not supported
patternProperties | HTTP 400 | output_format.schema: For 'object' type, property 'patternProperties' is not supported
propertyNames     | HTTP 400 | output_format.schema: For 'object' type, property 'propertyNames' is not supported
dependent         | HTTP 400 | output_format.schema: For 'object' type, property 'dependentRequired' is not supported
ifThenElse        | HTTP 400 | output_format.schema: For 'object' type, property 'if' is not supported
notKeyword        | HTTP 400 | output_format.schema: Schema keyword 'not' is not supported
prefixItems       | HTTP 400 | output_format.schema: For 'array' type, property 'prefixItems' is not supported
oneOf             | HTTP 400 | output_format.schema: Schema type 'oneOf' is not supported

Raw results, after (16dad256d4); note the outputs respect the stripped constraints via the advisory description notes (25 is a multiple of 5, the dog has a sound, the color is not red, the pair is two numbers). The empty meta objects are the injected additionalProperties: false at work once patternProperties/propertyNames are stripped from a schema that declares no named properties:

clean             | HTTP 200 | {"colors":["red","green","blue"]}
multipleOf        | HTTP 200 | {"n":25}
patternProperties | HTTP 200 | {"meta":{}}
propertyNames     | HTTP 200 | {"meta":{}}
dependent         | HTTP 200 | {"first":"John","last":"Smith"}
ifThenElse        | HTTP 200 | {"kind":"dog","sound":"woof"}
notKeyword        | HTTP 200 | {"color":"blue"}
prefixItems       | HTTP 200 | {"pair":[42.3601,-71.0589]}
oneOf             | HTTP 200 | {"id":42}

Type

🐛 Bug Fix

Changes

litellm/llms/anthropic/chat/transformation.py: filter_anthropic_output_schema now strips multipleOf, patternProperties, propertyNames, dependentRequired, dependentSchemas, unevaluatedProperties, if, then, else, not, and prefixItems into advisory description notes, alongside the keywords it already handled, and rewrites oneOf to anyOf exactly as the Anthropic SDK does. unsupported_fields is now derived from constraint_labels so there is a single source of truth, and the note builder iterates the insertion-ordered constraint_labels dict instead of a set, making the note order deterministic across processes; previously the order depended on PYTHONHASHSEED, so the same request could serialize differently on different proxy workers, which is hostile to request/prompt caching

Tests in tests/litellm/llms/anthropic/test_anthropic_schema_filter.py and the tests/test_litellm/ mirror cover each new keyword's removal and note text, the oneOf rewrite (including merging with an existing anyOf and recursive filtering inside variants), and a regression test asserting the exact note string for a five-constraint schema, which fails under set iteration with high probability

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

@greptile-apps

greptile-apps Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR extends the Anthropic output-schema filter to strip 11 additional JSON Schema keywords that the API rejects with 400 errors (multipleOf, patternProperties, propertyNames, dependentRequired, dependentSchemas, unevaluatedProperties, if, then, else, not, prefixItems), and rewrites oneOf to anyOf mirroring the Anthropic SDK. It also consolidates unsupported_fields as a derived set from constraint_labels and fixes non-deterministic note ordering caused by set iteration.

  • New keyword stripping: Each rejected keyword is added to constraint_labels and stripped at schema-traversal time, with its value serialized into an advisory description note so the model receives the constraint as a hint.
  • oneOfanyOf rewrite: oneOf variants are merged into any existing anyOf list (or a fresh one), matching the Anthropic SDK's own transformation; this trade-off is explicitly documented in code and PR.
  • Deterministic note ordering: Iterating the insertion-ordered constraint_labels dict instead of an unordered set ensures identical requests serialize the same way across proxy workers, preserving prompt-cache hits.

Confidence Score: 5/5

Safe to merge; the change is a pure expansion of an existing allow-list filter with no effect on schemas that do not contain the new keywords.

Every added keyword is independently strip-only: schemas that do not use the new keywords pass through the filter unchanged, so there is no regression surface for existing callers. The oneOf to anyOf rewrite is the only semantic transformation and is already documented and tested. The refactor from a hard-coded set to a derived set of constraint_labels is equivalent by construction. Tests cover each new keyword, the merge ordering, and the deterministic-note invariant.

No files require special attention.

Important Files Changed

Filename Overview
litellm/llms/anthropic/chat/transformation.py Extends filter_anthropic_output_schema with 11 new rejected keywords; refactors to single-source constraint_labels dict; rewrites oneOf to anyOf; fixes non-deterministic note ordering — all logic is correct and well-guarded.
tests/litellm/llms/anthropic/test_anthropic_schema_filter.py Adds targeted tests for every new keyword and the deterministic-ordering guarantee; tests are pure unit tests with no network calls and cover all branches including the oneOf merge.
tests/test_litellm/llms/anthropic/test_anthropic_output_format_filter.py Mirror coverage under tests/test_litellm/ for CI; all tests are mock-only with no network calls, compliant with the repo rule for this folder.

Reviews (3): Last reviewed commit: "Merge remote-tracking branch 'origin/lit..." | Re-trigger Greptile

Comment thread litellm/llms/anthropic/chat/transformation.py
@codecov

codecov Bot commented Jul 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.66667% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
litellm/llms/anthropic/chat/transformation.py 91.66% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

@codspeed-hq

codspeed-hq Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_anthropic_output_format_remaining_keywords (16dad25) with litellm_internal_staging (0c2b86e)1

Open in CodSpeed

Footnotes

  1. No successful run was found on litellm_internal_staging (eb2dce8) during the generation of this report, so 0c2b86e was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

…itellm_anthropic_output_format_remaining_keywords
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants