diff --git a/packages/core/src/catalog.ts b/packages/core/src/catalog.ts index 5f90b09f308c..63d0ae37cbf7 100644 --- a/packages/core/src/catalog.ts +++ b/packages/core/src/catalog.ts @@ -235,6 +235,11 @@ export const layer = Layer.effect( if (!record) return const provider = record.provider + // TODO: Remove these provider-specific assumptions once model syncing reliably reports available deployments. + if (providerID === ProviderV2.ID.azure || providerID === ProviderV2.ID.make("azure-cognitive-services")) { + return + } + if (providerID === ProviderV2.ID.opencode) { const gpt5Nano = record.models.get(ModelV2.ID.make("gpt-5-nano")) if (gpt5Nano?.enabled && gpt5Nano.status === "active") return projectModel(gpt5Nano, provider) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 088aa2fd7664..a461b5cd1a19 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -1868,44 +1868,43 @@ export const layer = Layer.effect( } } - const defaultPriority = [ - "claude-haiku-4-5", - "claude-haiku-4.5", - "3-5-haiku", - "3.5-haiku", - "gemini-3-flash", - "gemini-2.5-flash", - "gpt-5-nano", - ] + // TODO: Remove these provider-specific assumptions once model syncing reliably reports available deployments. + if (providerID === ProviderV2.ID.azure || providerID === ProviderV2.ID.make("azure-cognitive-services")) { + return undefined + } + const priority = providerID.startsWith("opencode") - ? ["gpt-5-nano"] + ? ["gpt-nano"] : providerID.startsWith("github-copilot") - ? ["gpt-5-mini", "claude-haiku-4.5", ...defaultPriority] - : defaultPriority - for (const item of priority) { + ? ["gpt-mini", ...smallModelFamilyPriority] + : smallModelFamilyPriority + const models = sortBy( + Object.values(provider.models), + [(model) => model.release_date, "desc"], + [(model) => model.id, "desc"], + ) + for (const family of priority) { + const candidates = models.filter((model) => model.family === family) if (providerID === ProviderV2.ID.amazonBedrock) { const crossRegionPrefixes = ["global.", "us.", "eu."] - const candidates = Object.keys(provider.models).filter((m) => m.includes(item)) - const globalMatch = candidates.find((m) => m.startsWith("global.")) - if (globalMatch) return provider.models[globalMatch] + const globalMatch = candidates.find((model) => model.id.startsWith("global.")) + if (globalMatch) return globalMatch const region = provider.options?.region if (region) { const regionPrefix = region.split("-")[0] if (regionPrefix === "us" || regionPrefix === "eu") { - const regionalMatch = candidates.find((m) => m.startsWith(`${regionPrefix}.`)) - if (regionalMatch) return provider.models[regionalMatch] + const regionalMatch = candidates.find((model) => model.id.startsWith(`${regionPrefix}.`)) + if (regionalMatch) return regionalMatch } } - const unprefixed = candidates.find((m) => !crossRegionPrefixes.some((p) => m.startsWith(p))) - if (unprefixed) return provider.models[unprefixed] - } else { - for (const model of Object.keys(provider.models)) { - if (model.includes(item)) return provider.models[model] - } + const unprefixed = candidates.find((model) => !crossRegionPrefixes.some((p) => model.id.startsWith(p))) + if (unprefixed) return unprefixed + continue } + if (candidates[0]) return candidates[0] } return undefined @@ -1962,6 +1961,7 @@ export const defaultLayer = Layer.suspend(() => ) const priority = ["gpt-5", "claude-sonnet-4", "big-pickle", "gemini-3-pro"] +const smallModelFamilyPriority = ["gemini-flash", "gpt-nano", "claude-haiku"] export function sort(models: T[]) { return sortBy( models, diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index 05d22aec4142..d3f29204be84 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -652,6 +652,102 @@ it.instance("getSmallModel returns appropriate small model", () => }), ) +it.instance("getSmallModel prefers Gemini for Google Vertex", () => + Effect.gen(function* () { + yield* set("GOOGLE_VERTEX_PROJECT", "test-project") + const model = yield* Provider.use.getSmallModel(ProviderV2.ID.googleVertex) + expect(model).toBeDefined() + expect(model?.id).toContain("gemini") + }), +) + +it.instance( + "getSmallModel selects the latest model in the preferred family", + Effect.gen(function* () { + const model = yield* Provider.use.getSmallModel(ProviderV2.ID.make("test-provider")) + expect(model?.id).toBe(ModelV2.ID.make("new-flash")) + }), + { + config: { + provider: { + "test-provider": { + name: "Test Provider", + npm: "@ai-sdk/openai-compatible", + models: { + "old-flash": { family: "gemini-flash", release_date: "2025-01-01" }, + "new-flash": { family: "gemini-flash", release_date: "2026-01-01" }, + "newer-haiku": { family: "claude-haiku", release_date: "2026-06-01" }, + }, + options: { apiKey: "test-key" }, + }, + }, + }, + }, +) + +it.instance( + "getSmallModel matches exact model families", + Effect.gen(function* () { + const model = yield* Provider.use.getSmallModel(ProviderV2.ID.make("test-provider")) + expect(model?.id).toBe(ModelV2.ID.make("claude-haiku")) + }), + { + config: { + provider: { + "test-provider": { + name: "Test Provider", + npm: "@ai-sdk/openai-compatible", + models: { + "glm-flash": { family: "glm-flash", release_date: "2026-06-01" }, + "claude-haiku": { family: "claude-haiku", release_date: "2026-01-01" }, + }, + options: { apiKey: "test-key" }, + }, + }, + }, + }, +) + +it.instance( + "getSmallModel ignores model IDs without family metadata", + Effect.gen(function* () { + const model = yield* Provider.use.getSmallModel(ProviderV2.ID.make("test-provider")) + expect(model).toBeUndefined() + }), + { + config: { + provider: { + "test-provider": { + name: "Test Provider", + npm: "@ai-sdk/openai-compatible", + models: { + "gpt-5-nano": { release_date: "2026-01-01" }, + }, + options: { apiKey: "test-key" }, + }, + }, + }, + }, +) + +it.instance("getSmallModel skips inferred models for Azure", () => + Effect.gen(function* () { + yield* set("AZURE_RESOURCE_NAME", "test-resource") + yield* set("AZURE_API_KEY", "test-key") + const model = yield* Provider.use.getSmallModel(ProviderV2.ID.azure) + expect(model).toBeUndefined() + }), +) + +it.instance("getSmallModel skips inferred models for Azure Cognitive Services", () => + Effect.gen(function* () { + yield* set("AZURE_COGNITIVE_SERVICES_RESOURCE_NAME", "test-resource") + yield* set("AZURE_COGNITIVE_SERVICES_API_KEY", "test-key") + const model = yield* Provider.use.getSmallModel(ProviderV2.ID.make("azure-cognitive-services")) + expect(model).toBeUndefined() + }), +) + it.instance( "getSmallModel respects config small_model override", Effect.gen(function* () {