Skip to content
This repository was archived by the owner on Sep 23, 2026. It is now read-only.

fix(kosong): strip JSON Schema metadata from Google GenAI tool parameters - #739

Open
xiaoju111a wants to merge 2 commits into
MoonshotAI:mainfrom
xiaoju111a:fix/google-genai-schema-metadata
Open

xiaoju111a wants to merge 2 commits into
MoonshotAI:mainfrom
xiaoju111a:fix/google-genai-schema-metadata

Conversation

@xiaoju111a

@xiaoju111a xiaoju111a commented Jan 28, 2026 •

Copy link
Copy Markdown
Contributor

Related Issue

Resolves #734

Description

This PR fixes a compatibility issue between Google GenAI provider and MCP tools that include standard JSON Schema metadata fields.

Problem

When using MCP tools (like Exa MCP) with Google GenAI provider, the following validation error occurs:

1 validation error for FunctionDeclaration
parameters.$schema
  Extra inputs are not permitted [type=extra_forbidden, input_value='http://json-schema.org/draft-07/schema#', input_type=str]

Root cause:

  • MCP tools' inputSchema includes standard JSON Schema metadata fields
  • Google GenAI SDK's Pydantic model has extra='forbid', which rejects these additional fields
  • Kimi CLI was passing the complete schema without filtering metadata

Solution

Strip JSON Schema metadata fields in tool_to_google_genai() function before passing to Google GenAI SDK. We filter 4 fields that are rejected by the SDK:

  • $schema, $id, $comment - JSON Schema metadata
  • examples - Example values (not part of validation schema)

Note: $defs and definitions are already removed by kosong's deref_json_schema() function, so we don't need to filter them here.

def tool_to_google_genai(tool: KosongTool) -> Tool:
    # Strip JSON Schema metadata fields (google-genai SDK has extra='forbid')
    # Note: $defs/definitions are already removed by kosong's deref_json_schema()
    parameters = {
        k: v
        for k, v in tool.parameters.items()
        if k not in ("$schema", "$id", "$comment", "examples")
    }
    
    return Tool(
        function_declarations=[
            FunctionDeclaration(
                name=tool.name,
                description=tool.description,
                parameters=parameters,
            )
        ]
    )

Impact

  • ✅ Only affects Google GenAI provider
  • ✅ Backward compatible (tools without metadata work as before)
  • ✅ No performance impact (simple dict filtering)
  • ✅ Follows JSON Schema best practices (metadata fields are not parameter definitions)

Testing

Manually verified the 4 rejected fields with Google GenAI SDK:

# Tested fields that cause ValidationError:
# ❌ $schema, $id, $comment, examples

# After fix: ✅ All metadata stripped automatically
# Tools work correctly with Google GenAI provider

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked the related issue, if any.
  • I have added tests that prove my fix is effective or that my feature works.
  • I have run make gen-changelog to update the changelog.
  • I have run make gen-docs to update the user documentation.

Open with Devin

…ters

Google GenAI SDK's Pydantic model has extra='forbid', which rejects
JSON Schema metadata fields like $schema, $id, and $comment.

This causes validation errors when using MCP tools that include
standard JSON Schema metadata in their inputSchema.

Fixes MoonshotAI#734

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 2 additional flags.

Open in Devin Review

@xxchan
xxchan requested a review from pvzheroes125 January 28, 2026 05:10

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 3 additional findings in Devin Review.

Open in Devin Review

Comment on lines +360 to +364
parameters = {
k: v
for k, v in tool.parameters.items()
if k not in ("$schema", "$id", "$comment", "examples")
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 JSON Schema metadata stripping only applied at top level, not recursively into nested schemas

The new filtering at packages/kosong/src/kosong/contrib/chat_provider/google_genai.py:360-364 only strips metadata fields ($schema, $id, $comment, examples) from the top-level keys of tool.parameters. However, these fields—especially examples—can appear at any depth in a JSON Schema (e.g., inside properties entries when Pydantic's Field(examples=[...]) is used). Since the google-genai SDK uses extra='forbid' (as noted in the comment on line 358), it will recursively validate nested schema dicts and reject any that contain these forbidden keys. A tool whose parameter field uses Field(examples=["foo"]) would produce a schema like {"properties": {"name": {"type": "string", "examples": ["foo"]}}}, and the nested examples would still cause the SDK to raise a validation error.

Prompt for agents
In packages/kosong/src/kosong/contrib/chat_provider/google_genai.py, the tool_to_google_genai function at lines 360-364 needs to recursively strip the metadata fields from the entire JSON Schema tree, not just the top-level dict. Replace the top-level dict comprehension with a recursive helper function that walks the schema dict (and any nested dicts/lists) and removes keys in the forbidden set ("$schema", "$id", "$comment", "examples") at every level. For example:

def _strip_schema_metadata(schema: dict) -> dict:
    FORBIDDEN = {"$schema", "$id", "$comment", "examples"}
    result = {}
    for k, v in schema.items():
        if k in FORBIDDEN:
            continue
        if isinstance(v, dict):
            result[k] = _strip_schema_metadata(v)
        elif isinstance(v, list):
            result[k] = [_strip_schema_metadata(item) if isinstance(item, dict) else item for item in v]
        else:
            result[k] = v
    return result

Then use parameters = _strip_schema_metadata(tool.parameters) in tool_to_google_genai().
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@percymcn

percymcn commented Aug 9, 2026

Copy link
Copy Markdown

There is a one-line version of this that removes the whole class of failure instead of the four keys, and I think it's worth considering before this lands: pass the schema to parameters_json_schema= rather than parameters=.

FunctionDeclaration has both fields. parameters= is the narrow OpenAPI-style Schema proto (extra='forbid', which is what raises here); parameters_json_schema= is the JSON-Schema field, and it takes the schema verbatim — no stripping, no rewriting. Measured on google-genai==2.17.0 with an Exa-shaped schema carrying every keyword at once:

RAW = {"$schema": "http://json-schema.org/draft-07/schema#", "type": "object",
       "properties": {"a": {"type": ["string", "null"]},
                      "c": {"type": "integer", "exclusiveMinimum": 0},
                      "d": {"type": "array", "prefixItems": [{"type": "integer"}]}}}

FunctionDeclaration(name="t", parameters=RAW)              # 6 validation errors
FunctionDeclaration(name="t", parameters_json_schema=RAW)  # accepted, byte-identical

This isn't an obscure corner — it's what the JS client already does for you. @google/genai checks for a top-level $schema and, when it finds one, routes the declaration to parametersJsonSchema and sends it unmodified. So the key that's crashing the Python client is the same key that tells the JS client to use the field that accepts everything. Python has no such auto-routing (there is no $schema handling anywhere in the package), which is why this only shows up here.

Two things about the current patch that I'd want on the record either way, both measured against the diff as written:

1. The strip is top-level only. tool.parameters.items() isn't recursive, and $comment / examples / $id almost always appear on individual properties rather than at the root. Running the PR's exact transform:

PR-fixed, top-level $schema     OK          <- the reported Exa case
PR-fixed, nested $comment       REJECTED    parameters.properties.q.$comment
PR-fixed, nested examples       REJECTED    parameters.properties.q.examples
PR-fixed, nested $id            REJECTED    parameters.properties.q.$id

2. Even a recursive strip leaves about a dozen more, and most of them have no $. Same transform, one keyword per run:

type: ["string","null"]  REJECTED    prefixItems      REJECTED
exclusiveMinimum         REJECTED    uniqueItems      REJECTED
enum: [1, 2]             REJECTED    const            REJECTED
oneOf                    REJECTED    anyOf            OK  (control)

plus array-form items, patternProperties, propertyNames, allOf and not. A $-prefix heuristic catches none of these, and MCP servers emit them routinely.

The two that matter most can't be fixed by stripping at all, because deleting them destroys the parameter rather than sanitising it:

  • type: ["string", "null"] — the standard spelling for an optional parameter. Dropping type leaves the model with no idea what to send. The lossless rewrite is {"type": "string", "nullable": true} — nullable is a real Schema field, verified accepted. (This is also exactly what @google/genai does silently on the narrow path.)
  • prefixItems: [int, int] / array-form items — dropping it turns "exactly two integers" into "an array of anything, any length", which returns a 200 and garbage. Lossless when the positions share a type: items: {...} + minItems/maxItems, both accepted.

So if the strip approach is preferred over the field switch, it needs to be a recursive rewrite, not a recursive delete.

Last thing, and it makes any of these testable: FunctionDeclaration(...) raises offline with no API key and no network, so the repro in #734 is already a CI test. Iterating a table of keyword → schema through it pins the accepted set in a few seconds and catches the next SDK version that moves the boundary.

(One surface caveat if this pattern gets reused elsewhere: the same SDK does inline $ref/$defs for you on response_schema, but not on FunctionDeclaration.parameters, where a $ref is rejected outright. Doesn't affect this PR — deref_json_schema() has already run — but the behaviour isn't uniform across the SDK's surfaces.)

This branch has not been deployed

No deployments
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Google GenAI provider fails with extra_forbidden for tool parameters containing $schema

3 participants