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 result = parseJsoncSafe<OpencodeConfig>(content)
|
||||||
const plugins = result.data?.plugin ?? []
|
const plugins = result.data?.plugin ?? []
|
||||||
|
|
||||||
for (const rawPlugin of plugins) {
|
for (const plugin of plugins) {
|
||||||
const plugin = typeof rawPlugin === "string" ? rawPlugin : Array.isArray(rawPlugin) ? rawPlugin[0] : null
|
const entry = typeof plugin === "string" ? plugin : Array.isArray(plugin) ? plugin[0] : null
|
||||||
if (typeof plugin !== "string") continue
|
if (typeof entry !== "string") continue
|
||||||
if (seenPluginEntries.has(plugin)) continue
|
if (seenPluginEntries.has(entry)) continue
|
||||||
seenPluginEntries.add(plugin)
|
seenPluginEntries.add(entry)
|
||||||
pluginEntries.push(plugin)
|
pluginEntries.push(entry)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
continue
|
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 {
|
function normalizeVariantKeys(value: unknown): string[] | undefined {
|
||||||
const arrayVariants = readStringArray(value)
|
const arrayVariants = readStringArray(value)
|
||||||
if (arrayVariants) {
|
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)) {
|
if (!isRecord(value)) {
|
||||||
@@ -36,13 +36,24 @@ function normalizeVariantKeys(value: unknown): string[] | undefined {
|
|||||||
function readModalityKeys(value: unknown): string[] | undefined {
|
function readModalityKeys(value: unknown): string[] | undefined {
|
||||||
const stringArray = readStringArray(value)
|
const stringArray = readStringArray(value)
|
||||||
if (stringArray) {
|
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)) {
|
if (!isRecord(value)) {
|
||||||
return undefined
|
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)
|
const enabled = Object.entries(value)
|
||||||
.filter(([, supported]) => supported === true)
|
.filter(([, supported]) => supported === true)
|
||||||
.map(([modality]) => modality.toLowerCase())
|
.map(([modality]) => modality.toLowerCase())
|
||||||
|
|||||||
Reference in New Issue
Block a user