Skip to content

Default to strict tool schemas for anthropic models that support it #3541

@dsfaccini

Description

@dsfaccini

Description

#3457 introduced support for strict tool schemas, but the JsonTransformer will default to tool.strict=False when the user doesn't explicitly set Tool(strict=True)

# in AnthropicJsonSchemaTransformer.walk()
self.is_strict_compatible = self.strict or False  # default to False if None

The desired behavior should be to default to True if the schema transformation is not lossy.

Here's a code snippet for potential handling:

# in profiles/anthropic.py
    @staticmethod
    def _has_lossy_changes(before: JsonSchema, after: JsonSchema) -> bool:  # noqa: C901
        """Check if transformation dropped validation constraints.

        Safe changes that don't count as lossy:
        - Adding additionalProperties: false
        - Removing title, $schema, or other metadata fields
        - Reordering keys

        Lossy changes:
        - Removing validation constraints (minLength, pattern, minimum, etc.)
        - Changing constraint values
        - Moving constraints to description field
        """

        def normalize(schema: JsonSchema) -> JsonSchema:
            """Remove fields that are safe to add/remove."""
            normalized = deepcopy(schema)

            # NOTE that this normalization can't be used because it's lossy in itself
            # if the tool schema has `additionalProperties`, the current anthropic sdk (0.75) doesn't support it
            normalized.pop('additionalProperties', None)

            normalized.pop('title', None)
            normalized.pop('$schema', None)
            return normalized

        def has_lossy_object_changes(before_obj: JsonSchema, after_obj: JsonSchema) -> bool:
            """Recursively check for lossy changes in object schemas.

            Returns:
                True if validation constraints were removed or modified (lossy changes detected).
                False if all validation constraints are preserved (no lossy changes).
            """
            validation_keys = {
                'minLength',
                'maxLength',
                'pattern',
                'format',
                'minimum',
                'maximum',
                'exclusiveMinimum',
                'exclusiveMaximum',
                'minItems',
                'maxItems',
                'uniqueItems',
                'minProperties',
                'maxProperties',
            }

            for key in validation_keys:
                if key in before_obj and key not in after_obj:
                    return True
                # should never happen that an sdk modifies a constraint value
                if key in before_obj and key in after_obj and before_obj[key] != after_obj[key]:
                    return True  # pragma: no cover

            before_props = before_obj.get('properties', {})
            after_props = after_obj.get('properties', {})
            for prop_name, before_prop in before_props.items():
                if prop_name in after_props:  # pragma: no branch
                    if has_lossy_schema_changes(before_prop, after_props[prop_name]):
                        return True

            if 'items' in before_obj and 'items' in after_obj:
                if has_lossy_schema_changes(before_obj['items'], after_obj['items']):
                    return True

            before_defs = before_obj.get('$defs', {})
            after_defs = after_obj.get('$defs', {})
            for def_name, before_def in before_defs.items():
                if def_name in after_defs:  # pragma: no branch
                    if has_lossy_schema_changes(before_def, after_defs[def_name]):  # pragma: no branch
                        return True

            return False

        def has_lossy_schema_changes(before_schema: JsonSchema, after_schema: JsonSchema) -> bool:
            """Check a single schema object for lossy changes.

            Returns:
                True if validation constraints were removed or modified (lossy changes detected).
                False if all validation constraints are preserved (no lossy changes).
            """
            if isinstance(before_schema, dict) and isinstance(after_schema, dict):
                return has_lossy_object_changes(before_schema, after_schema)
            # schemas should always be dicts
            assert_never(False)

        return has_lossy_schema_changes(normalize(before), normalize(after))

References

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions