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
12 changes: 12 additions & 0 deletions packages/opencode/src/provider/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1521,6 +1521,18 @@ export function schema(model: Provider.Model, schema: JSONSchema7): JSONSchema7
// Codex also applies lossy compaction above 4 KB; defer that until OpenCode needs the same schema budget.
}

if (model.api.npm === "@ai-sdk/anthropic" || model.api.npm === "@ai-sdk/google-vertex/anthropic") {
// Anthropic's input_schema does not support oneOf, allOf, or anyOf at the top
// level (nested composition inside properties is fine). MCP servers can still
// emit these, e.g. `anyOf: [{ required: ["route"] }, { required: ["file"] }]`.
if (isPlainObject(schema)) {
const dropped = ["oneOf", "allOf", "anyOf"]
schema = Object.fromEntries(
Object.entries(schema as JsonRecord).filter(([key]) => !dropped.includes(key)),
) as JSONSchema7
}
}

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
88 changes: 88 additions & 0 deletions packages/opencode/test/provider/transform.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1629,6 +1629,94 @@ describe("ProviderTransform.schema - openai supported schema subset", () => {
})
})

describe("ProviderTransform.schema - anthropic top-level composition removal", () => {
const anthropicModel = {
providerID: "anthropic",
api: {
id: "claude-sonnet-4",
npm: "@ai-sdk/anthropic",
},
} as any

const vertexAnthropicModel = {
providerID: "google-vertex",
api: {
id: "claude-sonnet-4",
npm: "@ai-sdk/google-vertex/anthropic",
},
} as any

test("drops anyOf at the schema root", () => {
const result = ProviderTransform.schema(anthropicModel, {
type: "object",
properties: {
route: { type: "string" },
file: { type: "string" },
},
anyOf: [{ required: ["route"] }, { required: ["file"] }],
} as any) as any

expect(result).toEqual({
type: "object",
properties: {
route: { type: "string" },
file: { type: "string" },
},
})
})

test("drops oneOf and allOf at the schema root", () => {
const result = ProviderTransform.schema(anthropicModel, {
type: "object",
properties: { query: { type: "string" } },
oneOf: [{ required: ["query"] }],
allOf: [{ required: ["query"] }],
} as any) as any

expect(result).toEqual({
type: "object",
properties: { query: { type: "string" } },
})
})

test("keeps nested anyOf inside properties", () => {
const result = ProviderTransform.schema(anthropicModel, {
type: "object",
properties: {
value: {
anyOf: [{ type: "string" }, { type: "number" }],
},
},
} as any) as any

expect(result.properties.value.anyOf).toEqual([{ type: "string" }, { type: "number" }])
})

test("sanitizes Claude models served through Vertex AI", () => {
const result = ProviderTransform.schema(vertexAnthropicModel, {
type: "object",
anyOf: [{ required: ["route"] }, { required: ["file"] }],
} as any) as any

expect(result).toEqual({ type: "object" })
})

test("does not affect providers without anthropic-specific sanitization", () => {
const result = ProviderTransform.schema(
{
providerID: "mistral",
api: { id: "mistral-large", npm: "@ai-sdk/mistral" },
} as any,
{
type: "object",
anyOf: [{ required: ["route"] }, { required: ["file"] }],
} as any,
) as any

expect(result.anyOf).toEqual([{ required: ["route"] }, { required: ["file"] }])
})
})

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