Gemini: nullable JSON Schema types are serialized into FunctionDeclaration.parameters and rejected
Summary
The Gemini provider sends generic JSON Schema tool parameters through
FunctionDeclaration.parameters.
When a schema contains a nullable JSON Schema type such as:
{
"type": ["string", "null"]
}
Gemini rejects the request because FunctionDeclaration.parameters uses
Gemini's OpenAPI-based Schema, where type is a scalar enum rather than an
array.
The request fails before model inference with HTTP 400:
Invalid JSON payload received. Unknown name "type" at
'tools[0].function_declarations[0].parameters.properties[1].value.items.properties[3].value':
Proto field is not repeating, cannot start list.
This is reproducible with LiteLLM v1.8.10 and the current repository commit
1d09e38ef9b756ab710c296350e551f6e178406d.
Origin of the function declaration
The original function is audit_foundation from
github.com/voocel/ainovel-cli.
The function declaration is defined here:
In particular, issues[].suggestion is declared as nullable:
schema.Property(
"suggestion",
llmcontract.Nullable(
schema.String("推荐修改方向;无需建议时为 null"),
),
).Required()
llmcontract.Nullable converts a scalar type into a JSON Schema union:
Equivalent output:
{
"suggestion": {
"description": "推荐修改方向;无需建议时为 null",
"type": ["string", "null"]
}
}
AINovel v0.7.9 currently depends on github.com/voocel/litellm v1.8.10.
Minimal reproduction
package main
import (
"context"
"log"
"os"
"github.com/voocel/litellm"
"github.com/voocel/litellm/provider/gemini"
)
func main() {
apiKey := os.Getenv("GEMINI_API_KEY")
if apiKey == "" {
log.Fatal("GEMINI_API_KEY is required")
}
model := os.Getenv("GEMINI_MODEL")
if model == "" {
model = "gemini-3.8-flash"
}
tool, err := litellm.NewTool(
"audit_foundation",
"Audit persisted foundation data for semantic consistency.",
map[string]any{
"type": "object",
"properties": map[string]any{
"fingerprint": map[string]any{
"type": "string",
},
"ready": map[string]any{
"type": "boolean",
},
"summary": map[string]any{
"type": "string",
},
"issues": map[string]any{
"type": "array",
"items": map[string]any{
"type": "object",
"properties": map[string]any{
"artifact": map[string]any{
"type": "string",
},
"description": map[string]any{
"type": "string",
},
"evidence": map[string]any{
"type": "string",
},
"suggestion": map[string]any{
"type": []string{"string", "null"},
},
},
"required": []string{
"artifact",
"description",
"evidence",
"suggestion",
},
},
},
},
"required": []string{
"fingerprint",
"ready",
"summary",
"issues",
},
},
)
if err != nil {
log.Fatal(err)
}
tool.Strict = litellm.StrictEnabled
client, err := gemini.NewClient(gemini.Config{
APIKey: apiKey,
})
if err != nil {
log.Fatal(err)
}
_, err = client.Chat(context.Background(), litellm.Request{
Model: model,
Messages: []litellm.Message{
litellm.UserText("Call audit_foundation with arbitrary test values."),
},
Tools: []litellm.Tool{tool},
ToolChoice: "auto",
MaxTokens: litellm.IntPtr(64),
})
if err != nil {
log.Fatal(err)
}
}
Run with:
export GEMINI_API_KEY='...'
go run .
Actual result:
Invalid JSON payload received. Unknown name "type" at
'tools[0].function_declarations[0].parameters.properties[1].value.items.properties[3].value':
Proto field is not repeating, cannot start list.
The same error was reproduced using the Go LiteLLM Gemini provider without AINovel or AgentCore in the call path.
Root cause
The Gemini wire type currently defines function parameters as:
type functionDeclaration struct {
Name string `json:"name"`
Description string `json:"description"`
Parameters map[string]any `json:"parameters,omitempty"`
}
convertTools unmarshals litellm.Tool.Parameters into a generic map and assigns
it directly to Parameters:
var params map[string]any
if len(t.Parameters) > 0 {
if err := json.Unmarshal(t.Parameters, ¶ms); err != nil {
return nil, false, fmt.Errorf(
"gemini: tool %q parameters must be object schema: %w",
t.Name,
err,
)
}
}
out.FunctionDeclarations = append(
out.FunctionDeclarations,
functionDeclaration{
Name: t.Name,
Description: t.Description,
Parameters: params,
},
)
Relevant implementation:
- https://github.com/voocel/litellm/blob/1d09e38ef9b756ab710c296350e551f6e178406d/provider/gemini/types.go#L71-L79
- https://github.com/voocel/litellm/blob/1d09e38ef9b756ab710c296350e551f6e178406d/provider/gemini/request.go#L346-L372
As a result, JSON Schema's array-valued type is sent through Gemini's legacy
OpenAPI Schema representation without conversion.
According to the Google Gemini GenerateContent API reference:
- Gemini's
Schemarepresents a selected subset of OpenAPI 3.0. Schema.typeis an enum, so it accepts one scalar type.Schema.nullableis the field used to indicate that a value may be null.
The same API reference documents two mutually exclusive fields on
FunctionDeclaration:
parameters: Gemini's OpenAPI-basedSchema.parametersJsonSchema: a JSON Schema value.
The Gemini function calling guide also describes function parameters as using a selected subset of OpenAPI Schema.
Therefore this JSON Schema:
{
"type": ["string", "null"]
}
cannot be forwarded unchanged through FunctionDeclaration.parameters.
When using parameters, the equivalent Gemini representation is:
{
"type": "string",
"nullable": true
}
Alternatively, raw JSON Schema should be sent using
parametersJsonSchema, subject to the Gemini API's supported JSON Schema
features.
Expected behavior
The Gemini provider should handle nullable JSON Schema tool parameters before sending the request.
Possible approaches:
Recursively normalize nullable unions when using
parameters:{ "type": ["string", "null"] }to:
{ "type": "string", "nullable": true }Send generic
litellm.Tool.ParametersthroughFunctionDeclaration.parametersJsonSchemarather thanparameters.If a schema cannot be represented safely by the selected Gemini field, return a local structured validation error instead of forwarding an invalid payload and relying on an HTTP 400 response.
Suggested regression coverage
Please add a Gemini request serialization test covering a nested nullable property, for example:
{
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"suggestion": {
"type": ["string", "null"]
}
},
"required": ["suggestion"]
}
}
},
"required": ["items"]
}
The resulting Gemini request should either:
- serialize the nested field as
"type": "string", "nullable": true; or - place the unmodified JSON Schema under
parametersJsonSchema.
It should not serialize an array-valued type under
FunctionDeclaration.parameters.
Source: voocel/litellm