Summary
The Gemini/Vertex connector joins plugin and function names with "__", but "__" is legal inside both halves. Two distinct kernel functions can therefore be emitted to the model under one identical tool name, and the reverse mapping routes the call to whichever wins the first-occurrence split — silently.
The comment above the constant already identifies this class
python/semantic_kernel/connectors/ai/google/shared_utils.py:
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.
The ambiguity was seen and "__" was chosen as the remedy. It closes the case where the function name contains a single underscore. It leaves open the case where the plugin name contains "__".
Where
GEMINI_FUNCTION_NAME_SEPARATOR = "__"
def format_gemini_function_name_to_kernel_function_fully_qualified_name(gemini_function_name: str) -> str:
if GEMINI_FUNCTION_NAME_SEPARATOR in gemini_function_name:
plugin_name, function_name = gemini_function_name.split(GEMINI_FUNCTION_NAME_SEPARATOR, 1)
return f"{plugin_name}{DEFAULT_FULLY_QUALIFIED_NAME_SEPARATOR}{function_name}"
return gemini_function_name
python/semantic_kernel/utils/validation.py permits _ in both:
PLUGIN_NAME_REGEX = ^[0-9A-Za-z_]+$
FUNCTION_NAME_REGEX = ^[0-9A-Za-z_-]+$
so plugin + "__" + function is not injective. Declarations are emitted without a duplicate-name check (google_ai/services/utils.py, vertex_ai/services/utils.py), so the model is handed two tools with the same name.
Reproduction
k = Kernel()
k.add_plugin(Utils(), plugin_name="utils") # function name: get__time
k.add_plugin(UtilsGet(), plugin_name="utils__get") # function name: time
=== tool declarations as the Gemini connector emits them ===
kernel fqn 'utils-get__time' -> gemini name 'utils__get__time'
kernel fqn 'utils__get-time' -> gemini name 'utils__get__time'
COLLISION: True
=== model calls 'utils__get__time' (meaning plugin utils__get / time) ===
converted kernel name: 'utils-get__time'
routed to plugin='utils' function='get__time'
python warnings raised: []
ACTUALLY EXECUTED: ['utils.get__time']
result returned to the model: 12:00 (from plugin 'utils', function 'get__time')
The model asked for one function, another ran, and the model was told it got what it asked for.
Negative controls
A — the default separator cannot collide, by construction. PLUGIN_NAME_REGEX forbids -, so split("-", maxsplit=1) always recovers the true plugin, and registering the colliding plugin name fails loudly:
LOUD: FunctionInitializationError: KernelFunction failed to initialize ...
'utils-get__time' -> plugin='utils' function='get__time' (correct)
B — a genuinely unknown name errors loudly:
The tool call with name `nosuch-func` is not part of the provided tools, please try again ...
Control A is the point: the same design applied to "__" would require forbidding __ in plugin names, and nothing does.
The silence
With logging at DEBUG and warnings.simplefilter("always"), the mis-routed call produced no warning and no error — only ordinary success logs:
INFO semantic_kernel.kernel: Calling utils-get__time function with args: {}
INFO semantic_kernel.functions.kernel_function: Function utils-get__time succeeded.
Three things compound it: the declaration list has no duplicate check; the allow-list in kernel.py compares against the post-split name, which is a genuinely allowed function, so validation passes; and the loud path that logs an error for a name resolving to nothing is never reached.
Scope
Reachability requires a plugin name containing __ in a kernel that also serves a Gemini/Vertex model. A function name containing __ alone round-trips correctly — I verified that. Plugin names come from a class name, an explicit plugin_name=, a directory name, or an OpenAPI/MCP import, so __ is uncommon but entirely legal and undefended.
The only test touching this exercises just the unambiguous plugin{SEP}function case, so the collision is untested rather than accepted.
Not tested: a live Gemini/Vertex round trip — the wire-name emission and reverse mapping are pure functions and were exercised directly, but I did not observe what Google's service does when handed two declarations sharing a name. If it rejects them, this degrades to a loud failure for that configuration; the local mis-split stands regardless. The dotnet/ and java/ implementations were out of scope.
I have not checked whether this was raised before; a pointer to an existing issue is welcome and I will close this in favour of it.
Version
semantic-kernel 1.44.1, source at ebc2ef9bb2d5b61db3b5c71fb5805c4824b4ad90, Python 3.12.
Summary
The Gemini/Vertex connector joins plugin and function names with
"__", but"__"is legal inside both halves. Two distinct kernel functions can therefore be emitted to the model under one identical tool name, and the reverse mapping routes the call to whichever wins the first-occurrence split — silently.The comment above the constant already identifies this class
python/semantic_kernel/connectors/ai/google/shared_utils.py:The ambiguity was seen and
"__"was chosen as the remedy. It closes the case where the function name contains a single underscore. It leaves open the case where the plugin name contains"__".Where
python/semantic_kernel/utils/validation.pypermits_in both:so
plugin + "__" + functionis not injective. Declarations are emitted without a duplicate-name check (google_ai/services/utils.py,vertex_ai/services/utils.py), so the model is handed two tools with the same name.Reproduction
The model asked for one function, another ran, and the model was told it got what it asked for.
Negative controls
A — the default separator cannot collide, by construction.
PLUGIN_NAME_REGEXforbids-, sosplit("-", maxsplit=1)always recovers the true plugin, and registering the colliding plugin name fails loudly:B — a genuinely unknown name errors loudly:
Control A is the point: the same design applied to
"__"would require forbidding__in plugin names, and nothing does.The silence
With logging at
DEBUGandwarnings.simplefilter("always"), the mis-routed call produced no warning and no error — only ordinary success logs:Three things compound it: the declaration list has no duplicate check; the allow-list in
kernel.pycompares against the post-split name, which is a genuinely allowed function, so validation passes; and the loud path that logs an error for a name resolving to nothing is never reached.Scope
Reachability requires a plugin name containing
__in a kernel that also serves a Gemini/Vertex model. A function name containing__alone round-trips correctly — I verified that. Plugin names come from a class name, an explicitplugin_name=, a directory name, or an OpenAPI/MCP import, so__is uncommon but entirely legal and undefended.The only test touching this exercises just the unambiguous
plugin{SEP}functioncase, so the collision is untested rather than accepted.Not tested: a live Gemini/Vertex round trip — the wire-name emission and reverse mapping are pure functions and were exercised directly, but I did not observe what Google's service does when handed two declarations sharing a name. If it rejects them, this degrades to a loud failure for that configuration; the local mis-split stands regardless. The
dotnet/andjava/implementations were out of scope.I have not checked whether this was raised before; a pointer to an existing issue is welcome and I will close this in favour of it.
Version
semantic-kernel1.44.1, source atebc2ef9bb2d5b61db3b5c71fb5805c4824b4ad90, Python 3.12.