diff --git a/AGENTS.md b/AGENTS.md index 98e5fa179e67..db253168b54e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,6 +36,20 @@ identity facts when merging or refactoring: single bookmark, so channel inference can be ambiguous or unavailable. The build fails closed in that case; the explicit channel pins the intended database and service identity. +- The upstream Claude Agent SDK provider stays on disk + (`packages/core/src/plugin/provider/claude-agent/**` and its tests, plus the + optional `@anthropic-ai/claude-agent-sdk` peer and `#claude-agent-sdk` + alias) so later syncs do not resurrect a deleted tree. It is not a Shuvcode + provider. The guarantee is three layers, not the bundled registry alone: + `ProviderPlugins` does not load it, `ConfigProviderPlugin` skips provider + ids `claude-agent` and `opencode.provider.claude-agent`, and every client + list of integrations, providers, or models calls + `isHiddenProvider` from `@opencode/util/hidden-provider`. Do not register + `ClaudeAgentPlugin` or `make(...)`, do not let config recreate the id, and + do not add a list surface that omits the predicate. Anthropic in the + product is `@shuvcode/claude-plugin` (`ClaudePlugin`). When merging + `upstream/dev`, drop any reintroduced registration, config path, or + user-facing surface for that provider. ## Live V2 TUI Testing diff --git a/PLAN-fresh-v2-rewrite.md b/PLAN-fresh-v2-rewrite.md index cee165a49fad..4dcdad430cc8 100644 --- a/PLAN-fresh-v2-rewrite.md +++ b/PLAN-fresh-v2-rewrite.md @@ -54,7 +54,11 @@ still `2.0.3`). Re-fetch again at kickoff and re-run the watchlist checks. 3. **Plugin rule:** if it can be a plugin, it is a plugin, **bundled and enabled** (Core `ProviderPlugins` / TUI `builtins`). Disable by config id (`-` in `plugins`). Do not require a user `plugins` array for the - shipped set. + shipped set. Exception: the upstream Claude Agent SDK provider + (`packages/core/src/plugin/provider/claude-agent/**`) stays on disk for + sync and stays unregistered. Config must not recreate it, and client + lists must keep filtering it. Do not re-enable it. Anthropic remains + `@shuvcode/claude-plugin`. See `AGENTS.md` Shuvcode Fork. 4. **Core may change only for:** identity, default port, XDG app id, one-shot DB convert script, OAuth refresh single-flight, host seams plugins cannot exist without, and watchlist items whose check **fails** at kickoff. diff --git a/packages/app/src/composer/selection.ts b/packages/app/src/composer/selection.ts index 2013ee5bbf72..db9f403d0d9c 100644 --- a/packages/app/src/composer/selection.ts +++ b/packages/app/src/composer/selection.ts @@ -3,6 +3,7 @@ import { createStore } from "solid-js/store" import type { ComposerControls } from "./adapter" import { useLocal, type ModelKey, type ModelSelection } from "@/providers/models/selection" import { useWorkspaceLocation } from "@/workspaces/location" +import { omitHiddenProviderCatalog } from "@/providers/catalog/order" import { useProviders } from "@/providers/catalog/providers" import { useData } from "@/runtime/server/current" import { normalizeAgentList, normalizeProviderList } from "@/runtime/server/global-sync/utils" @@ -55,7 +56,7 @@ export function createComposerControls(input: { model?: ModelSelection } = {}) { const catalog = data.location.model.list(location) if (!provider || !catalog) return - const normalized = normalizeProviderList(provider, catalog) + const normalized = omitHiddenProviderCatalog(normalizeProviderList(provider, catalog)) return { all: normalized.all, connected: normalized.connected } }) diff --git a/packages/app/src/providers/catalog/integrations.ts b/packages/app/src/providers/catalog/integrations.ts index f8bf0d7f1d65..df59d4217b15 100644 --- a/packages/app/src/providers/catalog/integrations.ts +++ b/packages/app/src/providers/catalog/integrations.ts @@ -1,6 +1,7 @@ import { useServerSDK } from "@/runtime/server/client" import { useData } from "@/runtime/server/current" import { createEffect, type Accessor } from "solid-js" +import { withoutHiddenProviders } from "./order" export function useIntegrations(directory: Accessor) { const serverSDK = useServerSDK() @@ -25,6 +26,6 @@ export function useIntegrations(directory: Accessor) { return { ready: () => data.location.integration.list(location()) !== undefined, - list: () => data.location.integration.list(location()) ?? [], + list: () => withoutHiddenProviders(data.location.integration.list(location()) ?? []), } } diff --git a/packages/app/src/providers/catalog/order.test.ts b/packages/app/src/providers/catalog/order.test.ts new file mode 100644 index 000000000000..b3fad851d747 --- /dev/null +++ b/packages/app/src/providers/catalog/order.test.ts @@ -0,0 +1,26 @@ +import { expect, test } from "bun:test" +import { connectPopular, omitHiddenProviderCatalog, popularProviders, withoutHiddenProviders } from "./order" + +test("keeps Anthropic visible and drops Claude Agent from provider menus", () => { + expect(popularProviders).toContain("anthropic") + expect(connectPopular).toContain("anthropic") + expect(popularProviders).not.toContain("claude-agent") + expect(connectPopular).not.toContain("claude-agent") + + expect(withoutHiddenProviders([{ id: "claude-agent" }, { id: "anthropic" }]).map((item) => item.id)).toEqual([ + "anthropic", + ]) + + const catalog = omitHiddenProviderCatalog({ + all: new Map([ + ["claude-agent", { name: "Claude Agent" }], + ["anthropic", { name: "Anthropic" }], + ]), + connected: ["claude-agent", "anthropic"], + default: { "claude-agent": "sonnet", anthropic: "claude-sonnet" }, + }) + + expect([...catalog.all.keys()]).toEqual(["anthropic"]) + expect(catalog.connected).toEqual(["anthropic"]) + expect(catalog.default).toEqual({ anthropic: "claude-sonnet" }) +}) diff --git a/packages/app/src/providers/catalog/order.ts b/packages/app/src/providers/catalog/order.ts index ca3b975f9863..3e83a4047852 100644 --- a/packages/app/src/providers/catalog/order.ts +++ b/packages/app/src/providers/catalog/order.ts @@ -1,11 +1,38 @@ +import { isHiddenProvider } from "@opencode/util/hidden-provider" + +export { isHiddenProvider } + export const popularProviders = [ "opencode-go", "opencode", "anthropic", - "claude-agent", "github-copilot", "openai", "google", "openrouter", "vercel", ] + +// Connect dialog group order. Matches the TUI priority list. +export const connectPopular = ["opencode", "opencode-go", "openai", "github-copilot", "anthropic", "google"] + +// App catalogs that call these helpers omit the upstream Claude Agent id. +// Other list surfaces call isHiddenProvider themselves. +export function withoutHiddenProviders(items: readonly T[]) { + return items.filter((item) => !isHiddenProvider(item.id)) +} + +export function omitHiddenProviderCatalog(catalog: { + all: Map + connected: readonly string[] + default: Record +}) { + const all = new Map([...catalog.all].filter(([id]) => !isHiddenProvider(id))) + const connected = catalog.connected.filter((id) => !isHiddenProvider(id)) + + return { + all, + connected, + default: Object.fromEntries(Object.entries(catalog.default).filter(([id]) => !isHiddenProvider(id))), + } +} diff --git a/packages/app/src/providers/catalog/providers.ts b/packages/app/src/providers/catalog/providers.ts index 45033d6ffaff..66745a7aa056 100644 --- a/packages/app/src/providers/catalog/providers.ts +++ b/packages/app/src/providers/catalog/providers.ts @@ -7,7 +7,7 @@ import { Iterable, pipe } from "effect" import { createEffect, createMemo, type Accessor } from "solid-js" import type { ProviderListResponse } from "@/runtime/server/types" import { useIntegrations } from "./integrations" -import { popularProviders } from "./order" +import { omitHiddenProviderCatalog, popularProviders } from "./order" export { popularProviders } from "./order" @@ -49,7 +49,7 @@ export function useProviders(directory: Accessor) { if (!provider || !model) return emptyProviderCatalog - return normalizeProviderList(provider, model) + return omitHiddenProviderCatalog(normalizeProviderList(provider, model)) }) const ready = () => { diff --git a/packages/app/src/providers/connect/controller.ts b/packages/app/src/providers/connect/controller.ts index 80764377f129..22e3c74ec381 100644 --- a/packages/app/src/providers/connect/controller.ts +++ b/packages/app/src/providers/connect/controller.ts @@ -32,8 +32,6 @@ type Polling = { // and connects with its own methods, the same as the TUI. export const CONSOLE_INTEGRATION = "opencode" -export const CLAUDE_AGENT_INTEGRATION = "claude-agent" - // Same order as the TUI: browser and command sign-ins first, API keys last. export function connectMethods(integration: IntegrationInfo) { return integration.methods diff --git a/packages/app/src/providers/connect/dialog.tsx b/packages/app/src/providers/connect/dialog.tsx index 6932a67e2284..78ecdf9d54d5 100644 --- a/packages/app/src/providers/connect/dialog.tsx +++ b/packages/app/src/providers/connect/dialog.tsx @@ -21,7 +21,6 @@ import { onMount, Show, Switch, - untrack, } from "solid-js" import { createStore } from "solid-js/store" import { useParams } from "@solidjs/router" @@ -32,12 +31,12 @@ import { useData } from "@/runtime/server/current" import { useGlobal } from "@/runtime/server/runtime" import { ServerConnection } from "@/runtime/server/registry" import { useProviders } from "@/providers/catalog/providers" +import { connectPopular, isHiddenProvider } from "@/providers/catalog/order" import { consoleProviderGroup, consoleProviderName } from "@/providers/catalog/console" import { useIntegrations } from "@/providers/catalog/integrations" import { ProviderModelGroup, ProviderModelIcon } from "@/providers/models/provider-group" import type { ModelSelection } from "@/providers/models/selection" import { decode64 } from "@/runtime/persistence/base64" -import { ExternalLink } from "@/runtime/platform/external-link" import { SettingsList } from "@/settings/list" import { useTabs } from "@/shell/tabs/tabs" import { Persist, persisted } from "@/runtime/persistence/storage" @@ -45,7 +44,6 @@ import { Persistence } from "@/runtime/persistence/schema" import { Schema } from "effect" import type { FormAnswer, FormField, FormValue, IntegrationInfo } from "@opencode/client/promise" import { - CLAUDE_AGENT_INTEGRATION, CONSOLE_INTEGRATION, connectMethods, createProviderConnectionController, @@ -268,12 +266,8 @@ export const DialogConnectProvider: Component<{ ) } -const CLAUDE_CODE_SETUP_URL = "https://code.claude.com/docs/en/setup" - -const CLAUDE_AGENT_DOCS_URL = "https://opencode.ai/v2/docs/providers#claude-agent" - -// The TUI's integration groups and order, plus Claude Agent, with connected integrations pulled up into their own group first. -const POPULAR = ["opencode", "opencode-go", "openai", "github-copilot", "anthropic", "claude-agent", "google"] +// The TUI's integration groups and order, with connected integrations pulled up into their own group first. +const POPULAR: readonly string[] = connectPopular function ProviderPicker(props: { directory?: string; onSelect: (provider: string) => void; onPrepare?: () => void }) { const integrations = useIntegrations(() => props.directory) @@ -518,7 +512,6 @@ function ProviderConnection(props: { const accountActions = useProviderAccountActions(directory) const integrationID = props.provider const isConsole = props.provider === CONSOLE_INTEGRATION - const isClaudeAgent = props.provider === CLAUDE_AGENT_INTEGRATION const remote = isConsole && authServerName(sdk.server) !== undefined const [state, setState] = createStore<{ @@ -535,7 +528,6 @@ function ProviderConnection(props: { switching?: string activating?: string renaming?: string - claudeAgent?: "checking" | "confirming" | "timeout" }>({ adding: false, direct: false, @@ -565,21 +557,12 @@ function ProviderConnection(props: { const controller = createProviderConnectionController({ provider: () => integrationID, directory, - // Claude Agent explains its setup before connecting, so its lone method never starts on its own. - manual: () => isClaudeAgent || (!!props.accounts && accounts().length > 0), + manual: () => !!props.accounts && accounts().length > 0, prepare: isConsole ? prepareConsoleCatalog : undefined, pollInterval: isConsole ? 500 : undefined, onComplete: () => { const method = controller.currentMethod() props.onConnected?.(method?.type === "oauth" ? method.id : undefined) - - // Saving the connection only records the choice; wait for the server's check of `claude`. - if (isClaudeAgent) { - setState("claudeAgent", "checking") - - return - } - showConnected() }, }) @@ -625,7 +608,8 @@ function ProviderConnection(props: { const connectionProviders = createMemo(() => (data.location.provider.list(location()) ?? []).filter( - (provider) => provider.id === props.provider || provider.integrationID === integrationID, + (provider) => + !isHiddenProvider(provider.id) && (provider.id === props.provider || provider.integrationID === integrationID), ), ) @@ -633,7 +617,11 @@ function ProviderConnection(props: { const ids = new Set(connectionProviders().map((provider) => provider.id)) return (data.location.model.list(location()) ?? []).filter( - (model) => ids.has(model.providerID) && model.enabled && model.status !== "deprecated", + (model) => + !isHiddenProvider(model.providerID) && + ids.has(model.providerID) && + model.enabled && + model.status !== "deprecated", ) }) @@ -647,58 +635,6 @@ function ProviderConnection(props: { const managedProviders = createMemo(() => (isConsole ? consoleProviderGroup(connectionProviders()) : undefined)) - // The server lists the active account first, and its status is the latest check of `claude`. - const claudeAgentProblem = () => (isClaudeAgent ? serverAccounts()[0]?.status : undefined) - - // A healthy connection keeps the usual accounts list; anything else shows the setup and its latest check. - const claudeAgentSetup = () => - isClaudeAgent && - controller.auth.state() !== "error" && - (!showAccounts() || !!state.claudeAgent || !!claudeAgentProblem() || controller.methodIndex() !== undefined) - - // A check that outlives the timeout can still finish, so a late result replaces the timeout message. - createEffect(() => { - if (!state.claudeAgent) return - - if (claudeAgentProblem()) return setState("claudeAgent", undefined) - - if (state.claudeAgent === "confirming" || connectionGroups().length === 0) return - setState("claudeAgent", "confirming") - void untrack(confirmClaudeAgent) - }) - - // A signed-out `claude` still lists its models, and the login problem arrives in a separate event, - // so read the connection again before treating the models as a working login. - async function confirmClaudeAgent() { - const ref = location() - data.location.integration.invalidate(ref) - await data.location.integration.sync(ref).catch(() => undefined) - - if (state.claudeAgent !== "confirming" || claudeAgentProblem()) return - setState("claudeAgent", undefined) - showConnected() - } - - createEffect(() => { - if (state.claudeAgent !== "checking") return - // The server allows 30 seconds for `claude` to start and 30 more to read each model's context size. - const timer = setTimeout(() => setState("claudeAgent", "timeout"), 65_000) - onCleanup(() => clearTimeout(timer)) - }) - - // Connecting again is what makes the server check `claude` again. - const checkClaudeAgent = async () => { - const previous = serverAccounts().map((account) => account.id) - setState("claudeAgent", undefined) - await controller.auth.select(0) - - if (controller.auth.state() !== "ready") return - // Each connect saves a new account, so drop the ones this check replaces. - await Promise.all( - previous.map((credentialID) => sdk.api.credential.remove({ credentialID }).catch(() => undefined)), - ) - } - // The server loads the Console workspace's providers after the grant lands, so the first refresh // can still show only the free catalog. Poll briefly for the workspace providers before moving on. async function prepareConsoleCatalog(active: () => boolean) { @@ -793,7 +729,7 @@ function ProviderConnection(props: { controller.authorization()?.mode === "auto" createEffect(() => { - if (showAccounts() && !claudeAgentSetup()) return props.onTitle(provider().name) + if (showAccounts()) return props.onTitle(provider().name) if (isConsole && controller.auth.state() === "error") return props.onTitle(language.t("provider.connect.opencode.errorTitle")) @@ -1215,7 +1151,6 @@ function ProviderConnection(props: { props.onAccounts( showAccounts() && controller.methodIndex() === undefined && - !claudeAgentSetup() && !(state.noModels && controller.auth.state() !== "error"), ), ) @@ -1480,8 +1415,8 @@ function ProviderConnection(props: { const addAccount = () => { setState({ adding: true, direct: false, confirming: undefined }) - // Like the TUI: a lone method starts directly, several show the method list. Claude Agent shows its setup first. - if (isClaudeAgent || controller.methods().length !== 1) return + // Like the TUI: a lone method starts directly, several show the method list. + if (controller.methods().length !== 1) return setState("direct", true) void controller.auth.select(0) } @@ -1634,72 +1569,6 @@ function ProviderConnection(props: { ) } - function ClaudeAgentSetup() { - const problem = () => - claudeAgentProblem()?.message ?? - (state.claudeAgent === "timeout" ? language.t("provider.connect.claudeAgent.timeout") : undefined) - - const checking = () => - state.claudeAgent === "checking" || - state.claudeAgent === "confirming" || - controller.auth.state() === "pending" || - controller.auth.state() === "refreshing" - - return ( -
-

- {language.t("provider.connect.claudeAgent.description")}{" "} - {language.t("provider.connect.claudeAgent.learnMore")} -

- - {(message) => ( -
- - {message()} -
- )} -
- - {language.t("provider.connect.claudeAgent.checking")}} - > -
- - -
-
-
- ) - } - function ConsoleNoModels() { return (
@@ -1911,9 +1780,6 @@ function ProviderConnection(props: { />
- - - diff --git a/packages/app/src/runtime/i18n/en.ts b/packages/app/src/runtime/i18n/en.ts index 3ea280e06667..9708688912c1 100644 --- a/packages/app/src/runtime/i18n/en.ts +++ b/packages/app/src/runtime/i18n/en.ts @@ -148,7 +148,6 @@ export const dict = { "dialog.provider.opencode.tagline": "Reliable optimized models", "dialog.provider.opencodeGo.tagline": "Low cost subscription for everyone", "dialog.provider.anthropic.note": "Direct access to Claude models via API key", - "dialog.provider.claudeAgent.note": "Use your Claude plan or another Claude Code login", "dialog.provider.copilot.note": "AI models for coding assistance via GitHub Copilot", "dialog.provider.openai.note": "GPT models for fast, capable general AI tasks", "dialog.provider.google.note": "Gemini models for fast, structured responses", @@ -229,16 +228,6 @@ export const dict = { "provider.connect.remote.description": "Your OpenCode credentials will be stored on this server. Models will be available through this server.", "provider.connect.title.anthropicProMax": "Login with Anthropic", - "provider.connect.claudeAgent.description": - "Use your Claude plan or another Claude Code login. Install Claude Code and sign in first.", - "provider.connect.claudeAgent.learnMore": "Learn more", - "provider.connect.claudeAgent.install": "Install Claude Code", - "provider.connect.claudeAgent.signIn.command": "Sign-in command", - "provider.connect.claudeAgent.connect": "Connect", - "provider.connect.claudeAgent.checkAgain": "Check again", - "provider.connect.claudeAgent.checking": "Checking Claude Code…", - "provider.connect.claudeAgent.timeout": - "Couldn't confirm that Claude Code is ready. Make sure it's installed and signed in, then check again.", "provider.connect.command.waiting": "Waiting for command to finish…", "provider.connect.command.expired": "Authentication expired", "provider.connect.form.required": "Answer required", diff --git a/packages/app/src/runtime/server/global-sync/child-store.ts b/packages/app/src/runtime/server/global-sync/child-store.ts index b463e342646e..ad1ebb3ec7eb 100644 --- a/packages/app/src/runtime/server/global-sync/child-store.ts +++ b/packages/app/src/runtime/server/global-sync/child-store.ts @@ -17,6 +17,7 @@ import { QueryOptionsApi } from "../sync" import { directoryKey, type DirectoryKey } from "./utils" import type { ServerScope } from "@/runtime/server/scope" import type { Data } from "@opencode/client/solid" +import { omitHiddenProviderCatalog } from "@/providers/catalog/order" import { normalizeAgentList, normalizeProviderList } from "./utils" import { ProjectState, VcsState } from "../persistence" @@ -213,7 +214,7 @@ export function createChildStoreManager(input: { if (!provider || !model) return { all: new Map(), connected: [], default: {} } - return normalizeProviderList(provider, model) + return omitHiddenProviderCatalog(normalizeProviderList(provider, model)) }, config: {}, get path() { diff --git a/packages/app/src/settings/providers/providers.tsx b/packages/app/src/settings/providers/providers.tsx index b6484445e2f5..ab613683de35 100644 --- a/packages/app/src/settings/providers/providers.tsx +++ b/packages/app/src/settings/providers/providers.tsx @@ -31,7 +31,6 @@ const PROVIDER_NOTES = [ { match: (id: string) => id === "opencode", key: "dialog.provider.opencode.note" }, { match: (id: string) => id === "opencode-go", key: "dialog.provider.opencodeGo.tagline" }, { match: (id: string) => id === "anthropic", key: "dialog.provider.anthropic.note" }, - { match: (id: string) => id === "claude-agent", key: "dialog.provider.claudeAgent.note" }, { match: (id: string) => id.startsWith("github-copilot"), key: "dialog.provider.copilot.note" }, { match: (id: string) => id === "openai", key: "dialog.provider.openai.note" }, { match: (id: string) => id === "google", key: "dialog.provider.google.note" }, diff --git a/packages/app/src/voice/tools.ts b/packages/app/src/voice/tools.ts index 1b9e2de2afd0..75e5c0e06cc6 100644 --- a/packages/app/src/voice/tools.ts +++ b/packages/app/src/voice/tools.ts @@ -1,5 +1,6 @@ import type { SessionInfo, SessionMessageInfo } from "@opencode/client/promise" import { SessionMessage } from "@opencode/schema/session-message" +import { isHiddenProvider } from "@/providers/catalog/order" import type { ServerCtx } from "@/runtime/server/runtime" import { decodeArguments, type ToolCall } from "./protocol" import { ReplyWatch } from "./replies" @@ -242,7 +243,7 @@ export class VoiceTools { const current = session.model const models = matchModels( - catalog.data.filter((model) => model.enabled), + catalog.data.filter((model) => model.enabled && !isHiddenProvider(model.providerID)), args.query, ) @@ -270,7 +271,7 @@ export class VoiceTools { if (!wanted) throw new Error("model required") const session = await api.session.get({ sessionID }, options) const catalog = await api.model.list({ location: { directory: session.location.directory } }, options) - const enabled = catalog.data.filter((item) => item.enabled) + const enabled = catalog.data.filter((item) => item.enabled && !isHiddenProvider(item.providerID)) const model = enabled.find((item) => `${item.providerID}/${item.id}` === wanted) ?? diff --git a/packages/cli/src/acp/catalog.ts b/packages/cli/src/acp/catalog.ts index 37eb1ee0e9c5..3ce7759424fa 100644 --- a/packages/cli/src/acp/catalog.ts +++ b/packages/cli/src/acp/catalog.ts @@ -3,6 +3,7 @@ import type { Agent } from "@opencode/schema/agent" import type { Command } from "@opencode/schema/command" import type { Model } from "@opencode/schema/model" import { FSUtil } from "@opencode/util/fs-util" +import { isHiddenProvider } from "@opencode/util/hidden-provider" import { Cause, Deferred, Effect, Exit, Schedule, Semaphore, Stream, SubscriptionRef } from "effect" import { ACPError } from "./error" @@ -147,10 +148,12 @@ const read = Effect.fnUntraced(function* (client: OpenCodeClient, cwd: string) { ], { concurrency: "unbounded" }, ).pipe(Effect.mapError((cause) => new ACPError.CatalogLoadError({ cause }))) - const models = modelResult.data.filter((model) => model.enabled) + const models = modelResult.data.filter((model) => model.enabled && !isHiddenProvider(model.providerID)) const preferred = defaultResult.data // The parallel default read can name a model missing from this list. - const defaultModel = preferred ? findModel(models, preferred) : models[0] + // A hidden Claude Agent default falls through to the first visible model. + const defaultModel = + preferred && !isHiddenProvider(preferred.providerID) ? findModel(models, preferred) : models[0] if (!defaultModel) return yield* new ACPError.CatalogNotReadyError({ reason: "models" }) const agents = agentResult.data.filter((agent) => agent.mode !== "subagent" && !agent.hidden) // Core lists its resolved default agent first, the same one a new session runs. diff --git a/packages/cli/src/commands/handlers/auth/login.ts b/packages/cli/src/commands/handlers/auth/login.ts index 82b31b581685..67bf0e4d359f 100644 --- a/packages/cli/src/commands/handlers/auth/login.ts +++ b/packages/cli/src/commands/handlers/auth/login.ts @@ -1,6 +1,7 @@ import { intro, log, outro, select, spinner, text } from "@clack/prompts" -import { Effect, Option } from "effect" import type { FormAnswer, IntegrationInfo, OpenCodeClient } from "@opencode/client" +import { isHiddenProvider } from "@opencode/util/hidden-provider" +import { Effect, Option } from "effect" import { Commands } from "../../commands" import { Runtime } from "../../../framework/runtime" import { selectIntegration, type IntegrationChoice } from "../../../ui/integration-picker" @@ -77,7 +78,8 @@ const findIntegration = Effect.fn("cli.auth.login.integration")(function* (clien export function loginChoices(integrations: IntegrationInfo[]): IntegrationChoice[] { return integrations - .filter((integration) => connectMethods(integration).length > 0) + // Shuvcode does not offer the upstream Claude Agent provider. + .filter((integration) => !isHiddenProvider(integration.id) && connectMethods(integration).length > 0) .toSorted( (a, b) => Number(b.metadata?.source === "mcp") - Number(a.metadata?.source === "mcp") || diff --git a/packages/cli/src/commands/handlers/auth/shared.ts b/packages/cli/src/commands/handlers/auth/shared.ts index 737bffab56bd..8706d1913182 100644 --- a/packages/cli/src/commands/handlers/auth/shared.ts +++ b/packages/cli/src/commands/handlers/auth/shared.ts @@ -1,6 +1,7 @@ -import { Effect } from "effect" import { OpenCode, type IntegrationInfo, type IntegrationMethod, type OpenCodeClient } from "@opencode/client" import { Service } from "@opencode/client/effect/service" +import { isHiddenProvider } from "@opencode/util/hidden-provider" +import { Effect } from "effect" import { ServerConnection } from "../../../services/server-connection" export const location = { directory: process.cwd() } @@ -19,7 +20,10 @@ export const loadIntegrations = Effect.fn("cli.auth.integrations")(function* (cl yield* request((signal) => client.model.default({ location }, { signal })) return yield* request((signal) => client.integration.list({ location }, { signal })).pipe( Effect.map((response) => - response.data.toSorted((a, b) => Number(b.id === "opencode-go") - Number(a.id === "opencode-go")), + response.data + // Shuvcode does not offer the upstream Claude Agent provider. + .filter((integration) => !isHiddenProvider(integration.id)) + .toSorted((a, b) => Number(b.id === "opencode-go") - Number(a.id === "opencode-go")), ), ) }) diff --git a/packages/cli/src/commands/handlers/models.ts b/packages/cli/src/commands/handlers/models.ts index f7a66bd7d0d5..9846bea0b01d 100644 --- a/packages/cli/src/commands/handlers/models.ts +++ b/packages/cli/src/commands/handlers/models.ts @@ -1,5 +1,6 @@ import { OpenCode } from "@opencode/client" import { Service } from "@opencode/client/effect/service" +import { isHiddenProvider } from "@opencode/util/hidden-provider" import { Effect } from "effect" import { EOL } from "node:os" import { Commands } from "../commands" @@ -16,6 +17,7 @@ export default Runtime.handler( }) const response = yield* Effect.promise(() => client.model.list({ location: { directory: process.cwd() } })) const models = response.data + .filter((model) => !isHiddenProvider(model.providerID)) .map((model) => `${model.providerID}/${model.id}`) .toSorted((a, b) => a.localeCompare(b)) if (models.length > 0) process.stdout.write(models.join(EOL) + EOL) diff --git a/packages/cli/test/auth-login-choices.test.ts b/packages/cli/test/auth-login-choices.test.ts index fc1f071861ff..aabeadf4b4d4 100644 --- a/packages/cli/test/auth-login-choices.test.ts +++ b/packages/cli/test/auth-login-choices.test.ts @@ -8,6 +8,15 @@ const integration = (value: Partial & Pick { + expect( + loginChoices([ + integration({ id: "claude-agent", name: "Claude Agent" }), + integration({ id: "anthropic", name: "Anthropic" }), + ]).map((choice) => choice.value), + ).toEqual(["anthropic"]) +}) + test("groups the CLI choices like /connect while keeping stable login IDs", () => { expect( loginChoices([ diff --git a/packages/core/src/config/plugin/provider.ts b/packages/core/src/config/plugin/provider.ts index e9d2d6d51a16..a6f818bdf30a 100644 --- a/packages/core/src/config/plugin/provider.ts +++ b/packages/core/src/config/plugin/provider.ts @@ -6,6 +6,7 @@ import { define } from "@opencode/plugin/effect/plugin" import { Document, type Entry } from "@opencode/schema/config" import { ConfigProvider } from "@opencode/schema/config/provider" import { Money } from "@opencode/schema/money" +import { isHiddenProvider } from "@opencode/util/hidden-provider" import { Effect } from "effect" import { Config } from "../../config.js" import { Model } from "../../model.js" @@ -109,7 +110,8 @@ export const Plugin = define({ // model transforms. They can recreate or re-enable a model within an available provider. yield* ctx.model.transform((models) => { const configuredDefault = sources.defaultModel - if (configuredDefault !== undefined) models.default.set(configuredDefault.providerID, configuredDefault.model) + if (configuredDefault !== undefined && !isHiddenProvider(configuredDefault.providerID)) + models.default.set(configuredDefault.providerID, configuredDefault.model) for (const [item, definition] of sources.models) { const providerID = definition.providerID for (const [id, config] of Object.entries(item.models ?? {})) { @@ -159,6 +161,7 @@ function configuredProviders(entries: readonly Entry[]) { return entries .filter((entry): entry is Document => entry.type === "document" && !(entry instanceof ConfigIntegration.Document)) .flatMap((file) => Object.entries(file.info.providers ?? {})) + .filter(([id]) => !isHiddenProvider(id)) } /** @@ -168,6 +171,7 @@ function configuredProviders(entries: readonly Entry[]) { */ export function integrate(providers: ProviderEditor, document: ConfigIntegration.Document) { for (const [providerID, item] of Object.entries(document.info.providers ?? {})) { + if (isHiddenProvider(providerID)) continue const { source } = overlayProvider(providers, providerID, item, document.integrationID) // An integration lists the models it routes: each starts again from the catalog and takes the provider's // package and URL. Its models are complete here, so later model transforms and config apply on top. diff --git a/packages/core/src/plugin/provider.ts b/packages/core/src/plugin/provider.ts index 59b697bb73e8..a6367b7df7b0 100644 --- a/packages/core/src/plugin/provider.ts +++ b/packages/core/src/plugin/provider.ts @@ -5,7 +5,6 @@ import { AmazonBedrockPlugin } from "./provider/amazon-bedrock.js" import { AzurePlugin } from "./provider/azure.js" import { CerebrasPlugin } from "./provider/cerebras.js" import { ChatGPTPlugin } from "./provider/chatgpt.js" -import { ClaudeAgentPlugin } from "./provider/claude-agent/index.js" import { CloudflareAIGatewayPlugin } from "./provider/cloudflare-ai-gateway.js" import { CloudflareWorkersAIPlugin } from "./provider/cloudflare-workers-ai.js" import { DatabricksPlugin } from "./provider/databricks.js" @@ -34,6 +33,10 @@ import { XAIPlugin } from "./provider/xai.js" import { ZenmuxPlugin } from "./provider/zenmux.js" import type { PluginInternal } from "./internal.js" +// Shuvcode keeps the upstream Claude Agent SDK tree on disk for sync and does +// not load it. Anthropic in the product is ClaudePlugin. Drop a reintroduced +// `opencode.provider.claude-agent` registration so restoring the import is not +// enough to publish the provider. export const ProviderPlugins: PluginInternal.InternalPlugin[] = [ AmazonBedrockPlugin, ClaudePlugin, @@ -69,5 +72,4 @@ export const ProviderPlugins: PluginInternal.InternalPlugin[] = [ DynamicProviderPlugin, // Reads the active subscription connections registered above; order it last. QuotaPlugin, - ClaudeAgentPlugin, -] +].filter((plugin) => plugin.id !== "opencode.provider.claude-agent") diff --git a/packages/core/src/plugin/provider/claude-agent/index.ts b/packages/core/src/plugin/provider/claude-agent/index.ts index ea217f5e29d0..b2157f228705 100644 --- a/packages/core/src/plugin/provider/claude-agent/index.ts +++ b/packages/core/src/plugin/provider/claude-agent/index.ts @@ -45,4 +45,12 @@ export const make = (io: ClaudeIO.Interface, registry = ClaudeSessions.registry( }), } satisfies PluginInternal.InternalPlugin) -export const ClaudeAgentPlugin = make(ClaudeIO.real) +// Product import. `make` still builds the upstream plugin for the on-disk tests. +// This export does not register a provider, integration, or model. +export const ClaudeAgentPlugin = define({ + id: ID, + effect: Effect.fn("ClaudeAgent.Plugin")(function* () { + // Registered plugins with this id are also filtered out of ProviderPlugins. + yield* Effect.void + }), +}) diff --git a/packages/core/test/config/provider.test.ts b/packages/core/test/config/provider.test.ts index ad4f1ae32f71..e008cb32055c 100644 --- a/packages/core/test/config/provider.test.ts +++ b/packages/core/test/config/provider.test.ts @@ -343,6 +343,46 @@ describe("ConfigProviderPlugin.Plugin", () => { ) } + it.effect("does not create Claude Agent from a configured provider id", () => + Effect.gen(function* () { + const integrations = yield* Integration.Service + const providers = yield* Provider.Service + const entries = [ + new Document({ + type: "document", + info: decode({ + providers: { + "claude-agent": { + name: "Claude Agent", + package: "aisdk:@ai-sdk/openai-compatible", + models: { chat: {} }, + }, + "opencode.provider.claude-agent": { + package: "aisdk:@ai-sdk/openai-compatible", + models: { chat: {} }, + }, + litellm: { + package: "aisdk:@ai-sdk/openai-compatible", + models: { chat: {} }, + }, + }, + }), + }), + ] + + yield* addPlugin(entries) + + expect(yield* integrations.get(Integration.ID.make("claude-agent"))).toBeUndefined() + expect(yield* integrations.get(Integration.ID.make("opencode.provider.claude-agent"))).toBeUndefined() + expect(yield* providers.get(Provider.ID.claudeAgent)).toBeUndefined() + expect(yield* providers.get(Provider.ID.make("opencode.provider.claude-agent"))).toBeUndefined() + expect(yield* integrations.get(Integration.ID.make("litellm"))).toMatchObject({ + id: "litellm", + methods: [{ type: "key", label: "Manually enter API Key" }], + }) + }), + ) + it.effect("adds key auth for custom providers without env credentials", () => Effect.gen(function* () { const integrations = yield* Integration.Service diff --git a/packages/core/test/plugin/bundled-subscriptions.test.ts b/packages/core/test/plugin/bundled-subscriptions.test.ts index a7ef25cee6d6..1637c100ff56 100644 --- a/packages/core/test/plugin/bundled-subscriptions.test.ts +++ b/packages/core/test/plugin/bundled-subscriptions.test.ts @@ -1,11 +1,13 @@ import { describe, expect, test } from "bun:test" import { Effect } from "effect" import { ClaudePlugin } from "@shuvcode/claude-plugin" +import { ClaudeAgentPlugin } from "@opencode/core/plugin/provider/claude-agent/index" import { GoogleAntigravityPlugin } from "@shuvcode/antigravity-plugin" import { Integration } from "@opencode/core/integration" import { Plugin } from "@opencode/core/plugin" import { PluginHost } from "@opencode/core/plugin/host" import { ProviderPlugins } from "@opencode/core/plugin/provider" +import { Provider } from "@opencode/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" @@ -23,6 +25,32 @@ describe("bundled subscription providers", () => { } }) + test("keeps Claude Pro/Max and does not register Claude Agent", () => { + expect(ProviderPlugins).toContain(ClaudePlugin) + expect(ProviderPlugins.filter((plugin) => plugin.id === ClaudePlugin.id)).toHaveLength(1) + expect(ProviderPlugins.map((plugin) => plugin.id)).not.toContain("opencode.provider.claude-agent") + expect(ProviderPlugins).not.toContain(ClaudeAgentPlugin) + }) + + it.effect("loads bundled providers without registering Claude Agent", () => + Effect.gen(function* () { + const integration = yield* Integration.Service + const provider = yield* Provider.Service + const plugin = yield* Plugin.Service + const host = yield* PluginHost.make(plugin) + for (const entry of ProviderPlugins) { + // SAFETY: PluginTestLayer provides the services these plugins read. The assertion + // only hides requirements this test effect's type does not list. + yield* entry.effect(host) as Effect.Effect + } + expect(yield* integration.get(Integration.ID.make("claude-agent"))).toBeUndefined() + expect(yield* provider.get(Provider.ID.claudeAgent)).toBeUndefined() + expect((yield* integration.get(Integration.ID.make("anthropic")))?.methods).toContainEqual( + expect.objectContaining({ id: "claude-pro-max", type: "oauth" }), + ) + }), + ) + it.effect("registers both login methods without external plugin configuration", () => Effect.gen(function* () { const integration = yield* Integration.Service diff --git a/packages/tui/src/component/dialog-integration.tsx b/packages/tui/src/component/dialog-integration.tsx index 17a5143af19b..73539a0e8b53 100644 --- a/packages/tui/src/component/dialog-integration.tsx +++ b/packages/tui/src/component/dialog-integration.tsx @@ -12,6 +12,7 @@ import type { FormValue, LocationRef, } from "@opencode/client" +import { isHiddenProvider } from "@opencode/util/hidden-provider" import { openUrl } from "@opencode/util/open" import { createEffect, createMemo, createSignal, For, onCleanup, onMount, Show } from "solid-js" import { createStore } from "solid-js/store" @@ -67,13 +68,15 @@ const SUBMIT = Symbol("submit") const OAUTH_CODE = /[A-Z0-9]{4}-[A-Z0-9]{4,5}/ export function integrationOptions(list: IntegrationInfo[]) { - return list.toSorted( - (a, b) => - Number(b.metadata?.source === "mcp") - Number(a.metadata?.source === "mcp") || - (INTEGRATION_PRIORITY[a.id] ?? 99) - (INTEGRATION_PRIORITY[b.id] ?? 99) || - a.name.localeCompare(b.name) || - a.id.localeCompare(b.id), - ) + return list + .filter((item) => !isHiddenProvider(item.id)) + .toSorted( + (a, b) => + Number(b.metadata?.source === "mcp") - Number(a.metadata?.source === "mcp") || + (INTEGRATION_PRIORITY[a.id] ?? 99) - (INTEGRATION_PRIORITY[b.id] ?? 99) || + a.name.localeCompare(b.name) || + a.id.localeCompare(b.id), + ) } export function connectMethods(integration: IntegrationInfo): ConnectMethod[] { diff --git a/packages/tui/src/component/dialog-model-free.tsx b/packages/tui/src/component/dialog-model-free.tsx index b419753138c3..f9b56e251663 100644 --- a/packages/tui/src/component/dialog-model-free.tsx +++ b/packages/tui/src/component/dialog-model-free.tsx @@ -1,3 +1,4 @@ +import { isHiddenProvider } from "@opencode/util/hidden-provider" import * as fuzzysort from "fuzzysort" import { createMemo, createSignal } from "solid-js" import { useData } from "../context/data" @@ -25,7 +26,7 @@ export function DialogFreeModels(props: { const options = createMemo(() => { const all = (data.location.model.list(location.ref) ?? []) - .filter((model) => model.status !== "deprecated" && isFreeModel(model)) + .filter((model) => model.status !== "deprecated" && isFreeModel(model) && !isHiddenProvider(model.providerID)) .map((model) => { const value = { providerID: model.providerID, modelID: model.id } return { diff --git a/packages/tui/src/component/dialog-model.tsx b/packages/tui/src/component/dialog-model.tsx index ea8f970469b5..21ff3abcb4fe 100644 --- a/packages/tui/src/component/dialog-model.tsx +++ b/packages/tui/src/component/dialog-model.tsx @@ -1,4 +1,5 @@ import type { ModelInfo } from "@opencode/client" +import { isHiddenProvider } from "@opencode/util/hidden-provider" import { createMemo, createSignal, onMount, Show } from "solid-js" import { useLocal } from "../context/local" import { DialogSelect } from "../ui/dialog-select" @@ -93,9 +94,16 @@ function DialogConnectedModels(props: { const connected = useConnected() const providers = createMemo( - () => new Map((data.location.provider.list(location.ref) ?? []).map((item) => [item.id, item])), + () => + new Map( + (data.location.provider.list(location.ref) ?? []) + .filter((item) => !isHiddenProvider(item.id)) + .map((item) => [item.id, item]), + ), + ) + const models = createMemo(() => + (data.location.model.list(location.ref) ?? []).filter((model) => !isHiddenProvider(model.providerID)), ) - const models = createMemo(() => data.location.model.list(location.ref) ?? []) const showExtra = createMemo(() => connected() && !props.providerID) diff --git a/packages/tui/src/component/usage.tsx b/packages/tui/src/component/usage.tsx index 63b3174dbcf9..52d014f6a1a2 100644 --- a/packages/tui/src/component/usage.tsx +++ b/packages/tui/src/component/usage.tsx @@ -1,4 +1,5 @@ import type { ModelInfo, SessionMessageAssistant } from "@opencode/client" +import { isHiddenProvider } from "@opencode/util/hidden-provider" import { TextAttributes } from "@opentui/core" import { createEffect, createMemo, createSignal, For, Match, onCleanup, onMount, Show, Switch } from "solid-js" import { useData } from "../context/data" @@ -33,7 +34,14 @@ export function useUsageLimits() { const data = useData() const location = useLocation() const ref = () => location.ref ?? data.location.default() - const providers = createMemo(() => new Map((data.location.provider.list(ref()) ?? []).map((item) => [item.id, item]))) + const providers = createMemo( + () => + new Map( + (data.location.provider.list(ref()) ?? []) + .filter((item) => !isHiddenProvider(item.id)) + .map((item) => [item.id, item]), + ), + ) const info = () => data.location.usage.limits(ref()) const usage = (model: ModelInfo) => { const value = info() @@ -46,7 +54,7 @@ export function useUsageLimits() { if (refresh) data.location.usage.invalidate(ref()) void data.location.usage.sync(ref()).catch(() => undefined) }, - models: () => data.location.model.list(ref()) ?? [], + models: () => (data.location.model.list(ref()) ?? []).filter((model) => !isHiddenProvider(model.providerID)), usage, served: (providerID: string) => servedByConsole(providers().get(providerID)), /** A paid model OpenCode Console serves without any limit on it. */ diff --git a/packages/tui/src/context/local.tsx b/packages/tui/src/context/local.tsx index d3304eb3390b..9a720215a835 100644 --- a/packages/tui/src/context/local.tsx +++ b/packages/tui/src/context/local.tsx @@ -25,6 +25,7 @@ import { useRoute } from "./route" import { useData } from "./data" import { usePermission } from "./permission" import { useLocation } from "./location" +import { isHiddenProvider } from "@opencode/util/hidden-provider" import { parse } from "../util/model" export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ @@ -41,8 +42,10 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ const permission = usePermission() const location = useLocation() - const models = () => data.location.model.list(location.ref) - const providers = () => data.location.provider.list(location.ref) + const models = () => + data.location.model.list(location.ref)?.filter((model) => !isHiddenProvider(model.providerID)) + const providers = () => + data.location.provider.list(location.ref)?.filter((provider) => !isHiddenProvider(provider.id)) function isModelValid(model: ModelPreferenceModel) { return !!models()?.some((item) => item.providerID === model.providerID && item.id === model.modelID) diff --git a/packages/tui/src/mini/catalog.shared.ts b/packages/tui/src/mini/catalog.shared.ts index d825c9869c00..1a2817996cb2 100644 --- a/packages/tui/src/mini/catalog.shared.ts +++ b/packages/tui/src/mini/catalog.shared.ts @@ -6,6 +6,7 @@ import type { OpenCodeClient, ProviderListOutput, } from "@opencode/client/promise" +import { isHiddenProvider } from "@opencode/util/hidden-provider" import type { RunAgent, RunCommand, RunProvider, RunReference } from "./types" type CurrentAgent = AgentListOutput["data"][number] @@ -48,6 +49,7 @@ export function runProviders(providers: CurrentProvider[], models: CurrentModel[ const grouped = new Map() for (const provider of providers) { + if (isHiddenProvider(provider.id)) continue grouped.set(provider.id, { id: provider.id, name: provider.name, @@ -56,6 +58,7 @@ export function runProviders(providers: CurrentProvider[], models: CurrentModel[ } for (const model of models) { + if (isHiddenProvider(model.providerID)) continue const provider = grouped.get(model.providerID) ?? { id: model.providerID, name: model.providerID, diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index 70116df4341f..df5ea08851dc 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -12,6 +12,7 @@ import { Switch, type Accessor, } from "solid-js" +import { isHiddenProvider } from "@opencode/util/hidden-provider" import path from "node:path" import { EOL, tmpdir } from "node:os" import { mkdir, writeFile } from "node:fs/promises" @@ -262,7 +263,9 @@ export function Session(props: { commands: [{ bind: "escape,ctrl+c", title: "Close sidebar", group: "Session", run: props.onToggleSidebar }], })) const contentWidth = createMemo(() => (props.width ?? dimensions().width - props.verticalTabsWidth) - 4) - const models = createMemo(() => data.location.model.list(location()) ?? []) + const models = createMemo( + () => (data.location.model.list(location()) ?? []).filter((model) => !isHiddenProvider(model.providerID)), + ) const scrollAcceleration = createMemo(() => getScrollAcceleration(config)) const toast = useToast() diff --git a/packages/tui/test/cli/cmd/tui/integration-options.test.ts b/packages/tui/test/cli/cmd/tui/integration-options.test.ts index 30c35f2f8d92..e8cc3b28803c 100644 --- a/packages/tui/test/cli/cmd/tui/integration-options.test.ts +++ b/packages/tui/test/cli/cmd/tui/integration-options.test.ts @@ -14,6 +14,15 @@ const integration = (value: Partial & Pick { + test("hides the upstream Claude Agent integration", () => { + expect( + integrationOptions([ + integration({ id: "claude-agent", name: "Claude Agent" }), + integration({ id: "anthropic", name: "Anthropic" }), + ]).map((item) => item.id), + ).toEqual(["anthropic"]) + }) + test("keeps popular integrations first and sorts the rest alphabetically", () => { expect( integrationOptions([ diff --git a/packages/tui/test/mini/catalog.shared.test.ts b/packages/tui/test/mini/catalog.shared.test.ts index 3d255781c353..4e0b2c712930 100644 --- a/packages/tui/test/mini/catalog.shared.test.ts +++ b/packages/tui/test/mini/catalog.shared.test.ts @@ -73,4 +73,26 @@ describe("run catalog shared", () => { }, ]) }) + + test("drops the upstream Claude Agent provider from the footer catalog", () => { + expect( + runProviders( + [catalogProvider("claude-agent", "Claude Agent"), catalogProvider("anthropic", "Anthropic")], + [ + catalogModel({ + id: "sonnet", + modelID: "sonnet", + providerID: "claude-agent", + name: "Claude Agent Sonnet", + }), + catalogModel({ + id: "claude-sonnet", + modelID: "claude-sonnet", + providerID: "anthropic", + name: "Claude Sonnet", + }), + ], + ).map((provider) => provider.id), + ).toEqual(["anthropic"]) + }) }) diff --git a/packages/util/src/hidden-provider.test.ts b/packages/util/src/hidden-provider.test.ts new file mode 100644 index 000000000000..4be2f933161c --- /dev/null +++ b/packages/util/src/hidden-provider.test.ts @@ -0,0 +1,8 @@ +import { expect, test } from "bun:test" +import { isHiddenProvider } from "./hidden-provider.js" + +test("hides the upstream Claude Agent ids and keeps Anthropic", () => { + expect(isHiddenProvider("claude-agent")).toBe(true) + expect(isHiddenProvider("opencode.provider.claude-agent")).toBe(true) + expect(isHiddenProvider("anthropic")).toBe(false) +}) diff --git a/packages/util/src/hidden-provider.ts b/packages/util/src/hidden-provider.ts new file mode 100644 index 000000000000..51c483af8296 --- /dev/null +++ b/packages/util/src/hidden-provider.ts @@ -0,0 +1,6 @@ +// Shuvcode keeps the upstream Claude Agent SDK on disk and does not offer it. +const hiddenProviderIDs = new Set(["claude-agent", "opencode.provider.claude-agent"]) + +export function isHiddenProvider(id: string) { + return hiddenProviderIDs.has(id) +}