mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-29 17:09:40 +08:00
fix(ai): add types to untyped enums in Moonshot tool schemas (#50961)
This commit is contained in:
@@ -9,12 +9,24 @@ const tupleItemsSchema = (items: ReadonlyArray<unknown>) => {
|
||||
return { anyOf: projected }
|
||||
}
|
||||
|
||||
// Moonshot rejects an `enum` without a sibling `type`. Its `type` may be one type, or one type plus "null".
|
||||
const enumType = (values: ReadonlyArray<unknown>) => {
|
||||
const types = [
|
||||
...new Set(values.map((value) => (value === null ? "null" : Array.isArray(value) ? "array" : typeof value))),
|
||||
]
|
||||
if (types.length === 1) return types[0]
|
||||
if (types.length === 2 && types.includes("null")) return [...types.filter((type) => type !== "null"), "null"]
|
||||
return undefined
|
||||
}
|
||||
|
||||
const moonshotNode = (schema: unknown): unknown => {
|
||||
if (Array.isArray(schema)) return schema.map(moonshotNode)
|
||||
if (!isRecord(schema)) return schema
|
||||
if (typeof schema.$ref === "string") return { $ref: schema.$ref }
|
||||
return Object.fromEntries(
|
||||
Object.entries(schema).flatMap(([key, value]) => {
|
||||
const type = schema.type === undefined && Array.isArray(schema.enum) ? enumType(schema.enum) : undefined
|
||||
return Object.fromEntries([
|
||||
...(type === undefined ? [] : [["type", type]]),
|
||||
...Object.entries(schema).flatMap(([key, value]) => {
|
||||
if (key === "items" && Array.isArray(value)) return [[key, tupleItemsSchema(value)]]
|
||||
if (key === "prefixItems") {
|
||||
if ("items" in schema) return []
|
||||
@@ -23,7 +35,7 @@ const moonshotNode = (schema: unknown): unknown => {
|
||||
if (key === "unevaluatedItems") return []
|
||||
return [[key, moonshotNode(value)]]
|
||||
}),
|
||||
)
|
||||
])
|
||||
}
|
||||
|
||||
const moonshot = (schema: JsonSchema): JsonSchema => {
|
||||
|
||||
@@ -28,6 +28,38 @@ describe("tool schema projections", () => {
|
||||
})
|
||||
})
|
||||
|
||||
test("moonshot derives a type for untyped enums", () => {
|
||||
expect(
|
||||
ToolSchemaProjection.moonshot({
|
||||
type: "object",
|
||||
properties: {
|
||||
kind: { description: "The kind of flag", enum: ["boolean", "string"] },
|
||||
level: { enum: [1, 2.5] },
|
||||
optional: { enum: [null, "a"] },
|
||||
choice: { anyOf: [{ enum: [true, false] }, { type: "null" }] },
|
||||
list: { type: "array", items: { enum: ["x"] } },
|
||||
map: { type: "object", additionalProperties: { enum: ["y"] } },
|
||||
typed: { type: "string", enum: ["a", null] },
|
||||
mixed: { enum: ["a", 1] },
|
||||
},
|
||||
$defs: { Mode: { enum: ["fast"] } },
|
||||
}),
|
||||
).toEqual({
|
||||
type: "object",
|
||||
properties: {
|
||||
kind: { type: "string", description: "The kind of flag", enum: ["boolean", "string"] },
|
||||
level: { type: "number", enum: [1, 2.5] },
|
||||
optional: { type: ["string", "null"], enum: [null, "a"] },
|
||||
choice: { anyOf: [{ type: "boolean", enum: [true, false] }, { type: "null" }] },
|
||||
list: { type: "array", items: { type: "string", enum: ["x"] } },
|
||||
map: { type: "object", additionalProperties: { type: "string", enum: ["y"] } },
|
||||
typed: { type: "string", enum: ["a", null] },
|
||||
mixed: { enum: ["a", 1] },
|
||||
},
|
||||
$defs: { Mode: { type: "string", enum: ["fast"] } },
|
||||
})
|
||||
})
|
||||
|
||||
test("gemini handles numeric enums, dangling required fields, untyped arrays, and scalar object keys", () => {
|
||||
expect(
|
||||
ToolSchemaProjection.gemini({
|
||||
|
||||
Reference in New Issue
Block a user