Repository: pydantic/pydantic-ai
File: pydantic_ai_slim/pydantic_ai/profiles/google.py (Python)

Code before:
    """Transforms the JSON Schema from Pydantic to be suitable for Gemini.

    Gemini supports [a subset of OpenAPI v3.0.3](https://ai.google.dev/gemini-api/docs/function-calling#function_declarations).

    Note: Gemini's tool calling system treats 'title' fields in nested schemas as callable function names,
    causing MALFORMED_FUNCTION_CALL errors. This fixes issue #3483 where nested Pydantic models were
    treated as tool calls instead of structured output schema. We remove 'title' from nested schemas
    (those defined in $defs) while preserving $ref/$defs structure for better schema organization.
    """

    def transform(self, schema: JsonSchema) -> JsonSchema:
        # Remove properties not supported by Gemini
        schema.pop('$schema', None)
        if (const := schema.pop('const', None)) is not None:
            # Gemini doesn't support const, but it does support enum with a single value
            schema['enum'] = [const]
        schema.pop('discriminator', None)
        schema.pop('examples', None)

        # Remove 'title' from nested schemas - Gemini treats these as callable function names
        # in tool calling mode, causing MALFORMED_FUNCTION_CALL errors for nested objects.
        # Only keep title at the root level for the function declaration name.
        # We detect nested schemas by checking if the title matches a key in $defs.
        if self.defs and 'title' in schema:
            # Check if this schema's title matches any key in $defs (meaning it's a nested definition)
            if schema.get('title') in self.defs:
                schema.pop('title', None)

        type_ = schema.get('type')
        if type_ == 'string' and (fmt := schema.pop('format', None)):
            description = schema.get('description')
            if description:
                schema['description'] = f'{description} (format: {fmt})'
            else:
                schema['description'] = f'Format: {fmt}'

        # Note: exclusiveMinimum/exclusiveMaximum are NOT yet supported
        schema.pop('exclusiveMinimum', None)
        schema.pop('exclusiveMaximum', None)

        return schema


Review comment (suggestion):
What do you think about just always dropping the `title`, like we do on OpenAI:

https://github.com/pydantic/pydantic-ai/blob/eae558b8412f721ccb87a12f87427ceb2afaf32d/pydantic_ai_slim/pydantic_ai/profiles/openai.py#L164-L166

And for tool def top level titles:

https://github.com/pydantic/pydantic-ai/blob/eae558b8412f721ccb87a12f87427ceb2afaf32d/pydantic_ai_slim/pydantic_ai/tools.py#L240-L241

---

My concern is that the def `title` is not guaranteed the `$defs` key, as the title could have been overridden by the user somehow.

Code after:
    """Transforms the JSON Schema from Pydantic to be suitable for Gemini.

    Gemini supports [a subset of OpenAPI v3.0.3](https://ai.google.dev/gemini-api/docs/function-calling#function_declarations).
    """

    def transform(self, schema: JsonSchema) -> JsonSchema:
        # Remove properties not supported by Gemini
        schema.pop('$schema', None)
        if (const := schema.pop('const', None)) is not None:
            # Gemini doesn't support const, but it does support enum with a single value
            schema['enum'] = [const]
        schema.pop('discriminator', None)
        schema.pop('examples', None)

        # Remove 'title' due to https://github.com/googleapis/python-genai/issues/1732
        schema.pop('title', None)

        type_ = schema.get('type')
        if type_ == 'string' and (fmt := schema.pop('format', None)):
            description = schema.get('description')
            if description:
                schema['description'] = f'{description} (format: {fmt})'
            else:
                schema['description'] = f'Format: {fmt}'

        # Note: exclusiveMinimum/exclusiveMaximum are NOT yet supported
        schema.pop('exclusiveMinimum', None)
        schema.pop('exclusiveMaximum', None)

        return schema