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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions