Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
b9e6d76
Implement enable_models feature
KelvinJRosado Jan 22, 2026
d22b065
Update docs with new enable_models option.
KelvinJRosado Jan 22, 2026
e8e127a
Added disabled_models option
KelvinJRosado Jan 22, 2026
b6ccf91
Added wildcard support
KelvinJRosado Jan 22, 2026
d0c20df
Implement enable_models feature
KelvinJRosado Jan 22, 2026
54b0856
Update docs with new enable_models option.
KelvinJRosado Jan 22, 2026
8024860
Added disabled_models option
KelvinJRosado Jan 22, 2026
5eb865a
Added wildcard support
KelvinJRosado Jan 22, 2026
0740cab
Merge branch 'enable-disable-models' of github.com:KelvinJRosado/open…
KelvinJRosado Jan 22, 2026
4e8471b
Merge branch 'dev' of https://github.com/anomalyco/opencode into enab…
KelvinJRosado Jan 23, 2026
3c091e9
Merge branch 'dev' of https://github.com/anomalyco/opencode into enab…
KelvinJRosado Jan 24, 2026
8b318aa
Consistency fix
KelvinJRosado Jan 24, 2026
012d336
Merge branch 'dev' into enable-disable-models
KelvinJRosado Jan 26, 2026
129206d
Merge branch 'dev' into enable-disable-models
KelvinJRosado Jan 27, 2026
ab11d08
Merge branch 'dev' into enable-disable-models
KelvinJRosado Jan 27, 2026
f7f9552
Merge branch 'dev' into enable-disable-models
KelvinJRosado Jan 27, 2026
6b2fc54
Merge branch 'dev' into enable-disable-models
KelvinJRosado Jan 27, 2026
b730bb0
Merge branch 'dev' into enable-disable-models
KelvinJRosado Jan 27, 2026
f35be11
Merge branch 'dev' of https://github.com/anomalyco/opencode into enab…
KelvinJRosado Jan 27, 2026
0fa094a
Sync up with remote
KelvinJRosado Jan 27, 2026
ac973d4
Merge branch 'enable-disable-models' of github.com:KelvinJRosado/open…
KelvinJRosado Jan 27, 2026
c214c09
test(opencode): remove redundant model filter cases
KelvinJRosado Jan 27, 2026
5473f78
Merge branch 'dev' of https://github.com/anomalyco/opencode into enab…
KelvinJRosado Jan 27, 2026
c53ce65
Merge branch 'dev' into enable-disable-models
KelvinJRosado Jan 28, 2026
d4083bd
Merge branch 'dev' into enable-disable-models
KelvinJRosado Jan 28, 2026
6f0edf7
Merge branch 'dev' into enable-disable-models
KelvinJRosado Jan 28, 2026
9e62f23
Merge branch 'dev' into enable-disable-models
KelvinJRosado Jan 28, 2026
52f9019
Merge branch 'dev' into enable-disable-models
KelvinJRosado Jan 28, 2026
2b59366
Merge branch 'dev' into enable-disable-models
KelvinJRosado Jan 28, 2026
7295063
Merge branch 'dev' into enable-disable-models
KelvinJRosado Jan 29, 2026
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
8 changes: 8 additions & 0 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -922,6 +922,14 @@ export namespace Config {
.array(z.string())
.optional()
.describe("When set, ONLY these providers will be enabled. All other providers will be ignored"),
enabled_models: z
.record(z.string(), z.array(z.string()))
.optional()
.describe("Limit models per provider to the listed allowlist"),
disabled_models: z
.record(z.string(), z.array(z.string()))
.optional()
.describe("Disable models per provider using a blacklist (takes priority over enabled_models)"),
model: z.string().describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(),
small_model: z
.string()
Expand Down
12 changes: 12 additions & 0 deletions packages/opencode/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import z from "zod"
import fuzzysort from "fuzzysort"
import { Config } from "../config/config"
import { mapValues, mergeDeep, omit, pickBy, sortBy } from "remeda"
import { Wildcard } from "../util/wildcard"
import { NoSuchModelError, type Provider as SDK } from "ai"
import { Log } from "../util/log"
import { BunProc } from "../bun"
Expand Down Expand Up @@ -680,13 +681,20 @@ export namespace Provider {

const disabled = new Set(config.disabled_providers ?? [])
const enabled = config.enabled_providers ? new Set(config.enabled_providers) : null
const allowedModels = config.enabled_models ? new Map(Object.entries(config.enabled_models)) : null
const disabledModels = config.disabled_models ? new Map(Object.entries(config.disabled_models)) : null

function isProviderAllowed(providerID: string): boolean {
if (enabled && !enabled.has(providerID)) return false
if (disabled.has(providerID)) return false
return true
}

function matchesModelPattern(patterns: string[] | undefined, modelID: string, apiID: string) {
if (!patterns) return false
return patterns.some((pattern) => Wildcard.match(modelID, pattern) || Wildcard.match(apiID, pattern))
}

const providers: { [providerID: string]: Info } = {}
const languages = new Map<string, LanguageModelV2>()
const modelLoaders: {
Expand Down Expand Up @@ -910,13 +918,17 @@ export namespace Provider {
}

const configProvider = config.provider?.[providerID]
const allowed = allowedModels?.get(providerID)
const blocked = disabledModels?.get(providerID)

for (const [modelID, model] of Object.entries(provider.models)) {
model.api.id = model.api.id ?? model.id ?? modelID
if (modelID === "gpt-5-chat-latest" || (providerID === "openrouter" && modelID === "openai/gpt-5-chat"))
delete provider.models[modelID]
if (model.status === "alpha" && !Flag.OPENCODE_ENABLE_EXPERIMENTAL_MODELS) delete provider.models[modelID]
if (model.status === "deprecated") delete provider.models[modelID]
if (matchesModelPattern(blocked, modelID, model.api.id)) delete provider.models[modelID]
if (allowed && !matchesModelPattern(allowed, modelID, model.api.id)) delete provider.models[modelID]
if (
(configProvider?.blacklist && configProvider.blacklist.includes(modelID)) ||
(configProvider?.whitelist && !configProvider.whitelist.includes(modelID))
Expand Down
177 changes: 177 additions & 0 deletions packages/opencode/test/provider/provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,183 @@ test("model blacklist excludes specific models", async () => {
})
})

test("enabled_models restricts to only listed models", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
enabled_models: {
openai: ["gpt-5.2-codex"],
},
}),
)
},
})
await Instance.provide({
directory: tmp.path,
init: async () => {
Env.set("OPENAI_API_KEY", "test-api-key")
},
fn: async () => {
const providers = await Provider.list()
expect(providers["openai"]).toBeDefined()
const models = Object.keys(providers["openai"].models)
expect(models).toContain("gpt-5.2-codex")
expect(models.length).toBe(1)
},
})
})

test("enabled_models empty list removes provider models", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
enabled_models: {
openai: [],
},
}),
)
},
})
await Instance.provide({
directory: tmp.path,
init: async () => {
Env.set("OPENAI_API_KEY", "test-api-key")
},
fn: async () => {
const providers = await Provider.list()
expect(providers["openai"]).toBeUndefined()
},
})
})

test("disabled_models excludes specific models", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
disabled_models: {
anthropic: ["claude-sonnet-4-20250514"],
},
}),
)
},
})
await Instance.provide({
directory: tmp.path,
init: async () => {
Env.set("ANTHROPIC_API_KEY", "test-api-key")
},
fn: async () => {
const providers = await Provider.list()
expect(providers["anthropic"]).toBeDefined()
const models = Object.keys(providers["anthropic"].models)
expect(models).not.toContain("claude-sonnet-4-20250514")
},
})
})

test("enabled_models supports wildcard patterns", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
enabled_models: {
openai: ["gpt-5.*"],
},
}),
)
},
})
await Instance.provide({
directory: tmp.path,
init: async () => {
Env.set("OPENAI_API_KEY", "test-api-key")
},
fn: async () => {
const providers = await Provider.list()
expect(providers["openai"]).toBeDefined()
const models = Object.keys(providers["openai"].models)
expect(models).toContain("gpt-5.2-codex")
expect(models.every((model) => model.startsWith("gpt-5."))).toBe(true)
},
})
})

test("disabled_models overrides enabled_models", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
enabled_models: {
openai: ["gpt-5.2-codex"],
},
disabled_models: {
openai: ["gpt-5.2-codex"],
},
}),
)
},
})
await Instance.provide({
directory: tmp.path,
init: async () => {
Env.set("OPENAI_API_KEY", "test-api-key")
},
fn: async () => {
const providers = await Provider.list()
expect(providers["openai"]).toBeUndefined()
},
})
})

test("disabled_models matches api id with wildcard", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
disabled_models: {
anthropic: ["claude-sonnet-*"],
},
provider: {
anthropic: {
models: {
"my-alias": {
id: "claude-sonnet-4-20250514",
},
},
},
},
}),
)
},
})
await Instance.provide({
directory: tmp.path,
init: async () => {
Env.set("ANTHROPIC_API_KEY", "test-api-key")
},
fn: async () => {
const providers = await Provider.list()
expect(providers["anthropic"]).toBeDefined()
expect(providers["anthropic"].models["my-alias"]).toBeUndefined()
},
})
})

test("custom model alias via config", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
Expand Down
39 changes: 39 additions & 0 deletions packages/web/src/content/docs/config.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,45 @@ If a provider appears in both `enabled_providers` and `disabled_providers`, the

---

### Disabled models

Use `disabled_models` to remove specific models per provider. Patterns can use `*` and `?` wildcards and match against both the model ID and the API ID.

```json title="opencode.json"
{
"$schema": "https://opencode.ai/config.json",
"disabled_models": {
"anthropic": ["claude-sonnet-4-5", "claude-haiku-*"]
}
}
```

:::note
The `disabled_models` option takes priority over `enabled_models` when both are set.
:::

---

### Allowlist models

Use `enabled_models` to limit which models are available per provider. Patterns can use `*` and `?` wildcards and match against both the model ID and the API ID. An empty list removes the provider, effectively disabling that provider.

```json title="opencode.json"
{
"$schema": "https://opencode.ai/config.json",
"enabled_models": {
"anthropic": ["claude-opus-4-5", "claude-sonnet-?"],
"openai": []
}
}
```

:::note
The `disabled_models` option takes priority over `enabled_models`.
:::

---

### Experimental

The `experimental` key contains options that are under active development.
Expand Down