fix(model-capabilities): handle object-shaped modalities in readModalityKeys
This commit is contained in:
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user