Skip to content

Commit c8692ce

Browse files
committed
fix(provider): sanitize xAI tool schemas for OpenRouter and native xai provider
Strip additionalProperties: false from tool/input JSON schemas when targeting xAI models via OpenRouter or the native xai provider. xAI rejects this keyword while their validator already defaults to false. Fixes anomalyco#23704
1 parent b5acc22 commit c8692ce

2 files changed

Lines changed: 174 additions & 0 deletions

File tree

‎packages/opencode/src/provider/transform.ts‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1110,5 +1110,24 @@ export function schema(model: Provider.Model, schema: JSONSchema.BaseSchema | JS
11101110
schema = sanitizeGemini(schema)
11111111
}
11121112

1113+
const isXaiTarget =
1114+
model.providerID === "xai"
1115+
|| (model.providerID === "openrouter" && model.api.id.startsWith("x-ai/"))
1116+
1117+
if (isXaiTarget) {
1118+
const sanitizeXai = (obj: unknown): unknown => {
1119+
if (obj === null || typeof obj !== "object") return obj
1120+
if (Array.isArray(obj)) return obj.map(sanitizeXai)
1121+
1122+
return Object.fromEntries(
1123+
Object.entries(obj)
1124+
.filter(([key, value]) => !(key === "additionalProperties" && value === false))
1125+
.map(([key, value]) => [key, sanitizeXai(value)]),
1126+
)
1127+
}
1128+
1129+
schema = sanitizeXai(schema) as JSONSchema7
1130+
}
1131+
11131132
return schema as JSONSchema7
11141133
}

‎packages/opencode/test/provider/transform.test.ts‎

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -855,6 +855,161 @@ describe("ProviderTransform.schema - gemini non-object properties removal", () =
855855
})
856856
})
857857

858+
describe("ProviderTransform.schema - xAI via OpenRouter", () => {
859+
const xaiModel = {
860+
providerID: "openrouter",
861+
api: {
862+
id: "x-ai/grok-4.1-fast",
863+
},
864+
} as any
865+
866+
test("removes additionalProperties false recursively", () => {
867+
const schema = {
868+
type: "object",
869+
properties: {
870+
command: { type: "string" },
871+
metadata: {
872+
type: "object",
873+
properties: {
874+
cwd: { type: "string" },
875+
},
876+
additionalProperties: false,
877+
},
878+
},
879+
additionalProperties: false,
880+
} as any
881+
882+
const result = ProviderTransform.schema(xaiModel, schema) as any
883+
884+
expect(result.additionalProperties).toBeUndefined()
885+
expect(result.properties.metadata.additionalProperties).toBeUndefined()
886+
})
887+
888+
test("preserves object and true additionalProperties forms", () => {
889+
const schema = {
890+
type: "object",
891+
properties: {
892+
answers: {
893+
type: "object",
894+
additionalProperties: { type: "string" },
895+
},
896+
passthrough: {
897+
type: "object",
898+
additionalProperties: true,
899+
},
900+
},
901+
} as any
902+
903+
const result = ProviderTransform.schema(xaiModel, schema) as any
904+
905+
expect(result.properties.answers.additionalProperties).toEqual({ type: "string" })
906+
expect(result.properties.passthrough.additionalProperties).toBe(true)
907+
})
908+
909+
test("does not affect non-xAI OpenRouter models", () => {
910+
const openrouterClaude = {
911+
providerID: "openrouter",
912+
api: {
913+
id: "anthropic/claude-sonnet-4.5",
914+
},
915+
} as any
916+
917+
const schema = {
918+
type: "object",
919+
properties: {
920+
command: { type: "string" },
921+
},
922+
additionalProperties: false,
923+
} as any
924+
925+
const result = ProviderTransform.schema(openrouterClaude, schema) as any
926+
927+
expect(result.additionalProperties).toBe(false)
928+
})
929+
})
930+
931+
describe("ProviderTransform.schema - xAI native provider", () => {
932+
const xaiNativeModel = {
933+
providerID: "xai",
934+
api: {
935+
id: "grok-4.1-fast",
936+
},
937+
} as any
938+
939+
test("removes additionalProperties false recursively", () => {
940+
const schema = {
941+
type: "object",
942+
properties: {
943+
command: { type: "string" },
944+
},
945+
additionalProperties: false,
946+
} as any
947+
948+
const result = ProviderTransform.schema(xaiNativeModel, schema) as any
949+
950+
expect(result.additionalProperties).toBeUndefined()
951+
})
952+
953+
test("preserves object and true additionalProperties forms", () => {
954+
const schema = {
955+
type: "object",
956+
properties: {
957+
data: {
958+
type: "object",
959+
additionalProperties: { type: "string" },
960+
},
961+
},
962+
additionalProperties: true,
963+
} as any
964+
965+
const result = ProviderTransform.schema(xaiNativeModel, schema) as any
966+
967+
expect(result.properties.data.additionalProperties).toEqual({ type: "string" })
968+
expect(result.additionalProperties).toBe(true)
969+
})
970+
})
971+
972+
describe("ProviderTransform.schema - xAI native provider", () => {
973+
const xaiNativeModel = {
974+
providerID: "xai",
975+
api: {
976+
id: "grok-4.1-fast",
977+
},
978+
} as any
979+
980+
test("removes additionalProperties false recursively", () => {
981+
const schema = {
982+
type: "object",
983+
properties: {
984+
command: { type: "string" },
985+
},
986+
additionalProperties: false,
987+
} as any
988+
989+
const result = ProviderTransform.schema(xaiNativeModel, schema) as any
990+
991+
expect(result.additionalProperties).toBeUndefined()
992+
})
993+
994+
test("preserves object and true additionalProperties forms", () => {
995+
const schema = {
996+
type: "object",
997+
properties: {
998+
data: {
999+
type: "object",
1000+
additionalProperties: { type: "string" },
1001+
},
1002+
},
1003+
additionalProperties: true,
1004+
} as any
1005+
1006+
const result = ProviderTransform.schema(xaiNativeModel, schema) as any
1007+
1008+
expect(result.properties.data.additionalProperties).toEqual({ type: "string" })
1009+
expect(result.additionalProperties).toBe(true)
1010+
})
1011+
})
1012+
8581013
describe("ProviderTransform.message - DeepSeek reasoning content", () => {
8591014
test("DeepSeek with tool calls includes reasoning_content in providerOptions", () => {
8601015
const msgs = [

0 commit comments

Comments
 (0)