Skip to content

Gemini connector joins plugin and function names with "__", which is legal in both, so two functions can share one tool name and the wrong one runs #14399

Description

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions