# Model Management — Data-Model Review

Designed from first principles against described behavior. Single-page reference. The implementation on `zero/model-settings-redesign` does not fully match this design — the companion Markdown spec at `designdocs/MODEL_DATA_MODEL_SPEC.md` includes a reconciliation appendix.

## 1. TL;DR

- Two layers of "provider": a read-only ProviderType (catalog metadata from models.dev) and a persisted ProviderInstance (the user's credential set). Multiple instances of the same type are allowed — two Anthropic keys live as two ProviderInstance rows.
- BYOK enrollment is the unit of identity: a ByokEnrollment row points at `(instanceId, modelId)`. Everything downstream holds `enrollmentRef = ${instanceId}::${modelId}`.
- Custom models attach to instances, not types — two Ollama machines can declare different model lineups.
- Per-use-case (consumer) selection: each consumer (Simple Chat, Vault QA, OpenCode agent, …) holds its own allow-list of model refs. A consumer ref is polymorphic: BYOK / backend-bundled / Copilot Plus.
- Pipeline reachability is declared per ProviderType (`pipelines: {langchain, opencode}`). Single-pipeline-only providers are acceptable; the consumer picker filters by pipeline.
- Six AdapterKinds, not 30. The catalog still surfaces dozens of providers; most route through `openai-compatible` with a custom `baseUrl`.

## 2. Old world (master, pre-refactor)

For context. The master branch (pre-`zero/model-settings-redesign`) had a much flatter shape. Everything was a `CustomModel`; credentials lived as a flat bag of fields on `CopilotSettings`.

### 2.1 The `CustomModel` mega-type

Quoted verbatim from `git show master:src/aiParams.ts`:

    export interface CustomModel {
      name: string;
      provider: string;
      baseUrl?: string;
      apiKey?: string;
      enabled: boolean;
      isEmbeddingModel?: boolean;
      isBuiltIn?: boolean;
      enableCors?: boolean;
      core?: boolean;
      stream?: boolean;
      streamUsage?: boolean;
      temperature?: number;
      maxTokens?: number;
      topP?: number;
      frequencyPenalty?: number;

      // Ollama specific fields
      numCtx?: number;

      // LM Studio specific fields
      useResponsesApi?: boolean;

      // OpenRouter specific fields
      enablePromptCaching?: boolean;

      projectEnabled?: boolean;
      plusExclusive?: boolean;
      believerExclusive?: boolean;
      capabilities?: ModelCapability[];
      displayName?: string;

      // Embedding models only (Jina at the moment)
      dimensions?: number;
      // OpenAI specific fields
      openAIOrgId?: string;

      // Azure OpenAI specific fields
      azureOpenAIApiInstanceName?: string;
      azureOpenAIApiDeploymentName?: string;
      azureOpenAIApiVersion?: string;
      azureOpenAIApiEmbeddingDeploymentName?: string;

      // Amazon Bedrock specific fields
      bedrockRegion?: string;

      // OpenAI GPT-5 and O-series specific fields
      reasoningEffort?: ReasoningEffort;
      verbosity?: Verbosity;
    }

### 2.2 Settings — credentials as a flat bag

Excerpted from `git show master:src/settings/model.ts` (`CopilotSettings`):

      openAIApiKey: string;
      openAIOrgId: string;
      huggingfaceApiKey: string;
      cohereApiKey: string;
      anthropicApiKey: string;
      azureOpenAIApiKey: string;
      azureOpenAIApiInstanceName: string;
      azureOpenAIApiDeploymentName: string;
      azureOpenAIApiVersion: string;
      azureOpenAIApiEmbeddingDeploymentName: string;
      googleApiKey: string;
      openRouterAiApiKey: string;
      xaiApiKey: string;
      mistralApiKey: string;
      deepseekApiKey: string;
      amazonBedrockApiKey: string;
      amazonBedrockRegion: string;
      siliconflowApiKey: string;
      githubCopilotAccessToken: string;
      githubCopilotToken: string;
      githubCopilotTokenExpiresAt: number;
      ...
      defaultChainType: ChainType;
      defaultModelKey: string;       // "<name>|<provider>"
      embeddingModelKey: string;
      temperature: number;
      maxTokens: number;
      ...
      activeModels: Array<CustomModel>;
      activeEmbeddingModels: Array<CustomModel>;

### 2.3 Provider enum

From `git show master:src/constants.ts`:

    export enum ChatModelProviders {
      OPENROUTERAI = "openrouterai",
      OPENAI = "openai",
      OPENAI_FORMAT = "3rd party (openai-format)",
      ANTHROPIC = "anthropic",
      GOOGLE = "google",
      XAI = "xai",
      AMAZON_BEDROCK = "amazon-bedrock",
      AZURE_OPENAI = "azure openai",
      GROQ = "groq",
      OLLAMA = "ollama",
      LM_STUDIO = "lm-studio",
      COPILOT_PLUS = "copilot-plus",
      MISTRAL = "mistralai",
      DEEPSEEK = "deepseek",
      COHEREAI = "cohereai",
      SILICONFLOW = "siliconflow",
      GITHUB_COPILOT = "github-copilot",
    }

Takeaway. Every concept that's now an entity (catalog metadata, credential set, enrollment, consumer selection) was a field — or worse, a string — in one big record. Identity was `"<name>|<provider>"`; one default per app; one credential per provider type. Multi-instance was structurally impossible.

## 3. Behaviors driving the new design

1. Provider configuration. Pick a type, supply name / URL / key, plus adapter-specific extras (Azure deployment name, Bedrock region, OpenAI org id).
2. Multiple instances of the same provider type. Two Anthropic credential sets ("prod" and "staging"), two Ollama endpoints, etc. Each independently configured and enrolled.
3. Catalog drives the provider list. Add Provider iterates the full `models.dev` catalog (dozens of entries).
4. Catalog drives the model list per provider. Once a provider is configured, the catalog says what models it serves.
5. BYOK enrollment. The user picks specific models to bring into the pool — separate from "do I have a key for this provider."
6. Custom models on custom providers. Self-hosted endpoints have no catalog; the user declares models by hand.
7. Per-use-case (consumer) model selection. Simple Chat, Vault QA, OpenCode agent, Quick Chat, custom commands, etc. — each holds its own allow-list and its own default. Same enrollment can be selected by several consumers.
8. Backend-supplied models. Agent backends report models at runtime that aren't BYOK (OpenCode bundled, Claude Code subscription, Codex subscription, Copilot Plus hosted). Pickers union BYOK + backend-reported, intersected with the user's per-consumer selection.
9. Per-pipeline reachability is declared, not assumed. Each provider is reachable from (a) the LangChain direct path or (b) the OpenCode byokBridge path, or both. Single-pipeline-only is acceptable. Subscription backends sit outside this axis.
10. Embeddings parity (deferred). The same five-layer pipeline applies; rollout sequenced separately.

## 4. Entity catalog

### 4.1 ProviderType read-only

Describes a kind of provider. Sourced from `models.dev` plus our corrections layer. Not persisted in user settings.

    interface ProviderType {
      id: string;                          // "anthropic", "openai", "google", "mistral", "groq", "deepseek", "openrouter", …
      displayName: string;
      adapter: AdapterKind;                // closed at six values (see §5)
      defaultBaseUrl?: string;             // catalog-supplied
      auth: "api-key" | "oauth" | "none";
      envVars?: string[];
      pipelines: { langchain: boolean; opencode: boolean };  // see §6
      models: Record<string, CatalogModel>;
    }

    type AdapterKind =
      | "anthropic"
      | "openai-compatible"
      | "google"
      | "azure"
      | "bedrock"
      | "github-copilot";

### 4.2 ProviderInstance persisted

A user-configured credential set for a `ProviderType`. Multiple instances per type are allowed.

    interface ProviderInstance {
      instanceId: string;                  // UUID — primary key; keychain namespace
      providerTypeId: string;              // FK to ProviderType.id (non-unique)
      displayName: string;                 // "Anthropic (prod)" / "Anthropic (staging)"
      baseUrl?: string;                    // overrides ProviderType.defaultBaseUrl
      apiKey?: KeychainRef | null;
      extras?: Record<string, unknown>;    // azure deployment, bedrock region, openai org id, …
      addedAt: number;
      lastVerifiedAt?: number;
      lastVerificationError?: string;
    }

    type KeychainRef =
      | { kind: "keychain"; id: string }   // OS keychain entry, vault-scoped
      | { kind: "inline"; value: string }; // plaintext fallback

Example:

    {
      "instanceId": "anthropic-prod-uuid",
      "providerTypeId": "anthropic",
      "displayName": "Anthropic (prod)",
      "apiKey": { "kind": "keychain", "id": "provider-anthropic-prod-uuid-apiKey" },
      "addedAt": 1716000000000
    }

### 4.3 CatalogModel read-only

    interface CatalogModel {
      id: string;                          // wire form, e.g. "claude-sonnet-4-5"
      displayName: string;
      family?: string;                     // "Claude 4"
      modalities: { input: string[]; output: string[] };
      limits: { context: number; output: number };
      capabilities: {
        reasoning?: boolean;
        toolCall?: boolean;
        attachment?: boolean;
        temperature?: boolean;
      };
      cost?: { input: number; output: number; cacheRead?: number; cacheWrite?: number };
      knowledge?: string;
      releaseDate?: string;
      lastUpdated?: string;
      openWeights?: boolean;
    }

### 4.4 CustomModel persisted

For self-hosted endpoints with no catalog. Attached to a ProviderInstance, not a ProviderType.

    interface CustomModel {
      instanceId: string;                  // FK to ProviderInstance (must be synthetic-typed)
      modelId: string;                     // "llama3.3:70b"
      displayName: string;
      declaredCapabilities?: { reasoning?: boolean; toolCall?: boolean; attachment?: boolean };
      contextLimit?: number;
      extras?: Record<string, unknown>;    // ollama numCtx, etc.
      addedAt: number;
    }

### 4.5 ByokEnrollment persisted unit of identity

    interface ByokEnrollment {
      instanceId: string;
      modelId: string;                     // catalog model OR custom model (never both)
      displayName: string;
      enrolledAt: number;
      overrides?: Record<string, unknown>; // per-model knobs — NOT temperature/maxTokens
      lastVerifiedAt?: number;
      lastVerificationError?: string;
    }

Stable enrollment key: `enrollmentRef = ${instanceId}::${modelId}`. Used everywhere downstream as a single string handle. Double colon avoids collisions with ids that contain `/`.

Multi-instance example — same model enrolled under two Anthropic credentials:

    [
      { "instanceId": "anthropic-prod-uuid", "modelId": "claude-sonnet-4-5", ... },
      { "instanceId": "anthropic-staging-uuid", "modelId": "claude-sonnet-4-5", ... }
    ]

### 4.6 ConsumerConfig persisted

    type ConsumerId =
      | "chat" | "vault-qa" | "project" | "copilot-plus" | "quick-chat"
      | "agent:opencode" | "agent:claude-code" | "agent:codex"
      | `command:${string}`;

    interface ConsumerConfig {
      consumerId: ConsumerId;
      enabledModels: ConsumerModelRef[];   // empty = "use default heuristic"
      defaultModel?: ConsumerModelRef | null;
    }

### 4.7 ConsumerModelRef

    type ConsumerModelRef =
      | { source: "byok"; enrollmentRef: string }
      | { source: "backend-bundled"; backendId: BackendId; backendModelId: string }
      | { source: "copilot-plus"; modelId: string };

    type BackendId = "opencode" | "claude-code" | "codex";

Examples:

    // Simple Chat enables GPT-5 via the user's only OpenAI instance
    { "source": "byok", "enrollmentRef": "openai-uuid-1::gpt-5" }

    // OpenCode agent enables an OpenCode-bundled model
    { "source": "backend-bundled", "backendId": "opencode", "backendModelId": "bigpickle/big-pickle" }

    // Quick Chat enables a Copilot Plus hosted model
    { "source": "copilot-plus", "modelId": "copilot-plus-flash" }

    // OpenCode agent enables Sonnet via the prod Anthropic instance (NOT staging)
    { "source": "byok", "enrollmentRef": "anthropic-prod-uuid::claude-sonnet-4-5" }

### 4.8 BackendInventory runtime-only

Held in memory; never persisted.

    interface BackendInventory {
      backendId: BackendId;
      models: BackendModelEntry[];   // bundled + BYOK-bridged + hosted
    }

    interface BackendModelEntry {
      backendModelId: string;
      origin:
        | { kind: "bundled" }
        | { kind: "byok"; enrollmentRef: string }
        | { kind: "copilot-plus" };
      displayName?: string;
      capabilities?: CatalogModel["capabilities"];
    }

## 5. AdapterKind rationale

`AdapterKind` is not the same thing as `ProviderType.id`. The id is the catalog handle (one per `models.dev` entry, potentially 30+). The adapter is the protocol / SDK we wire to internally — closed at six values.

AdapterKind | LangChain path | OpenCode path
--- | --- | ---
`anthropic` | `@langchain/anthropic` (`ChatAnthropic`) | `provider.set type=anthropic`
`openai-compatible` | `@langchain/openai` + custom `baseUrl` | `provider.set type=openai-compatible`
`google` | `@langchain/google-genai` | `provider.set type=google`
`azure` | `@langchain/openai` (Azure path) + deployment knobs | `provider.set type=azure`
`bedrock` | `@langchain/aws` | `provider.set type=bedrock`
`github-copilot` | Custom bridge (OAuth-based) | `provider.set type=github-copilot`

Why six and not more. Each one is real wiring in one or both pipelines. Adding an AdapterKind requires shipping LangChain integration code AND ensuring (or explicitly disclaiming via `pipelines`) OpenCode support. We don't multiply adapter kinds just because a new provider exists.

Why we still surface dozens of providers. The catalog (e.g. Mistral, Groq, DeepSeek, Together, Perplexity, xAI, OpenRouter, SiliconFlow, …) maps most providers onto `openai-compatible` with their own `defaultBaseUrl`. So the Add Provider list mirrors the full `models.dev` catalog; what differs is the adapter the runtime uses behind the scenes.

Surfacing, not silencing. A provider whose adapter we don't support yet should appear in the Add Provider UI with a "not yet supported in this plugin" affordance — not be silently filtered out. The user should know the gap exists.

## 6. Pipeline compatibility

`ProviderType.pipelines = { langchain, opencode }` is load-bearing. It gates which consumer pickers a provider's enrollments can appear in.

### 6.1 Consumer ↔ pipeline mapping

ConsumerId | Pipeline
--- | ---
`chat` | `langchain`
`vault-qa` | `langchain`
`project` | `langchain`
`copilot-plus` | `langchain`
`quick-chat` | `langchain`
`command:*` | `langchain`
`agent:opencode` | `opencode`
`agent:claude-code` | (subscription)
`agent:codex` | (subscription)

Subscription consumers don't accept BYOK at all; their `enabledModels` are all `backend-bundled` refs.

### 6.2 What causes a flag to flip false

#### pipelines.langchain = false

- No LangChain SDK package available; not OpenAI-compatible enough for `@langchain/openai`.
- CLI-bound session token or hardware-token auth that the in-process LangChain class can't bind to.
- Provider is bundled end-to-end by OpenCode (SDK + OAuth) and exposing it via LangChain would require shipping its SDK in the plugin bundle.

#### pipelines.opencode = false

- OpenCode's `provider.set` config doesn't recognize the protocol — LangChain works; OpenCode upstream hasn't wired it up.
- Per-request state (custom headers, signed URLs) the byokBridge can't inject into OpenCode's HTTP client.
- Local endpoint OpenCode's subprocess can't reach (rare).

### 6.3 Worked examples

ProviderType.id | adapter | langchain | opencode | notes
--- | --- | --- | --- | ---
`anthropic` | `anthropic` | ✓ | ✓ |
`openai` | `openai-compatible` | ✓ | ✓ |
`google` | `google` | ✓ | ✓ |
`mistral` | `openai-compatible` | ✓ | ✓ |
`groq` | `openai-compatible` | ✓ | ✓ |
`openrouter` | `openai-compatible` | ✓ | ✓ |
`bedrock` | `bedrock` | ✓ | ✓ | AWS SDK auth in both
`azure` | `azure` | ✓ | ✓ | deployment name in `extras`
`github-copilot` | `github-copilot` | ✓ | ✓ | OAuth refresh handled in both
`ollama` | `openai-compatible` | ✓ | ✓ | synthetic; CustomModels per instance
hypothetical | `openai-compatible` | ✓ | ✗ | LangChain works; OpenCode unaware
hypothetical | (custom OAuth) | ✗ | ✓ | OpenCode CLI manages OAuth; can't replicate in-process

## 7. ER diagram

                                ┌───────────────────────────────┐
                                │ ProviderType (read-only)       │
                                │   id (PK)                      │
                                │   adapter                      │
                                │   pipelines {langchain,opencode}│
                                │   defaultBaseUrl               │
                                │   models: {id → CatalogModel}  │
                                └───────────┬───────────────────┘
                                            │ 1
                                            │
                                            │ 1..n
                                ┌───────────▼───────────┐
                                │ ProviderInstance       │
                                │   instanceId (PK)      │
                                │   providerTypeId (FK)  │
                                │   displayName          │
                                │   baseUrl, apiKey,extras│
                                └───┬────────────┬──────┘
                                    │            │
                                  1 │          1 │
                                    │            │
                              0..n  │      0..n  │
                  ┌─────────────────▼──┐   ┌────▼──────────────────┐
                  │ CustomModel         │   │ ByokEnrollment        │
                  │   instanceId (FK)   │   │   instanceId (FK)     │
                  │   modelId (PK part) │   │   modelId (PK part)   │
                  │   declaredCaps      │   │   displayName         │
                  └────────┬────────────┘   │   overrides           │
                           │                │                       │
                           └─── resolves ──►│   (modelId points     │
                                            │    at catalog or      │
                                            │    customModel)       │
                                            └───────────┬───────────┘
                                                        │ 1
                                                        │
                                                  0..n  │
                           ┌────────────────────────────▼───────────┐
                           │ ConsumerConfig                          │
                           │   consumerId (PK)                       │
                           │   enabledModels[]: ConsumerModelRef     │
                           │   defaultModel?: ConsumerModelRef       │
                           └────────────────────────────────────────┘
                                    ▲
                                    │ runtime-only intersection
                           ┌────────┴────────────────────────────────┐
                           │ BackendInventory (in-memory)            │
                           │   per BackendId: bundled + bridged BYOK │
                           │                  + Copilot Plus models  │
                           └─────────────────────────────────────────┘

## 8. Invariants

1. `enrollment.instanceId ∈ keys(providers)`. (write path, migration)
2. `(instanceId, modelId)` pair resolves either via the instance's ProviderType catalog OR via a `CustomModel` row — never both. (write path)
3. At most one `ByokEnrollment` per `(instanceId, modelId)`. (write path)
4. At most one `CustomModel` per `(instanceId, modelId)`. (write path)
5. If `ProviderType.auth = "api-key"`, `ProviderInstance.apiKey` is non-null; otherwise the provider is "incomplete" and its enrollments are greyed in pickers. (runtime)
6. Every `ConsumerModelRef` with `source: "byok"` points at a present enrollment. Broken refs surface in the UI — never silently pruned. (runtime)
7. When a consumer's `defaultModel` is removed from `enabledModels`, `defaultModel` is set to `null`. (write path)
8. Deleting a `ProviderInstance` cascades to its enrollments and custom models. Consumer refs pointing at those enrollments become broken refs (surfaced, not pruned). (write path)
9. A consumer whose pipeline is `langchain` only renders enrollments whose `ProviderType.pipelines.langchain = true`; same for `opencode`. Ineligible enrollments are shown as ineligible in the BYOK panel with a reason — not hidden. (runtime)
10. NO `providerTypeId` uniqueness constraint across `providers`. Multi-instance is the whole point. (explicit non-constraint)

## 9. Resolution traces

### Trace A — Simple Chat, single OpenAI instance

    1. User opens BYOK panel
       ── catalog shows OpenAI ProviderType.models

    2. User adds OpenAI provider
       ── providers["openai-uuid-1"] = {
            instanceId: "openai-uuid-1",
            providerTypeId: "openai",
            apiKey: { kind: "keychain", id: "provider-openai-uuid-1-apiKey" }
          }

    3. User enrolls "gpt-5"
       ── enrollments += { instanceId: "openai-uuid-1", modelId: "gpt-5" }

    4. User enables it in Simple Chat
       ── consumers["chat"] = {
            consumerId: "chat",
            enabledModels: [{ source: "byok", enrollmentRef: "openai-uuid-1::gpt-5" }],
            defaultModel:   { source: "byok", enrollmentRef: "openai-uuid-1::gpt-5" }
          }

    5. Picker renders
       ── iterate enabledModels → resolve enrollmentRef
       ── join with catalog → render "GPT-5"

### Trace B — OpenCode-bundled (not BYOK)

    1. Plugin starts → OpenCode CLI launched
       ── BackendInventory["opencode"].models includes
            { backendModelId: "bigpickle/big-pickle", origin: { kind: "bundled" }, ... }

    2. User enables it in OpenCode consumer
       ── consumers["agent:opencode"].enabledModels += {
            source: "backend-bundled",
            backendId: "opencode",
            backendModelId: "bigpickle/big-pickle"
          }

    3. Picker renders
       ── resolve against live BackendInventory["opencode"] by backendModelId

### Trace C — BYOK shared with OpenCode (byokBridge)

    Pre-existing:
      providers["anthropic-prod-uuid"] = { providerTypeId: "anthropic", displayName: "Anthropic (prod)", apiKey: K1 }
      enrollments += { instanceId: "anthropic-prod-uuid", modelId: "claude-sonnet-4-5" }

    1. User enables it in OpenCode consumer
       ── consumers["agent:opencode"].enabledModels += {
            source: "byok",
            enrollmentRef: "anthropic-prod-uuid::claude-sonnet-4-5"
          }

    2. byokBridge runs at session start
       ── registers prod Anthropic creds with OpenCode (one bridged provider per ProviderInstance, keyed by instanceId)
       ── OpenCode now reports a model with origin: { kind: "byok", enrollmentRef: "anthropic-prod-uuid::claude-sonnet-4-5" }

    3. Picker reconciles
       ── matches consumer's "byok" ref to inventory entry by enrollmentRef
       ── if user also enabled staging-instance enrollment, both render as separate rows
          disambiguated by parent ProviderInstance.displayName

### Trace D — Two Anthropic keys (multi-instance, catalog-backed)

    1. providers["anthropic-prod-uuid"] = { ..., displayName: "Anthropic (prod)", apiKey: K1 }

    2. User clicks "Add Provider → Anthropic" AGAIN
       ── BYOK UI does NOT filter Anthropic out (multi-instance is allowed)
       ── providers["anthropic-staging-uuid"] = { ..., displayName: "Anthropic (staging)", apiKey: K2 }

    3. User enrolls Sonnet 4.5 on both instances
       ── enrollments += { instanceId: "anthropic-prod-uuid", modelId: "claude-sonnet-4-5" }
       ── enrollments += { instanceId: "anthropic-staging-uuid", modelId: "claude-sonnet-4-5" }

    4. Simple Chat picker
       ── shows TWO rows: "Claude Sonnet 4.5 (prod)" and "Claude Sonnet 4.5 (staging)"

    5. Delete staging instance
       ── cascade: enrollment removed
       ── any consumer ref pointing at it becomes a broken ref (surfaced in UI)
       ── prod enrollment untouched

### Trace E — Custom providers, multi-instance, custom models

    1. providers["ollama-uuid-1"] = {
         providerTypeId: "ollama",
         displayName: "Ollama (laptop)",
         baseUrl: "http://localhost:11434",
         apiKey: null
       }

    2. providers["ollama-uuid-2"] = {
         providerTypeId: "ollama",
         displayName: "Ollama (workstation)",
         baseUrl: "http://192.168.1.50:11434",
         apiKey: null
       }

    3. customModels += { instanceId: "ollama-uuid-1", modelId: "llama3.3:70b" }
       customModels += { instanceId: "ollama-uuid-2", modelId: "mistral:7b" }
       (each instance owns its own model list)

    4. enrollments += two rows, one per instance
       (from here, same as Trace A)

## 10. Per-consumer picker formulas

    visibleEntries(consumerId) =
        let pipeline = pipelineOf(consumerId)
        let inventory = (pipeline === "opencode")
            ? BackendInventory[backendIdOf(consumerId)]
            : null

        enabled = consumer.enabledModels        // empty ⇒ default-selection heuristic

        for each ref in enabled:
            case ref.source:
              "byok":
                let e = enrollments[ref.enrollmentRef]
                let p = providers[e.instanceId]
                let t = providerTypes[p.providerTypeId]
                require t.pipelines[pipeline]
                require p.apiKey != null OR t.auth == "none"
                if pipeline == "opencode":
                  find matching inventory entry by enrollmentRef
                emit entry

              "backend-bundled":
                require inventory != null && inventory.includes(ref.backendModelId)
                emit entry

              "copilot-plus":
                require copilotPlusCatalog.includes(ref.modelId)
                emit entry

No capability filtering. No runtime "this model doesn't support tool_call so hide it" check — runtime errors surface incompatibility via the normal error path.

## 11. Explicit non-goals

- No global chat knobs. No `temperature` / `maxTokens` / `reasoningEffort` / `verbosity` / `topP` / `frequencyPenalty` in the data model. Adapter SDK defaults are used. If user-tunable knobs are ever needed, they belong elsewhere (per-invocation, per-skill, per-command).
- No per-agent capability filtering at picker render.
- No per-model `temperature` / `maxTokens` / `topP`. Same reason.
- No `enabled` flag on `ByokEnrollment`. "Enabled for which consumer?" is ambiguous; per-consumer `enabledModels` carries the meaning.
- No `capabilities` array on `ByokEnrollment`. Read from catalog or `CustomModel.declaredCapabilities` at query time.
- No chat-embedding mixing in `enrollments` at first — embeddings can fold in once the carve-out resolves.

## 12. Open questions

1. Embeddings unification timing. Stay legacy now, fold in later?
2. Project-level chain default storage. Currently `ProjectConfig.projectModelKey` is a wire-form string. Becomes `ProjectConfig.defaultModelRef: ConsumerModelRef | null`? Or does each project override `consumers["project"]` wholesale?
3. Per-invocation knobs. If we ever surface a user-facing temperature slider, where? Per-message, per-skill, per-command? Spec says "not in the data model" — does that hold?
4. "Not yet supported in this plugin" affordance for catalog providers we don't have an adapter for. Disabled list item with tooltip? Separate section?
5. Per-pipeline-only badging. Do we put "works with chat only" / "works with OpenCode only" copy on the BYOK enrollment row? Or only at the provider level?
6. backend-bundled ref staleness. OpenCode releases change bundled lists. Broken ref behavior or auto-prune?
7. Custom command consumers. One `ConsumerConfig` per command? Or fold into a single record keyed by command id?

For implementation handoff: see `designdocs/MODEL_DATA_MODEL_SPEC.md` — same data model in canonical form, plus a reconciliation appendix diffing this design against the current `src/modelManagement/` branch.