|
|
@@ -1286,6 +1286,96 @@ export function maxOutputTokens(model: Provider.Model, outputTokenMax = OUTPUT_T
|
|
|
return Math.min(model.limit.output, outputTokenMax) || outputTokenMax
|
|
|
}
|
|
|
|
|
|
+type JsonRecord = Record<string, unknown>
|
|
|
+
|
|
|
+function isPlainObject(value: unknown): value is JsonRecord {
|
|
|
+ return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
|
+}
|
|
|
+
|
|
|
+// Mirrors Codex's Rust JSON schema compatibility lowering for OpenAI tool schemas.
|
|
|
+function sanitizeOpenAISchema(value: unknown): unknown {
|
|
|
+ const types = ["string", "number", "boolean", "integer", "object", "array", "null"]
|
|
|
+ const compositionKeys = ["anyOf", "oneOf", "allOf"]
|
|
|
+
|
|
|
+ // JSON Schema's boolean form (`true`/`false`) is unsupported by OpenAI tool schemas.
|
|
|
+ if (typeof value === "boolean") return { type: "string" }
|
|
|
+ if (Array.isArray(value)) return value.map(sanitizeOpenAISchema)
|
|
|
+ if (!isPlainObject(value)) return value
|
|
|
+
|
|
|
+ const result: JsonRecord = {}
|
|
|
+
|
|
|
+ if (typeof value.$ref === "string") result.$ref = value.$ref
|
|
|
+ if (typeof value.description === "string") result.description = value.description
|
|
|
+ if ("const" in value) result.enum = [value.const]
|
|
|
+ else if (Array.isArray(value.enum)) result.enum = value.enum
|
|
|
+
|
|
|
+ if (isPlainObject(value.properties)) {
|
|
|
+ result.properties = Object.fromEntries(
|
|
|
+ Object.entries(value.properties).map(([key, item]) => [key, sanitizeOpenAISchema(item)]),
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ if (Array.isArray(value.required)) {
|
|
|
+ result.required = value.required.filter((item) => typeof item === "string")
|
|
|
+ }
|
|
|
+
|
|
|
+ if ("items" in value) result.items = sanitizeOpenAISchema(value.items)
|
|
|
+
|
|
|
+ if ("additionalProperties" in value) {
|
|
|
+ result.additionalProperties =
|
|
|
+ typeof value.additionalProperties === "boolean"
|
|
|
+ ? value.additionalProperties
|
|
|
+ : sanitizeOpenAISchema(value.additionalProperties)
|
|
|
+ }
|
|
|
+
|
|
|
+ for (const key of compositionKeys) {
|
|
|
+ if (Array.isArray(value[key])) result[key] = value[key].map(sanitizeOpenAISchema)
|
|
|
+ }
|
|
|
+
|
|
|
+ for (const key of ["$defs", "definitions"]) {
|
|
|
+ if (isPlainObject(value[key])) {
|
|
|
+ result[key] = Object.fromEntries(
|
|
|
+ Object.entries(value[key]).map(([name, item]) => [name, sanitizeOpenAISchema(item)]),
|
|
|
+ )
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const schemaTypes =
|
|
|
+ typeof value.type === "string"
|
|
|
+ ? types.includes(value.type)
|
|
|
+ ? [value.type]
|
|
|
+ : []
|
|
|
+ : Array.isArray(value.type)
|
|
|
+ ? value.type.filter((item) => typeof item === "string" && types.includes(item))
|
|
|
+ : []
|
|
|
+
|
|
|
+ if (schemaTypes.length === 0 && (typeof result.$ref === "string" || compositionKeys.some((key) => key in result))) {
|
|
|
+ return result
|
|
|
+ }
|
|
|
+
|
|
|
+ // MCP schemas may omit `type` while still using keywords that imply one.
|
|
|
+ // Keep the schema usable after unsupported keywords are dropped.
|
|
|
+ const inferredTypes =
|
|
|
+ schemaTypes.length > 0
|
|
|
+ ? schemaTypes
|
|
|
+ : ["properties", "required", "additionalProperties"].some((key) => key in value)
|
|
|
+ ? ["object"]
|
|
|
+ : ["items", "prefixItems"].some((key) => key in value)
|
|
|
+ ? ["array"]
|
|
|
+ : "enum" in result || "format" in value
|
|
|
+ ? ["string"]
|
|
|
+ : ["minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum", "multipleOf"].some((key) => key in value)
|
|
|
+ ? ["number"]
|
|
|
+ : []
|
|
|
+
|
|
|
+ if (inferredTypes.length === 0) return {}
|
|
|
+
|
|
|
+ result.type = inferredTypes.length === 1 ? inferredTypes[0] : inferredTypes
|
|
|
+ if (inferredTypes.includes("object") && !("properties" in result)) result.properties = {}
|
|
|
+ if (inferredTypes.includes("array") && !("items" in result)) result.items = { type: "string" }
|
|
|
+ return result
|
|
|
+}
|
|
|
+
|
|
|
export function schema(model: Provider.Model, schema: JSONSchema7): JSONSchema7 {
|
|
|
/*
|
|
|
if (["openai", "azure"].includes(providerID)) {
|
|
|
@@ -1305,6 +1395,11 @@ export function schema(model: Provider.Model, schema: JSONSchema7): JSONSchema7
|
|
|
}
|
|
|
*/
|
|
|
|
|
|
+ if (model.api.npm === "@ai-sdk/openai" || model.api.npm === "@ai-sdk/azure") {
|
|
|
+ schema = sanitizeOpenAISchema(schema) as JSONSchema7
|
|
|
+ // Codex also applies lossy compaction above 4 KB; defer that until OpenCode needs the same schema budget.
|
|
|
+ }
|
|
|
+
|
|
|
if (model.providerID === "moonshotai" || model.api.id.toLowerCase().includes("kimi")) {
|
|
|
const sanitizeMoonshot = (obj: unknown): unknown => {
|
|
|
if (obj === null || typeof obj !== "object") return obj
|