mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-14 03:56:55 +03:00
fix(ai): preserve OpenAI tool schemas (#44546)
This commit is contained in:
committed by
GitHub
Unverified
parent
c6f4fa0d1f
commit
9f9184b002
@@ -362,7 +362,7 @@ export const lowerTool = Effect.fn("OpenResponses.lowerTool")(function* (
|
||||
type: "function" as const,
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
parameters: ToolSchemaProjection.responses(inputSchema),
|
||||
parameters: inputSchema,
|
||||
// The common tool definition does not currently express Responses strict-schema policy.
|
||||
strict: false,
|
||||
}
|
||||
|
||||
@@ -269,7 +269,7 @@ const lowerTool = (tool: ToolDefinition, inputSchema: JsonSchema, options: Lower
|
||||
function: {
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
parameters: ToolSchemaProjection.openAI(inputSchema),
|
||||
parameters: inputSchema,
|
||||
},
|
||||
cache_control: options.cacheControl?.(tool.cache),
|
||||
})
|
||||
|
||||
@@ -2,20 +2,6 @@ import type { JsonSchema, LanguageModelToolSchemaCompatibility } from "../../sch
|
||||
import { isRecord } from "../../utils/record.js"
|
||||
import { GeminiToolSchema } from "./gemini-tool-schema.js"
|
||||
|
||||
const removeNullSchemas = (value: unknown): unknown => {
|
||||
if (Array.isArray(value)) return value.map(removeNullSchemas)
|
||||
if (!isRecord(value)) return value
|
||||
const fields = Object.fromEntries(
|
||||
Object.entries(value)
|
||||
.filter(([key]) => key !== "anyOf")
|
||||
.map(([key, field]) => [key, removeNullSchemas(field)]),
|
||||
)
|
||||
if (!Array.isArray(value.anyOf)) return fields
|
||||
const variants = value.anyOf.filter((variant) => !isRecord(variant) || variant.type !== "null").map(removeNullSchemas)
|
||||
if (variants.length === 1 && isRecord(variants[0])) return { ...fields, ...variants[0] }
|
||||
return { ...fields, anyOf: variants }
|
||||
}
|
||||
|
||||
const tupleItemsSchema = (items: ReadonlyArray<unknown>) => {
|
||||
const projected = items.map(moonshotNode)
|
||||
if (projected.length === 0) return {}
|
||||
@@ -45,24 +31,7 @@ const moonshot = (schema: JsonSchema): JsonSchema => {
|
||||
return isRecord(projected) ? projected : {}
|
||||
}
|
||||
|
||||
const openAI = (schema: JsonSchema): JsonSchema => {
|
||||
const variants = Array.isArray(schema.anyOf) ? schema.anyOf.filter(isRecord) : []
|
||||
const flattened =
|
||||
variants.length === 0
|
||||
? { ...schema, type: "object" }
|
||||
: {
|
||||
...Object.fromEntries(Object.entries(schema).filter(([key]) => key !== "anyOf")),
|
||||
type: "object",
|
||||
properties: variants.reduce(
|
||||
(properties, variant) => ({ ...(isRecord(variant.properties) ? variant.properties : {}), ...properties }),
|
||||
{},
|
||||
),
|
||||
additionalProperties: false,
|
||||
}
|
||||
const normalized = removeNullSchemas(flattened)
|
||||
return isRecord(normalized) ? normalized : { type: "object" }
|
||||
}
|
||||
|
||||
const openAI = (schema: JsonSchema): JsonSchema => schema
|
||||
const responses = openAI
|
||||
|
||||
const gemini = (schema: JsonSchema): JsonSchema => GeminiToolSchema.convert(schema) ?? {}
|
||||
|
||||
@@ -196,7 +196,7 @@ describe("OpenAI Responses route", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("flattens top-level object unions in function schemas", () =>
|
||||
it.effect("preserves function schemas", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
LLMRequest.update(request, {
|
||||
@@ -236,13 +236,22 @@ describe("OpenAI Responses route", () => {
|
||||
strict: false,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
path: { type: "string" },
|
||||
reference: { type: "string" },
|
||||
limit: { type: "integer", maximum: 2000 },
|
||||
resource: { type: "string" },
|
||||
},
|
||||
additionalProperties: false,
|
||||
anyOf: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
path: { type: "string" },
|
||||
reference: { anyOf: [{ type: "string" }, { type: "null" }] },
|
||||
limit: { type: "integer", maximum: 2000 },
|
||||
},
|
||||
required: ["path"],
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: { resource: { type: "string" }, limit: { type: "integer", maximum: 51200 } },
|
||||
required: ["resource"],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
@@ -50,32 +50,7 @@ describe("tool schema projections", () => {
|
||||
})
|
||||
})
|
||||
|
||||
test("openai keeps one flat object top-level schema", () => {
|
||||
expect(
|
||||
ToolSchemaProjection.openAI({
|
||||
anyOf: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
path: { type: "string" },
|
||||
maybe: { anyOf: [{ type: "string" }, { type: "null" }] },
|
||||
},
|
||||
},
|
||||
{ type: "object", properties: { resource: { type: "string" } } },
|
||||
],
|
||||
}),
|
||||
).toEqual({
|
||||
type: "object",
|
||||
properties: {
|
||||
path: { type: "string" },
|
||||
maybe: { type: "string" },
|
||||
resource: { type: "string" },
|
||||
},
|
||||
additionalProperties: false,
|
||||
})
|
||||
})
|
||||
|
||||
it.effect("applies model compatibility before protocol projection", () =>
|
||||
it.effect("applies model compatibility without changing schema semantics", () =>
|
||||
Effect.gen(function* () {
|
||||
const model = OpenAIChat.route
|
||||
.with({ endpoint: { baseURL: "https://api.openai.test/v1/" }, auth: Auth.bearer("test") })
|
||||
@@ -107,11 +82,15 @@ describe("tool schema projections", () => {
|
||||
|
||||
expect(prepared.body.tools?.[0]?.function.parameters).toEqual({
|
||||
type: "object",
|
||||
properties: {
|
||||
tuple: { type: "array", items: { anyOf: [{ type: "string" }, { type: "number" }] } },
|
||||
linked: { $ref: "#/$defs/Linked" },
|
||||
},
|
||||
additionalProperties: false,
|
||||
anyOf: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
tuple: { type: "array", items: { anyOf: [{ type: "string" }, { type: "number" }] } },
|
||||
linked: { $ref: "#/$defs/Linked" },
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user