Skip to content
Merged
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
10 changes: 9 additions & 1 deletion src/shared/connected-providers-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,16 @@ interface ConnectedProvidersCache {
updatedAt: string
}

interface ModelMetadata {
id: string
provider?: string
context?: number
output?: number
name?: string
}

interface ProviderModelsCache {
models: Record<string, string[]>
models: Record<string, string[] | ModelMetadata[]>
connected: string[]
updatedAt: string
}
Expand Down
68 changes: 67 additions & 1 deletion src/shared/model-availability.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -619,7 +619,7 @@ describe("fetchAvailableModels with provider-models cache (whitelist-filtered)",
rmSync(tempDir, { recursive: true, force: true })
})

function writeProviderModelsCache(data: { models: Record<string, string[]>; connected: string[] }) {
function writeProviderModelsCache(data: { models: Record<string, string[] | any[]>; connected: string[] }) {
const cacheDir = join(tempDir, "oh-my-opencode")
require("fs").mkdirSync(cacheDir, { recursive: true })
writeFileSync(join(cacheDir, "provider-models.json"), JSON.stringify({
Expand Down Expand Up @@ -723,6 +723,72 @@ describe("fetchAvailableModels with provider-models cache (whitelist-filtered)",
expect(result.has("anthropic/claude-opus-4-5")).toBe(false)
expect(result.has("google/gemini-3-pro")).toBe(false)
})

it("should handle object[] format with metadata (Ollama-style)", async () => {
writeProviderModelsCache({
models: {
ollama: [
{ id: "ministral-3:14b-32k-agent", provider: "ollama", context: 32768, output: 8192 },
{ id: "qwen3-coder:32k-agent", provider: "ollama", context: 32768, output: 8192 }
]
},
connected: ["ollama"]
})

const result = await fetchAvailableModels(undefined, {
connectedProviders: ["ollama"]
})

expect(result.size).toBe(2)
expect(result.has("ollama/ministral-3:14b-32k-agent")).toBe(true)
expect(result.has("ollama/qwen3-coder:32k-agent")).toBe(true)
})

it("should handle mixed string[] and object[] formats across providers", async () => {
writeProviderModelsCache({
models: {
anthropic: ["claude-opus-4-5", "claude-sonnet-4-5"],
ollama: [
{ id: "ministral-3:14b-32k-agent", provider: "ollama" },
{ id: "qwen3-coder:32k-agent", provider: "ollama" }
]
},
connected: ["anthropic", "ollama"]
})

const result = await fetchAvailableModels(undefined, {
connectedProviders: ["anthropic", "ollama"]
})

expect(result.size).toBe(4)
expect(result.has("anthropic/claude-opus-4-5")).toBe(true)
expect(result.has("anthropic/claude-sonnet-4-5")).toBe(true)
expect(result.has("ollama/ministral-3:14b-32k-agent")).toBe(true)
expect(result.has("ollama/qwen3-coder:32k-agent")).toBe(true)
})

it("should skip invalid entries in object[] format", async () => {
writeProviderModelsCache({
models: {
ollama: [
{ id: "valid-model", provider: "ollama" },
{ provider: "ollama" },
{ id: "", provider: "ollama" },
null,
"string-model"
]
},
connected: ["ollama"]
})

const result = await fetchAvailableModels(undefined, {
connectedProviders: ["ollama"]
})

expect(result.size).toBe(2)
expect(result.has("ollama/valid-model")).toBe(true)
expect(result.has("ollama/string-model")).toBe(true)
})
})

describe("isModelAvailable", () => {
Expand Down
21 changes: 14 additions & 7 deletions src/shared/model-availability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,16 +187,23 @@ export async function fetchAvailableModels(
if (providerCount === 0) {
log("[fetchAvailableModels] provider-models cache empty, falling back to models.json")
} else {
log("[fetchAvailableModels] using provider-models cache (whitelist-filtered)")

for (const [providerId, modelIds] of Object.entries(providerModelsCache.models)) {
if (!connectedSet.has(providerId)) {
continue
}
for (const modelId of modelIds) {
log("[fetchAvailableModels] using provider-models cache (whitelist-filtered)")

for (const [providerId, modelIds] of Object.entries(providerModelsCache.models)) {
if (!connectedSet.has(providerId)) {
continue
}
for (const modelItem of modelIds) {
// Handle both string[] (legacy) and object[] (with metadata) formats
const modelId = typeof modelItem === 'string'
? modelItem
: (modelItem as any)?.id

if (modelId) {
modelSet.add(`${providerId}/${modelId}`)
}
}
}

log("[fetchAvailableModels] parsed from provider-models cache", {
count: modelSet.size,
Expand Down
Loading