diff --git a/python/semantic_kernel/connectors/ai/google/google_ai/services/utils.py b/python/semantic_kernel/connectors/ai/google/google_ai/services/utils.py index 0fd460b32433..0eeaa5c6f166 100644 --- a/python/semantic_kernel/connectors/ai/google/google_ai/services/utils.py +++ b/python/semantic_kernel/connectors/ai/google/google_ai/services/utils.py @@ -14,6 +14,7 @@ FUNCTION_CHOICE_TYPE_TO_GOOGLE_FUNCTION_CALLING_MODE, GEMINI_FUNCTION_NAME_SEPARATOR, sanitize_schema_for_google_ai, + validate_gemini_plugin_name, ) from semantic_kernel.contents.chat_message_content import ChatMessageContent from semantic_kernel.contents.function_call_content import FunctionCallContent @@ -148,6 +149,7 @@ def format_tool_message(message: ChatMessageContent) -> list[Part]: def kernel_function_metadata_to_google_ai_function_call_format(metadata: KernelFunctionMetadata) -> dict[str, Any]: """Convert the kernel function metadata to function calling format.""" + validate_gemini_plugin_name(metadata.plugin_name) parameters: dict[str, Any] | None = None if metadata.parameters: properties = {} diff --git a/python/semantic_kernel/connectors/ai/google/shared_utils.py b/python/semantic_kernel/connectors/ai/google/shared_utils.py index 1ff2d73c8342..fca2a224b20e 100644 --- a/python/semantic_kernel/connectors/ai/google/shared_utils.py +++ b/python/semantic_kernel/connectors/ai/google/shared_utils.py @@ -41,10 +41,22 @@ def filter_system_message(chat_history: ChatHistory) -> str | None: # This is required since Gemini doesn't work well with "-" in the function name. # https://ai.google.dev/gemini-api/docs/function-calling#function_declarations # Using double underscore to avoid situations where the function name already contains a single underscore. -# For example, we may incorrect split a function name with a single score when the function doesn't have a plugin name. +# A plugin name ending in a single underscore is also unsafe because its suffix +# overlaps the leading underscore of the separator. GEMINI_FUNCTION_NAME_SEPARATOR = "__" +def validate_gemini_plugin_name(plugin_name: str | None) -> None: + """Reject plugin names that collide with Gemini's function-name separator.""" + if plugin_name and ( + GEMINI_FUNCTION_NAME_SEPARATOR in plugin_name or plugin_name.endswith("_") + ): + raise ServiceInvalidRequestError( + f"Gemini function names use '{GEMINI_FUNCTION_NAME_SEPARATOR}' to separate plugin and function names; " + f"plugin name {plugin_name!r} contains that separator and cannot be represented safely." + ) + + def format_gemini_function_name_to_kernel_function_fully_qualified_name(gemini_function_name: str) -> str: """Format the Gemini function name to the kernel function fully qualified name.""" if GEMINI_FUNCTION_NAME_SEPARATOR in gemini_function_name: diff --git a/python/semantic_kernel/connectors/ai/google/vertex_ai/services/utils.py b/python/semantic_kernel/connectors/ai/google/vertex_ai/services/utils.py index 832899417ef9..bca2ce6ee764 100644 --- a/python/semantic_kernel/connectors/ai/google/vertex_ai/services/utils.py +++ b/python/semantic_kernel/connectors/ai/google/vertex_ai/services/utils.py @@ -12,6 +12,7 @@ FUNCTION_CHOICE_TYPE_TO_GOOGLE_FUNCTION_CALLING_MODE, GEMINI_FUNCTION_NAME_SEPARATOR, sanitize_schema_for_google_ai, + validate_gemini_plugin_name, ) from semantic_kernel.connectors.ai.google.vertex_ai.vertex_ai_prompt_execution_settings import ( VertexAIChatPromptExecutionSettings, @@ -138,6 +139,7 @@ def format_tool_message(message: ChatMessageContent) -> list[Part]: def kernel_function_metadata_to_vertex_ai_function_call_format(metadata: KernelFunctionMetadata) -> FunctionDeclaration: """Convert the kernel function metadata to function calling format.""" + validate_gemini_plugin_name(metadata.plugin_name) properties: dict[str, Any] = {} if metadata.parameters: for param in metadata.parameters: diff --git a/python/tests/unit/connectors/ai/google/google_ai/services/test_google_ai_utils.py b/python/tests/unit/connectors/ai/google/google_ai/services/test_google_ai_utils.py index 181d9f5d470e..64994359f43c 100644 --- a/python/tests/unit/connectors/ai/google/google_ai/services/test_google_ai_utils.py +++ b/python/tests/unit/connectors/ai/google/google_ai/services/test_google_ai_utils.py @@ -199,3 +199,27 @@ def test_google_ai_function_call_format_empty_parameters() -> None: ) result = kernel_function_metadata_to_google_ai_function_call_format(metadata) assert result["parameters"] is None + + +def test_google_ai_function_call_format_rejects_ambiguous_plugin_name() -> None: + metadata = KernelFunctionMetadata( + name="time", + plugin_name="utils__get", + description="Ambiguous plugin name", + is_prompt=False, + ) + + with pytest.raises(ServiceInvalidRequestError, match="cannot be represented safely"): + kernel_function_metadata_to_google_ai_function_call_format(metadata) + + +def test_google_ai_function_call_format_rejects_plugin_name_ending_in_separator_prefix() -> None: + metadata = KernelFunctionMetadata( + name="time", + plugin_name="utils_", + description="Plugin name overlaps the separator prefix", + is_prompt=False, + ) + + with pytest.raises(ServiceInvalidRequestError, match="cannot be represented safely"): + kernel_function_metadata_to_google_ai_function_call_format(metadata) diff --git a/python/tests/unit/connectors/ai/google/vertex_ai/services/test_vertex_ai_utils.py b/python/tests/unit/connectors/ai/google/vertex_ai/services/test_vertex_ai_utils.py index 882fec8915e6..32b849e35613 100644 --- a/python/tests/unit/connectors/ai/google/vertex_ai/services/test_vertex_ai_utils.py +++ b/python/tests/unit/connectors/ai/google/vertex_ai/services/test_vertex_ai_utils.py @@ -197,3 +197,27 @@ def test_vertex_ai_function_call_format_empty_parameters() -> None: ) result = kernel_function_metadata_to_vertex_ai_function_call_format(metadata) assert isinstance(result, FunctionDeclaration) + + +def test_vertex_ai_function_call_format_rejects_ambiguous_plugin_name() -> None: + metadata = KernelFunctionMetadata( + name="time", + plugin_name="utils__get", + description="Ambiguous plugin name", + is_prompt=False, + ) + + with pytest.raises(ServiceInvalidRequestError, match="cannot be represented safely"): + kernel_function_metadata_to_vertex_ai_function_call_format(metadata) + + +def test_vertex_ai_function_call_format_rejects_plugin_name_ending_in_separator_prefix() -> None: + metadata = KernelFunctionMetadata( + name="time", + plugin_name="utils_", + description="Plugin name overlaps the separator prefix", + is_prompt=False, + ) + + with pytest.raises(ServiceInvalidRequestError, match="cannot be represented safely"): + kernel_function_metadata_to_vertex_ai_function_call_format(metadata)