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
39 changes: 39 additions & 0 deletions packages/opencode/src/provider/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { JSONSchema7 } from "@ai-sdk/provider"
import type * as Provider from "./provider"
import type * as ModelsDev from "@opencode-ai/core/models-dev"
import { iife } from "@/util/iife"
import { isRecord } from "@/util/record"

type Modality = NonNullable<ModelsDev.Model["modalities"]>["input"][number]

Expand Down Expand Up @@ -1286,6 +1287,39 @@ export function maxOutputTokens(model: Provider.Model, outputTokenMax = OUTPUT_T
return Math.min(model.limit.output, outputTokenMax) || outputTokenMax
}

function expandRefs(schema: JSONSchema7): JSONSchema7 {
const definitions = schema.$defs ?? (schema as any).definitions

function expand(value: unknown, seen = new Set<string>()): unknown {
if (Array.isArray(value)) return value.map((v) => expand(v, seen))
if (!isRecord(value)) return value

// Resolve local $ref
if (typeof value.$ref === "string" && definitions) {
const name =
value.$ref.match(/^#\/\$defs\/(.+)$/)?.[1] ??
value.$ref.match(/^#\/definitions\/(.+)$/)?.[1]
if (name && !seen.has(name)) {
const target = (definitions as Record<string, unknown>)[name]
if (target && isRecord(target)) {
const { $ref: _, ...targetRest } = value
seen.add(name)
return expand({ ...target, ...targetRest }, seen)
}
}
}

const result: Record<string, unknown> = {}
for (const [key, val] of Object.entries(value)) {
if (key === "$defs" || key === "definitions") continue
result[key] = expand(val, seen)
}
return result
}

return expand(schema) as JSONSchema7
}

type JsonRecord = Record<string, unknown>

function isPlainObject(value: unknown): value is JsonRecord {
Expand Down Expand Up @@ -1418,6 +1452,11 @@ export function schema(model: Provider.Model, schema: JSONSchema7): JSONSchema7
}
}

// Expand $ref/$defs for DeepSeek (rejects JSON Schema 2020-12 $ref pointers)
if (model.api.id.toLowerCase().includes("deepseek")) {
schema = expandRefs(schema)
}

// Convert integer enums to string enums for Google/Gemini
if (model.providerID === "google" || model.api.id.includes("gemini")) {
const isPlainObject = (node: unknown): node is Record<string, any> =>
Expand Down
106 changes: 106 additions & 0 deletions packages/opencode/test/provider/transform.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4406,3 +4406,109 @@ describe("ProviderTransform.providerOptions - ai-gateway-provider", () => {
expect(result).toEqual({ openaiCompatible: { reasoningEffort: "high" } })
})
})

describe("ProviderTransform.schema - deepseek $ref expansion", () => {
const deepseekModel = {
providerID: "deepseek",
api: { id: "deepseek-chat" },
} as any

test("expands simple $ref with $defs", () => {
const schema = {
type: "object",
properties: { query: { $ref: "#/$defs/Query" } },
$defs: { Query: { type: "string", description: "Search query" } },
} as any
const result = ProviderTransform.schema(deepseekModel, schema) as any
expect(result.properties?.query).toEqual({ type: "string", description: "Search query" })
expect(result.$defs).toBeUndefined()
})

test("expands nested $ref references", () => {
const schema = {
type: "object",
properties: { user: { $ref: "#/$defs/User" } },
$defs: {
User: {
type: "object",
properties: { name: { type: "string" }, address: { $ref: "#/$defs/Address" } },
},
Address: { type: "string" },
},
} as any
const result = ProviderTransform.schema(deepseekModel, schema) as any
expect(result.properties?.user).toEqual({
type: "object",
properties: { name: { type: "string" }, address: { type: "string" } },
})
})

test("handles circular $ref by returning empty object", () => {
const schema = {
type: "object",
properties: { node: { $ref: "#/$defs/Node" } },
$defs: { Node: { type: "object", properties: { self: { $ref: "#/$defs/Node" } } } },
} as any
const result = ProviderTransform.schema(deepseekModel, schema) as any
// Circular ref: second visit returns empty (seen set prevents infinite loop)
expect(result.properties?.node?.properties?.self).toBeDefined()
})

test("preserves description when expanding $ref", () => {
const schema = {
type: "object",
properties: {
query: {
$ref: "#/$defs/Query",
description: "Override description",
},
},
$defs: { Query: { type: "string", description: "Original description" } },
} as any
const result = ProviderTransform.schema(deepseekModel, schema) as any
// Override description should be preserved over definition description
expect(result.properties?.query).toEqual({
type: "string",
description: "Override description",
})
})

test("expands definitions (legacy format)", () => {
const schema = {
type: "object",
properties: { query: { $ref: "#/definitions/Query" } },
definitions: { Query: { type: "string" } },
} as any
const result = ProviderTransform.schema(deepseekModel, schema) as any
expect(result.properties?.query).toEqual({ type: "string" })
expect(result.definitions).toBeUndefined()
})

test("does not expand $ref for non-DeepSeek providers", () => {
const openaiModel = { providerID: "openai", api: { id: "gpt-4o" } } as any
const schema = {
type: "object",
properties: { query: { $ref: "#/$defs/Query" } },
$defs: { Query: { type: "string" } },
} as any
const result = ProviderTransform.schema(openaiModel, schema) as any
// Should be unchanged — $ref preserved
expect(result.properties?.query?.$ref).toBe("#/$defs/Query")
expect(result.$defs).toBeDefined()
})

test("expands $ref in array items", () => {
const schema = {
type: "object",
properties: {
tags: {
type: "array",
items: { $ref: "#/$defs/Tag" },
},
},
$defs: { Tag: { type: "string" } },
} as any
const result = ProviderTransform.schema(deepseekModel, schema) as any
expect(result.properties?.tags?.items).toEqual({ type: "string" })
})
})
Loading