Skip to content

output_config.format cannot take the pydantic model that output_format deprecates, and the dict workaround silently skips transform_schema #1819

Description

@percymcn

Summary

On anthropic==0.121.0, passing output_format= to the beta messages resource emits

DeprecationWarning: The 'output_format' parameter is deprecated. Please use 'output_config.format' instead.
— resources/beta/messages/messages.py:4229

but output_config.format cannot accept a pydantic model, which is the input form
output_format= exists for. Following the warning literally raises TypeError, and the
natural workaround (hand it Model.model_json_schema() as a dict) silently skips
transform_schema
— so migrating changes the schema that goes on the wire.

Repro

Executed as written against anthropic==0.121.0 / pydantic==2.13.4, with
httpx.Client.send patched to capture the request body and return a 401 (no API key,
nothing leaves the machine).

class Item(BaseModel):
    code: str = Field(pattern=r'^[A-Z]{3}$', min_length=3)

A — the deprecated form. Warns, and transform_schema runs:

{"type":"object","title":"Item",
 "properties":{"code":{"type":"string","title":"Code",
                       "description":"{minLength: 3, pattern: ^[A-Z]{3}$}"}},
 "additionalProperties":false,"required":["code"]}

B — the migration the warning names. output_config={"format": Item}:

TypeError: Object of type ModelMetaclass is not JSON serializable

C — the workaround, output_config={"format": {"type":"json_schema","schema": Item.model_json_schema()}}:

{"properties":{"code":{"minLength":3,"pattern":"^[A-Z]{3}$","title":"Code","type":"string"}},
 "required":["code"],"title":"Item","type":"object"}

C is not A. additionalProperties: false is gone, and minLength/pattern are no
longer folded into description. So a user who follows the deprecation notice stops
getting the normalization the SDK applies today, with no error and no warning.

Cause

transform_schema runs only on the elif branch of

if is_dict(output_format):                       # beta messages.py:1769, :3778
    transformed_output_format = cast(...)        # cast through, untransformed
elif is_given(output_format) and output_format is not None:
    schema = adapted_type.json_schema()
    transformed_output_format = JSONOutputFormatParam(schema=transform_schema(schema), ...)

output_config has no equivalent branch at all — its format is forwarded as given, so
every form of it is untransformed. Measured across create / parse / stream and both
the beta and non-beta resources: output_config.format never transforms.

Two smaller notes from the same measurement:

  • The DeprecationWarning fires on the beta resource only. messages.parse,
    messages.stream and messages.count_tokens on the non-beta resource accept
    output_format= silently, so the two resources disagree about whether this parameter
    is deprecated.
  • count_tokens(output_format=<dict>) is also untransformed while
    count_tokens(output_format=<model>) is transformed, so a token count taken via the
    dict form is counting a different document than the one parse() would send.

Suggested shape of a fix

Either route output_config.format through the same branch (accept a type there and
transform it), or state in the deprecation message that output_config.format takes a
pre-built json_schema dict and applies no transform — the current wording implies a
drop-in rename, and it is not one.

Test-design warning

A regression test that asserts only "the request was built" cannot see this — the
request builds fine in both A and C. It has to assert on the emitted schema. And a
fixture whose model carries no constrained fields is weaker than it looks: it will still
differ on additionalProperties, but nothing else, so the demotion half of the change
goes untested. Use a field with pattern=/min_length= as above.

Scope

No API key was used, so this is entirely about the request the SDK builds; I am not
claiming anything about how the service treats either payload.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions