Skip to content

fix(relaykit): preserve full JSON Schema constraints in Gemini tool conversion #485

Description

@LIghtJUNction

Upstream source

Problem

Gemini functionDeclarations.parameters accepts only Gemini's OpenAPI-style schema subset. The current converter calls CleanFunctionParameters, which silently drops valid full-JSON-Schema keywords such as const, additionalProperties, oneOf, propertyNames, exclusiveMinimum, etc.

That changes tool semantics without returning an error. Example: a client schema that pins a tool argument with const can be forwarded as an empty {} schema, allowing values the caller explicitly prohibited.

Gemini also supports parametersJsonSchema, which can carry the full schema. Upstream #7537 starts using that field whenever cleaning would otherwise remove unsupported keywords.

api.lmm.best gap

Current main (517d29ed1d575add0796be0fb166b426b9987f2f when this issue was opened) still has the same vulnerable conversion paths:

  • apps/api-go/relaykit/relayconvert/internal/oai_chat/to_gemini_chat_req.go calls sharedgemini.CleanFunctionParameters directly.
  • apps/api-go/relaykit/relayconvert/internal/oai_responses/to_gemini_chat_req.go does the same.
  • apps/api-go/relaykit/relayconvert/internal/shared/gemini/schema.go drops non-allowlisted keywords.
  • LMM currently has no parametersJsonSchema output field in dto.FunctionRequest.

This is distinct from #479. #479 sanitizes invalid required: null values before strict providers see them; this issue is about preserving valid JSON Schema constraints when converting to Gemini.

Why not copy upstream #7537 yet

The upstream patch is still open and its current head has unresolved review findings that can still weaken schemas:

  1. The unsupported-keyword detector stops at the depth cutoff before inspecting deeper nodes, so constraints below that boundary may still be stripped.
  2. Tuple-form items: [...] currently checks only the first item; unsupported keywords in later tuple entries can be missed.
  3. EncodeFunctionParametersForGemini checks properties: {} before unsupported keywords, so a constrained empty object such as { "type":"object", "properties":{}, "additionalProperties":false } can lose the constraint entirely.

Upstream CI is also action_required for the forked PR, while CodeRabbit explicitly reports moderate merge risk. Porting that exact patch now would copy known correctness gaps into LMM.

Recommended LMM-native implementation

Once the upstream review findings are resolved (or independently implement them here):

  1. Add ParametersJsonSchema any \json:"parametersJsonSchema,omitempty"`` to the Gemini function declaration representation used by LMM.
  2. Centralize selection in one helper:
    • if the schema contains only Gemini parameters-safe keywords, preserve the existing cleaned parameters path;
    • if any reachable schema node contains a keyword that cleaning would remove, emit the original schema as parametersJsonSchema and omit parameters;
    • never emit both fields.
  3. Detect unsupported constraints across all reachable schema-bearing locations used here, including object properties, every array/tuple item and combinators used by the converter.
  4. Keep recursion bounded/cycle-safe without silently treating uninspected deep descendants as safe.
  5. Test unsupported-keyword detection before the existing properties: {} omission so constrained empty objects remain constrained.
  6. Wire the helper through OpenAI Chat → Gemini, Responses → Gemini and generic toolconv → Gemini paths.
  7. Do not alter raw same-protocol pass-through behavior, pricing, groups, model price lock, /fast, OAuth restrictions or the administrator AI assistant.

Acceptance criteria

  • A nested const survives conversion to Gemini via parametersJsonSchema.
  • additionalProperties: false, oneOf, propertyNames and numeric constraints that the current cleaner would remove are preserved.
  • Tuple-form items preserves an unsupported constraint in any tuple element, not only index 0.
  • A constrained empty object (properties: {} plus an unsupported constraint) is preserved rather than omitted.
  • A schema with only Gemini-supported fields continues to use cleaned parameters and retains current type normalization behavior.
  • A genuinely empty allowlisted object keeps the current omission behavior where required for Gemini compatibility.
  • The converter never emits both parameters and parametersJsonSchema for one declaration.
  • Deep schemas do not silently lose constraints at the recursion boundary; tests cover the chosen depth/cycle policy.
  • OpenAI Chat → Gemini, Responses → Gemini and generic relayconvert/toolconv paths all have regression coverage.
  • Existing relaykit tests and Go build pass.

Validation status

Static comparison confirms LMM still contains the affected cleaner/call sites and has no existing parametersJsonSchema implementation or duplicate PR. No code PR is opened in this run because the only upstream implementation currently has unresolved correctness findings; this issue records the stable behavior target so we can port the corrected form rather than the known-incomplete patch.

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