fix(gemini): prevent gemini incompatibility with some tools (#31877)
This commit is contained in:
parent
e2527db3c7
commit
2e71292f2f
2 changed files with 105 additions and 0 deletions
|
|
@ -1375,6 +1375,24 @@ export function schema(model: Provider.Model, schema: JSONSchema7): JSONSchema7
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gemini requires a single `type`, not a JSON Schema type array such as
|
||||||
|
// `["number","string"]` (emitted by some MCP servers). Plain `@ai-sdk/google`
|
||||||
|
// rewrites these into an `anyOf` of single-type schemas, but OpenAI-compatible
|
||||||
|
// transports (e.g. GitHub Copilot proxying to Gemini) forward them verbatim
|
||||||
|
// and the backend rejects the array form. Mirror the SDK: split non-null
|
||||||
|
// types into `anyOf`, and lift `null` into `nullable`.
|
||||||
|
if (Array.isArray(result.type)) {
|
||||||
|
const hasNull = result.type.includes("null")
|
||||||
|
const nonNull = result.type.filter((entry: unknown) => entry !== "null")
|
||||||
|
if (nonNull.length === 0) {
|
||||||
|
result.type = "null"
|
||||||
|
} else {
|
||||||
|
delete result.type
|
||||||
|
result.anyOf = nonNull.map((entry: unknown) => ({ type: entry }))
|
||||||
|
if (hasNull) result.nullable = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Filter required array to only include fields that exist in properties
|
// Filter required array to only include fields that exist in properties
|
||||||
if (result.type === "object" && result.properties && Array.isArray(result.required)) {
|
if (result.type === "object" && result.properties && Array.isArray(result.required)) {
|
||||||
result.required = result.required.filter((field: any) => field in result.properties)
|
result.required = result.required.filter((field: any) => field in result.properties)
|
||||||
|
|
|
||||||
|
|
@ -857,6 +857,93 @@ describe("ProviderTransform.schema - gemini nested array items", () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("ProviderTransform.schema - gemini type arrays", () => {
|
||||||
|
// Mirrors @ai-sdk/google's convertJSONSchemaToOpenAPISchema: JSON Schema type
|
||||||
|
// arrays (e.g. `["number","string"]`, common in MCP tool schemas) become an
|
||||||
|
// `anyOf` of single-type schemas, with `null` lifted into `nullable`. Plain
|
||||||
|
// @ai-sdk/google rewrites these, but OpenAI-compatible transports such as
|
||||||
|
// GitHub Copilot (proxying to Gemini) forward them verbatim and the backend
|
||||||
|
// rejects the array form.
|
||||||
|
const geminiModel = {
|
||||||
|
providerID: "google",
|
||||||
|
api: {
|
||||||
|
id: "gemini-3-pro",
|
||||||
|
},
|
||||||
|
} as any
|
||||||
|
|
||||||
|
test("splits a multi-type array into anyOf and drops the type array", () => {
|
||||||
|
const schema = {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
status: { type: ["number", "string"], description: "status filter" },
|
||||||
|
},
|
||||||
|
} as any
|
||||||
|
|
||||||
|
const result = ProviderTransform.schema(geminiModel, schema) as any
|
||||||
|
|
||||||
|
expect(result.properties.status.type).toBeUndefined()
|
||||||
|
expect(result.properties.status.anyOf).toEqual([{ type: "number" }, { type: "string" }])
|
||||||
|
expect(result.properties.status.nullable).toBeUndefined()
|
||||||
|
// Sibling keywords stay alongside the generated anyOf.
|
||||||
|
expect(result.properties.status.description).toBe("status filter")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("lifts null into nullable for a nullable type array", () => {
|
||||||
|
const schema = {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
maybe: { type: ["string", "null"], description: "nullable string" },
|
||||||
|
},
|
||||||
|
} as any
|
||||||
|
|
||||||
|
const result = ProviderTransform.schema(geminiModel, schema) as any
|
||||||
|
|
||||||
|
expect(result.properties.maybe.type).toBeUndefined()
|
||||||
|
expect(result.properties.maybe.anyOf).toEqual([{ type: "string" }])
|
||||||
|
expect(result.properties.maybe.nullable).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("collapses an all-null type array to type null", () => {
|
||||||
|
const schema = {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
nothing: { type: ["null"] },
|
||||||
|
},
|
||||||
|
} as any
|
||||||
|
|
||||||
|
const result = ProviderTransform.schema(geminiModel, schema) as any
|
||||||
|
|
||||||
|
expect(result.properties.nothing.type).toBe("null")
|
||||||
|
expect(result.properties.nothing.anyOf).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("rewrites type arrays for gemini served through github-copilot", () => {
|
||||||
|
const copilotGeminiModel = {
|
||||||
|
providerID: "github-copilot",
|
||||||
|
api: {
|
||||||
|
id: "gemini-3.5-flash",
|
||||||
|
npm: "@ai-sdk/github-copilot",
|
||||||
|
},
|
||||||
|
} as any
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
hook_id: { type: "number", description: "ID of the webhook" },
|
||||||
|
status: { type: ["number", "string"], description: "Filter by response status code" },
|
||||||
|
},
|
||||||
|
required: ["hook_id"],
|
||||||
|
additionalProperties: false,
|
||||||
|
} as any
|
||||||
|
|
||||||
|
const result = ProviderTransform.schema(copilotGeminiModel, schema) as any
|
||||||
|
|
||||||
|
expect(result.properties.status.anyOf).toEqual([{ type: "number" }, { type: "string" }])
|
||||||
|
expect(result.properties.status.type).toBeUndefined()
|
||||||
|
expect(result.properties.hook_id.type).toBe("number")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("ProviderTransform.schema - gemini combiner nodes", () => {
|
describe("ProviderTransform.schema - gemini combiner nodes", () => {
|
||||||
const geminiModel = {
|
const geminiModel = {
|
||||||
providerID: "google",
|
providerID: "google",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue