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
5 changes: 5 additions & 0 deletions packages/core/src/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
48 changes: 24 additions & 24 deletions packages/opencode/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<T extends { id: string }>(models: T[]) {
return sortBy(
models,
Expand Down
96 changes: 96 additions & 0 deletions packages/opencode/test/provider/provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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* () {
Expand Down
Loading