diff --git a/src/components/settings/acp-agent-settings.opencode-models.test.tsx b/src/components/settings/acp-agent-settings.opencode-models.test.tsx
index 868fb288ab1..114870599bd 100644
--- a/src/components/settings/acp-agent-settings.opencode-models.test.tsx
+++ b/src/components/settings/acp-agent-settings.opencode-models.test.tsx
@@ -188,5 +188,51 @@ describe(
expect(saved.provider.relay.models).toEqual(PICKED)
expect(saved.providers).toBeUndefined()
})
+
+ it.each(["models", "provider"])(
+ "keeps disabled %s out of both default model pickers before the catalog loads",
+ async (disabled) => {
+ const user = userEvent.setup()
+ vi.mocked(acpListAgents).mockResolvedValue([
+ openCodeAgent({
+ providers: {
+ relay: {
+ ...NATIVE_CONFIG.providers.relay,
+ models: {
+ "glm-5.2": { disabled: disabled === "models" },
+ old: { disabled: disabled === "models" },
+ },
+ },
+ },
+ ...(disabled === "provider"
+ ? {
+ policies: [
+ {
+ action: "provider.use",
+ resource: "relay",
+ effect: "deny",
+ },
+ ],
+ }
+ : {}),
+ }),
+ ])
+ render(
+
+
+
+ )
+ const inputs =
+ await screen.findAllByPlaceholderText("provider/model-id")
+ expect(inputs).toHaveLength(2)
+ for (const input of inputs) {
+ await user.click(input)
+ expect(
+ screen.queryAllByRole("option", { name: /relay\// })
+ ).toHaveLength(0)
+ await user.keyboard("{Escape}")
+ }
+ }
+ )
}
)
diff --git a/src/components/settings/acp-agent-settings.tsx b/src/components/settings/acp-agent-settings.tsx
index f0da2efbcad..02a1e0b6262 100644
--- a/src/components/settings/acp-agent-settings.tsx
+++ b/src/components/settings/acp-agent-settings.tsx
@@ -1770,26 +1770,6 @@ const OPENCODE_PROVIDER_NPM_OPTIONS = [
},
] as const
-function buildOpenCodeModelOptions(
- config: OpenCodeConfigView | null
-): OpenCodeModelOptionGroup[] {
- if (!config) return []
- const groups: OpenCodeModelOptionGroup[] = []
- for (const providerId of config.providerIds) {
- const provider = config.providers[providerId]
- if (!provider || provider.modelIds.length === 0) continue
- groups.push({
- providerId,
- label: provider.name || providerId,
- models: provider.modelIds.map((modelId) => ({
- value: `${providerId}/${modelId}`,
- label: modelId,
- })),
- })
- }
- return groups
-}
-
function OpenCodeModelCombobox({
value,
onValueChange,
@@ -6429,16 +6409,14 @@ export function AcpAgentSettings() {
openCodeIntegrations,
openCodeCatalog,
])
- const openCodeModelOptions = useMemo(() => {
- const catalogGroups = buildConnectedModelOptions({
- connected: openCodeConnected,
- catalog: openCodeCatalog,
- })
- // Fall back to the config-derived groups before the catalog has loaded.
- return catalogGroups.length > 0
- ? catalogGroups
- : buildOpenCodeModelOptions(selectedOpenCodeConfig)
- }, [openCodeConnected, openCodeCatalog, selectedOpenCodeConfig])
+ const openCodeModelOptions = useMemo(
+ () =>
+ buildConnectedModelOptions({
+ connected: openCodeConnected,
+ catalog: openCodeCatalog,
+ }),
+ [openCodeConnected, openCodeCatalog]
+ )
const openCodeCatalogIds = useMemo(
() => new Set(openCodeCatalog.map((p) => p.id)),
[openCodeCatalog]
diff --git a/src/lib/opencode-connect.test.ts b/src/lib/opencode-connect.test.ts
index 623d702b0c1..d2cd6fd77f0 100644
--- a/src/lib/opencode-connect.test.ts
+++ b/src/lib/opencode-connect.test.ts
@@ -543,6 +543,70 @@ describe("integrations and the panel view", () => {
})
expect(groups[1].models.map((m) => m.value)).toEqual(["relay/relay-1"])
})
+
+ it("filters disabled configured and catalog models by provider and config ID", () => {
+ const connected = buildConnectedProviders({
+ config: {
+ provider: {
+ anthropic: { models: { enabled: { disabled: true } } },
+ },
+ providers: {
+ anthropic: {
+ blacklist: ["enabled"],
+ models: {
+ "claude-x": { disabled: true },
+ "vendor/hidden": { disabled: true },
+ enabled: { disabled: false, modelID: "claude-x" },
+ implicit: {},
+ malformed: { disabled: "true" },
+ },
+ },
+ relay: { models: { "claude-x": {} } },
+ },
+ },
+ integrations: [],
+ catalog: CATALOG,
+ })
+ const groups = buildConnectedModelOptions({ connected, catalog: CATALOG })
+ expect(groups.flatMap((group) => group.models.map((m) => m.value))).toEqual(
+ [
+ "anthropic/enabled",
+ "anthropic/implicit",
+ "anthropic/malformed",
+ "relay/claude-x",
+ ]
+ )
+ })
+
+ it.each([{ catalog: [] }, { catalog: CATALOG }])(
+ "omits providers with only disabled models (catalog size: $catalog.length)",
+ ({ catalog }) => {
+ const connected = buildConnectedProviders({
+ config: {
+ providers: {
+ anthropic: { models: { "claude-x": { disabled: true } } },
+ },
+ },
+ integrations: [],
+ catalog,
+ })
+ expect(buildConnectedModelOptions({ connected, catalog })).toEqual([])
+ }
+ )
+
+ it("keeps models a 1.x provider block marks disabled, as OpenCode 2 does", () => {
+ const connected = buildConnectedProviders({
+ config: {
+ provider: { relay: { models: { a: {}, b: { disabled: true } } } },
+ },
+ integrations: [],
+ catalog: [],
+ })
+ const groups = buildConnectedModelOptions({ connected, catalog: [] })
+ expect(groups.flatMap((group) => group.models.map((m) => m.value))).toEqual(
+ ["relay/a", "relay/b"]
+ )
+ })
})
describe("custom provider ids and labels", () => {
diff --git a/src/lib/opencode-connect.ts b/src/lib/opencode-connect.ts
index 323f77b1386..d67236d8136 100644
--- a/src/lib/opencode-connect.ts
+++ b/src/lib/opencode-connect.ts
@@ -156,6 +156,7 @@ export type OpenCodeProviderShape = "native" | "legacy"
export interface OpenCodeProviderModelView {
id: string
name: string
+ disabled: boolean
/** Keys besides `name` (and 2.x's `modelID` alias), kept as they are. */
extraFieldCount: number
}
@@ -182,6 +183,7 @@ function modelViews(
views[id] = {
id,
name: str(model.name),
+ disabled: model.disabled === true,
extraFieldCount: Object.keys(model).filter(
(key) => key !== "name" && key !== "modelID" && key !== "id"
).length,
@@ -643,6 +645,7 @@ export interface OpenCodeConnectedProvider {
baseUrl: string
npm: string
modelIds: string[]
+ disabledModelIds: string[]
}
function connectionKindOf(integration: OpenCodeIntegration | undefined): {
@@ -699,6 +702,14 @@ export function buildConnectedProviders(params: {
baseUrl: block?.baseUrl ?? "",
npm: block?.npm ?? "",
modelIds: block?.modelIds ?? [],
+ // OpenCode 2 reads `disabled` on 2.x blocks only; a 1.x block's
+ // model-level `disabled` is dropped when it migrates the block.
+ disabledModelIds:
+ block?.shape === "native"
+ ? Object.values(block.models)
+ .filter((model) => model.disabled)
+ .map((model) => model.id)
+ : [],
})
}
result.sort(
@@ -741,6 +752,7 @@ export function buildConnectedModelOptions(params: {
const metaById = new Map(catalog?.models.map((m) => [m.id, m]) ?? [])
const ids = new Set(provider.modelIds)
if (catalog) for (const model of catalog.models) ids.add(model.id)
+ for (const modelId of provider.disabledModelIds) ids.delete(modelId)
if (ids.size === 0) continue
groups.push({
providerId: provider.id,
diff --git a/src/lib/session-settings.test.ts b/src/lib/session-settings.test.ts
index 086ae347128..c343ae14e90 100644
--- a/src/lib/session-settings.test.ts
+++ b/src/lib/session-settings.test.ts
@@ -354,3 +354,17 @@ describe("MiniMax Code ACP model settings", () => {
])
})
})
+
+it("shows a filtered current model without offering it again", () => {
+ const setting = choice(
+ build([
+ select("model", "Model", "model", "relay/disabled", ["relay/allowed"]),
+ ])[0]
+ )
+ expect(setting.currentLabel).toBe("relay/disabled")
+ expect(
+ setting.groups.flatMap((group) =>
+ group.options.map((option) => option.value)
+ )
+ ).toEqual(["relay/allowed"])
+})