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"]) +})