fix(model-capabilities): handle object-shaped modalities in readModalityKeys

This commit is contained in:
Sami Jawhar
2026-04-05 20:55:23 +00:00
parent f540249838
commit 94e71936fb
3 changed files with 130 additions and 8 deletions
+6 -6
View File
@@ -51,12 +51,12 @@ export function loadOpencodePlugins(directory: string): string[] {
const result = parseJsoncSafe<OpencodeConfig>(content)
const plugins = result.data?.plugin ?? []
for (const rawPlugin of plugins) {
const plugin = typeof rawPlugin === "string" ? rawPlugin : Array.isArray(rawPlugin) ? rawPlugin[0] : null
if (typeof plugin !== "string") continue
if (seenPluginEntries.has(plugin)) continue
seenPluginEntries.add(plugin)
pluginEntries.push(plugin)
for (const plugin of plugins) {
const entry = typeof plugin === "string" ? plugin : Array.isArray(plugin) ? plugin[0] : null
if (typeof entry !== "string") continue
if (seenPluginEntries.has(entry)) continue
seenPluginEntries.add(entry)
pluginEntries.push(entry)
}
} catch {
continue
@@ -0,0 +1,111 @@
import { describe, expect, it } from "bun:test"
import { readRuntimeModelModalities } from "./runtime-model-readers"
describe("readRuntimeModelModalities", () => {
describe("object-shaped modalities (OpenCode schema)", () => {
it("#given modalities with input/output string arrays #when reading runtime model #then returns normalized modalities", () => {
const runtimeModel = {
id: "test-model",
modalities: {
input: ["text", "image", "pdf"],
output: ["text"],
},
}
const result = readRuntimeModelModalities(runtimeModel as Record<string, unknown>)
expect(result).toBeDefined()
expect(result?.input).toEqual(["text", "image", "pdf"])
expect(result?.output).toEqual(["text"])
})
it("#given modalities with mixed-case strings #when reading runtime model #then lowercases all entries", () => {
const runtimeModel = {
id: "test-model",
modalities: {
input: ["Text", "IMAGE", "Pdf"],
output: ["TEXT"],
},
}
const result = readRuntimeModelModalities(runtimeModel as Record<string, unknown>)
expect(result?.input).toEqual(["text", "image", "pdf"])
expect(result?.output).toEqual(["text"])
})
it("#given modalities nested in capabilities #when reading runtime model #then finds and normalizes them", () => {
const runtimeModel = {
id: "test-model",
capabilities: {
modalities: {
input: ["text", "audio"],
output: ["text"],
},
},
}
const result = readRuntimeModelModalities(runtimeModel as Record<string, unknown>)
expect(result).toBeDefined()
expect(result?.input).toEqual(["text", "audio"])
expect(result?.output).toEqual(["text"])
})
})
describe("flat string array modalities", () => {
it("#given modalities as boolean map at top level #when reading runtime model #then returns undefined (not supported at this level)", () => {
const runtimeModel = {
id: "test-model",
modalities: {
text: true,
image: true,
audio: false,
},
}
const result = readRuntimeModelModalities(runtimeModel as Record<string, unknown>)
// Boolean maps are only recognized inside input/output keys, not at the modalities root
expect(result).toBeUndefined()
})
})
describe("no crash on unexpected shapes", () => {
it("#given modalities is undefined #when reading runtime model #then returns undefined", () => {
const runtimeModel = { id: "test-model" }
const result = readRuntimeModelModalities(runtimeModel as Record<string, unknown>)
expect(result).toBeUndefined()
})
it("#given modalities contains nested arrays #when reading runtime model #then does not throw", () => {
const runtimeModel = {
id: "test-model",
modalities: {
input: [["text", "image"], ["pdf"]],
output: [["text"]],
},
}
expect(() => {
readRuntimeModelModalities(runtimeModel as Record<string, unknown>)
}).not.toThrow()
})
it("#given modalities values are non-string non-array #when reading runtime model #then does not throw", () => {
const runtimeModel = {
id: "test-model",
modalities: {
input: 42,
output: null,
},
}
expect(() => {
readRuntimeModelModalities(runtimeModel as Record<string, unknown>)
}).not.toThrow()
})
})
})
@@ -22,7 +22,7 @@ function readStringArray(value: unknown): string[] | undefined {
function normalizeVariantKeys(value: unknown): string[] | undefined {
const arrayVariants = readStringArray(value)
if (arrayVariants) {
return arrayVariants.map((variant) => variant.toLowerCase())
return arrayVariants.filter((v): v is string => typeof v === "string").map((variant) => variant.toLowerCase())
}
if (!isRecord(value)) {
@@ -36,13 +36,24 @@ function normalizeVariantKeys(value: unknown): string[] | undefined {
function readModalityKeys(value: unknown): string[] | undefined {
const stringArray = readStringArray(value)
if (stringArray) {
return stringArray.map((entry) => entry.toLowerCase())
return stringArray.filter((entry): entry is string => typeof entry === "string").map((entry) => entry.toLowerCase())
}
if (!isRecord(value)) {
return undefined
}
// Handle OpenCode's object-shaped modalities: { input: string[], output: string[] }
// When the full modalities object reaches here (e.g. via the normalizeModalities
// fallback path), flatten nested string arrays before applying toLowerCase.
const fromNested = Object.values(value)
.filter((v): v is string[] => Array.isArray(v))
.flat()
.filter((item): item is string => typeof item === "string")
if (fromNested.length > 0) {
return fromNested.map((entry) => entry.toLowerCase())
}
const enabled = Object.entries(value)
.filter(([, supported]) => supported === true)
.map(([modality]) => modality.toLowerCase())