Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions src/fast_agent/llm/provider/google/google_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ def _clean_schema_for_google(self, schema: Dict[str, Any]) -> Dict[str, Any]:
if key in unsupported_keys:
continue # Skip this key

# Rewrite unsupported 'const' to a safe form for Gemini tools
# - For string const, convert to enum [value]
# - For non-string const (booleans/numbers), drop the constraint
if key == "const":
if isinstance(value, str):
cleaned_schema["enum"] = [value]
continue

if (
key == "format"
and schema.get("type") == "string"
Expand Down Expand Up @@ -140,9 +148,8 @@ def convert_to_google_content(
)
elif is_resource_content(part_content):
assert isinstance(part_content, EmbeddedResource)
if (
"application/pdf" == part_content.resource.mimeType
and isinstance(part_content.resource, BlobResourceContents)
if "application/pdf" == part_content.resource.mimeType and isinstance(
part_content.resource, BlobResourceContents
):
pdf_bytes = base64.b64decode(part_content.resource.blob)
parts.append(
Expand Down
38 changes: 38 additions & 0 deletions tests/e2e/llm/test_llm_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ def get_test_models():
"gpt-5-mini.low",
"gemini25",
"gpt-oss",
# "responses.gpt-5-mini",
# "generic.qwen3:8b",
]

Expand Down Expand Up @@ -92,6 +93,23 @@ async def llm_agent_setup(model_name):
inputSchema=_input_schema,
)

_const_input_schema = {
"type": "object",
"properties": {
"mode": {
"type": "string",
"const": "auto-cancel",
"description": "The mode must always be the literal 'auto-cancel'.",
}
},
"required": ["mode"],
}
_const_tool = Tool(
name="const_mode",
description="Demonstrates a tool schema that includes a const constraint.",
inputSchema=_const_input_schema,
)


@pytest.mark.e2e
@pytest.mark.asyncio
Expand Down Expand Up @@ -194,6 +212,26 @@ async def test_tool_user_continuation(llm_agent_setup, model_name):
assert "sunny" in result.last_text().lower()


@pytest.mark.e2e
@pytest.mark.asyncio
@pytest.mark.parametrize("model_name", TEST_MODELS)
async def test_tool_const_schema(llm_agent_setup, model_name):
"""Ensure providers accept tool schemas that include const constraints."""
agent = llm_agent_setup
result = await agent.generate(
"call the const_mode tool so I can confirm the mode you must use.",
tools=[_const_tool],
request_params=RequestParams(maxTokens=400),
)

assert result.stop_reason is LlmStopReason.TOOL_USE
assert result.tool_calls
assert 1 == len(result.tool_calls)
tool_id = next(iter(result.tool_calls.keys()))
tool_call: CallToolRequest = result.tool_calls[tool_id]
assert "const_mode" == tool_call.params.name


@pytest.mark.e2e
@pytest.mark.asyncio
@pytest.mark.parametrize("model_name", TEST_MODELS)
Expand Down
23 changes: 23 additions & 0 deletions tests/unit/fast_agent/llm/providers/test_google_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,26 @@ def test_convert_function_results_to_google_text_only():
assert fn_resp.name == "weather"
assert isinstance(fn_resp.response, dict)
assert fn_resp.response.get("tool_name") == "weather"


def test_clean_schema_for_google_const_string_to_enum():
converter = GoogleConverter()
schema = {"type": "string", "const": "all"}
cleaned = converter._clean_schema_for_google(schema)
# Expect const rewritten to enum ["all"]
assert "const" not in cleaned
assert cleaned.get("enum") == ["all"]


def test_clean_schema_for_google_const_non_string_dropped():
converter = GoogleConverter()
schema_bool = {"type": "boolean", "const": True}
cleaned_bool = converter._clean_schema_for_google(schema_bool)
# Non-string const dropped
assert "const" not in cleaned_bool
assert "enum" not in cleaned_bool

schema_num = {"type": "number", "const": 3.14}
cleaned_num = converter._clean_schema_for_google(schema_num)
assert "const" not in cleaned_num
assert "enum" not in cleaned_num
Loading