Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions packages/opencode/src/provider/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1281,6 +1281,10 @@ export function schema(model: Provider.Model, schema: JSONSchema7): JSONSchema7
}
*/

if (model.api.npm === "@ai-sdk/openai-compatible") {
schema = inlineLocalRefs(schema)
}

if (model.providerID === "moonshotai" || model.api.id.toLowerCase().includes("kimi")) {
const sanitizeMoonshot = (obj: unknown): unknown => {
if (obj === null || typeof obj !== "object") return obj
Expand Down Expand Up @@ -1381,4 +1385,66 @@ export function schema(model: Provider.Model, schema: JSONSchema7): JSONSchema7
return schema
}

function inlineLocalRefs(schema: JSONSchema7): JSONSchema7 {
const visit = (node: unknown, stack: Set<string>): unknown => {
if (Array.isArray(node)) return node.map((item) => visit(item, stack))
if (!isSchemaObject(node)) return node

const ref = typeof node.$ref === "string" ? node.$ref : undefined
if (ref?.startsWith("#/")) {
if (stack.has(ref)) return localRefSiblings(node)

const target = resolveLocalRef(schema, ref)
if (target !== undefined) {
const nextStack = new Set(stack).add(ref)
const expanded = visit(target, nextStack)
const siblings = localRefSiblings(node)
const resolved = isSchemaObject(expanded) ? mergeDeep(expanded, siblings) : siblings
return visit(resolved, nextStack)
}
return localRefSiblings(node)
}

return Object.fromEntries(
Object.entries(node)
.filter(([key]) => key !== "$defs" && key !== "definitions")
.map(([key, value]) => [key, visit(value, stack)]),
)
}

const result = visit(schema, new Set())
if (isSchemaObject(result)) return result as JSONSchema7
return schema
}

function localRefSiblings(node: Record<string, unknown>) {
return Object.fromEntries(
Object.entries(node)
.filter(([key]) => key !== "$ref")
.map(([key, value]) => [key, cloneSchemaValue(value)]),
)
}

function cloneSchemaValue(node: unknown): unknown {
if (Array.isArray(node)) return node.map(cloneSchemaValue)
if (!isSchemaObject(node)) return node
return Object.fromEntries(Object.entries(node).map(([key, value]) => [key, cloneSchemaValue(value)]))
}

function resolveLocalRef(root: JSONSchema7, ref: string) {
return ref
.slice(2)
.split("/")
.map((part) => part.replace(/~1/g, "/").replace(/~0/g, "~"))
.reduce<unknown>((node, key) => {
if (isSchemaObject(node)) return node[key]
if (Array.isArray(node)) return node[Number(key)]
return undefined
}, root)
}

function isSchemaObject(node: unknown): node is Record<string, unknown> {
return node !== null && typeof node === "object" && !Array.isArray(node)
}

export * as ProviderTransform from "./transform"
84 changes: 84 additions & 0 deletions packages/opencode/test/provider/transform.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -925,6 +925,90 @@ describe("ProviderTransform.schema - gemini non-object properties removal", () =
})
})

describe("ProviderTransform.schema - openai-compatible local refs", () => {
const model = {
providerID: "fireworks",
api: {
id: "accounts/fireworks/models/deepseek-v4-flash",
npm: "@ai-sdk/openai-compatible",
},
} as any

function hasRef(node: unknown): boolean {
if (node === null || typeof node !== "object") return false
if (Array.isArray(node)) return node.some(hasRef)
return Object.entries(node).some(([key, value]) => key === "$ref" || hasRef(value))
}

test("inlines local refs in MCP tool schemas", () => {
const result = ProviderTransform.schema(model, {
type: "object",
properties: {
operations: {
type: "array",
items: {
anyOf: [
{
type: "object",
properties: {
asset_id: {
$ref: "#/$defs/AssetId",
description: "The Canva asset id to edit.",
},
},
required: ["asset_id"],
},
],
},
},
},
$defs: {
AssetId: {
type: "string",
minLength: 1,
},
},
} as any) as any

expect(result.properties.operations.items.anyOf[0].properties.asset_id).toEqual({
type: "string",
minLength: 1,
description: "The Canva asset id to edit.",
})
expect(result.$defs).toBeUndefined()
expect(hasRef(result)).toBe(false)
})

test("replaces recursive local refs with permissive schemas", () => {
const result = ProviderTransform.schema(model, {
type: "object",
properties: {
operation: {
$ref: "#/$defs/Operation",
},
},
$defs: {
Operation: {
type: "object",
properties: {
name: { type: "string" },
child: { $ref: "#/$defs/Operation" },
},
},
},
} as any) as any

expect(result.properties.operation).toEqual({
type: "object",
properties: {
name: { type: "string" },
child: {},
},
})
expect(hasRef(result)).toBe(false)
})
})

describe("ProviderTransform.schema - moonshot $ref siblings", () => {
const moonshotModel = {
providerID: "moonshotai",
Expand Down
Loading