fix: sanitize gemini openai tool schemas#7703
fix: sanitize gemini openai tool schemas#7703bugkeep wants to merge 1 commit intoAstrBotDevs:masterfrom
Conversation
There was a problem hiding this comment.
Hey - I've found 1 issue
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="astrbot/core/agent/tool.py" line_range="277" />
<code_context>
+
+ return result
+
+ def openai_schema(
+ self,
+ omit_empty_parameter_field: bool = False,
</code_context>
<issue_to_address>
**issue (complexity):** Consider splitting Gemini-specific behavior into dedicated helpers and an explicit `openai_gemini_schema` method so `openai_schema` keeps a single, clear responsibility and provider concerns stay separated.
You can keep the new functionality but localize the complexity by:
1. Splitting the Gemini behavior out of `openai_schema` into a dedicated method.
2. Making the boolean flag a thin compatibility wrapper instead of the primary contract.
3. Keeping `_google_compatible_schema` provider-focused and adding an OpenAI adapter.
For example:
```python
@staticmethod
def _gemini_function_schema(schema: dict[str, Any]) -> dict[str, Any]:
"""Convert schema to the subset accepted by Gemini function declarations."""
# (move the current _google_compatible_schema implementation here)
...
```
Then make OpenAI’s Gemini-compatible variant explicit:
```python
def openai_schema(
self,
omit_empty_parameter_field: bool = False,
gemini_compatible_schema: bool = False,
) -> list[dict]:
"""
Convert tools to OpenAI API function calling schema format.
gemini_compatible_schema is kept for backwards compatibility; prefer
openai_gemini_schema() for new code.
"""
if gemini_compatible_schema:
return self.openai_gemini_schema(omit_empty_parameter_field)
result = []
for tool in self.tools:
func_def = {"type": "function", "function": {"name": tool.name}}
if tool.description:
func_def["function"]["description"] = tool.description
if tool.parameters is not None:
if (
tool.parameters and tool.parameters.get("properties")
) or not omit_empty_parameter_field:
func_def["function"]["parameters"] = tool.parameters
result.append(func_def)
return result
def openai_gemini_schema(
self,
omit_empty_parameter_field: bool = False,
) -> list[dict]:
"""OpenAI schema adapted to Gemini-compatible subset."""
result = []
for tool in self.tools:
func_def = {"type": "function", "function": {"name": tool.name}}
if tool.description:
func_def["function"]["description"] = tool.description
if tool.parameters is not None:
if (
tool.parameters and tool.parameters.get("properties")
) or not omit_empty_parameter_field:
func_def["function"]["parameters"] = self._gemini_function_schema(
tool.parameters
)
result.append(func_def)
return result
```
`google_schema` then stays clearly provider-specific:
```python
def google_schema(self) -> dict:
"""Convert tools to Google GenAI API format."""
tools = []
for tool in self.tools:
d: dict[str, Any] = {"name": tool.name}
if tool.description:
d["description"] = tool.description
if tool.parameters:
d["parameters"] = self._gemini_function_schema(tool.parameters)
tools.append(d)
declarations: dict[str, Any] = {}
if tools:
declarations["function_declarations"] = tools
return declarations
```
Finally, you can keep the deprecated method’s surface area minimal by delegating to the primary OpenAI method without exposing the new mode in its signature:
```python
@deprecated(reason="Use openai_schema() instead", version="4.0.0")
def get_func_desc_openai_style(
self,
omit_empty_parameter_field: bool = False,
):
# If you really need Gemini semantics here, call openai_gemini_schema()
return self.openai_schema(omit_empty_parameter_field)
```
This preserves all current behavior but:
- `openai_schema` regains a single clear responsibility.
- Gemini-specific behavior is discoverable via `openai_gemini_schema()` and `_gemini_function_schema`.
- Provider-specific schema logic is not conflated under a “google-compatible” helper reused from OpenAI.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
|
|
||
| return result | ||
|
|
||
| def openai_schema( |
There was a problem hiding this comment.
issue (complexity): Consider splitting Gemini-specific behavior into dedicated helpers and an explicit openai_gemini_schema method so openai_schema keeps a single, clear responsibility and provider concerns stay separated.
You can keep the new functionality but localize the complexity by:
- Splitting the Gemini behavior out of
openai_schemainto a dedicated method. - Making the boolean flag a thin compatibility wrapper instead of the primary contract.
- Keeping
_google_compatible_schemaprovider-focused and adding an OpenAI adapter.
For example:
@staticmethod
def _gemini_function_schema(schema: dict[str, Any]) -> dict[str, Any]:
"""Convert schema to the subset accepted by Gemini function declarations."""
# (move the current _google_compatible_schema implementation here)
...Then make OpenAI’s Gemini-compatible variant explicit:
def openai_schema(
self,
omit_empty_parameter_field: bool = False,
gemini_compatible_schema: bool = False,
) -> list[dict]:
"""
Convert tools to OpenAI API function calling schema format.
gemini_compatible_schema is kept for backwards compatibility; prefer
openai_gemini_schema() for new code.
"""
if gemini_compatible_schema:
return self.openai_gemini_schema(omit_empty_parameter_field)
result = []
for tool in self.tools:
func_def = {"type": "function", "function": {"name": tool.name}}
if tool.description:
func_def["function"]["description"] = tool.description
if tool.parameters is not None:
if (
tool.parameters and tool.parameters.get("properties")
) or not omit_empty_parameter_field:
func_def["function"]["parameters"] = tool.parameters
result.append(func_def)
return result
def openai_gemini_schema(
self,
omit_empty_parameter_field: bool = False,
) -> list[dict]:
"""OpenAI schema adapted to Gemini-compatible subset."""
result = []
for tool in self.tools:
func_def = {"type": "function", "function": {"name": tool.name}}
if tool.description:
func_def["function"]["description"] = tool.description
if tool.parameters is not None:
if (
tool.parameters and tool.parameters.get("properties")
) or not omit_empty_parameter_field:
func_def["function"]["parameters"] = self._gemini_function_schema(
tool.parameters
)
result.append(func_def)
return resultgoogle_schema then stays clearly provider-specific:
def google_schema(self) -> dict:
"""Convert tools to Google GenAI API format."""
tools = []
for tool in self.tools:
d: dict[str, Any] = {"name": tool.name}
if tool.description:
d["description"] = tool.description
if tool.parameters:
d["parameters"] = self._gemini_function_schema(tool.parameters)
tools.append(d)
declarations: dict[str, Any] = {}
if tools:
declarations["function_declarations"] = tools
return declarationsFinally, you can keep the deprecated method’s surface area minimal by delegating to the primary OpenAI method without exposing the new mode in its signature:
@deprecated(reason="Use openai_schema() instead", version="4.0.0")
def get_func_desc_openai_style(
self,
omit_empty_parameter_field: bool = False,
):
# If you really need Gemini semantics here, call openai_gemini_schema()
return self.openai_schema(omit_empty_parameter_field)This preserves all current behavior but:
openai_schemaregains a single clear responsibility.- Gemini-specific behavior is discoverable via
openai_gemini_schema()and_gemini_function_schema. - Provider-specific schema logic is not conflated under a “google-compatible” helper reused from OpenAI.
There was a problem hiding this comment.
Code Review
This pull request refactors Gemini-compatible schema generation into a static method and adds a gemini_compatible_schema flag to openai_schema to sanitize tool parameters for Gemini models. The openai_source provider is updated to apply this sanitization for Gemini models, and unit tests are added to verify the behavior. Feedback was provided to explicitly set the nullable property when converting type lists that include "null" to ensure nullability is preserved in the Gemini API.
| # Gemini API expects 'type' to be a string, while JSON Schema allows lists. | ||
| if isinstance(origin_type, list): | ||
| target_type = next((t for t in origin_type if t != "null"), "string") |
There was a problem hiding this comment.
When converting a list of types (e.g., ["string", "null"]) to a single type for Gemini, the information about nullability is lost unless the nullable keyword is also present in the original schema. While Gemini supports the nullable field, it might be safer to explicitly set result["nullable"] = True if "null" was present in the origin_type list, ensuring the model knows the field can be null.
| # Gemini API expects 'type' to be a string, while JSON Schema allows lists. | |
| if isinstance(origin_type, list): | |
| target_type = next((t for t in origin_type if t != "null"), "string") | |
| # Gemini API expects 'type' to be a string, while JSON Schema allows lists. | |
| if isinstance(origin_type, list): | |
| if "null" in origin_type: | |
| result["nullable"] = True | |
| target_type = next((t for t in origin_type if t != "null"), "string") |
|
I think it's better to delete |
Summary
examples,default, andadditionalProperties.Fixes #7572
Testing
uv run pytest tests/unit/test_tool_google_schema.py tests/unit/test_func_tool_manager.py -quv run pytest tests/unit -quv run ruff format .uv run ruff check .Summary by Sourcery
Sanitize OpenAI-compatible tool parameter schemas when used with Gemini models while preserving existing behavior for other providers.
New Features:
Bug Fixes:
Enhancements:
Tests: