# Model Provider Settings Design

## Goal

Add a dedicated model provider management page to the desktop UI so users can configure arbitrary OpenAI- and Anthropic-compatible gateways without returning to the terminal or manually editing pi configuration files.

The page manages provider and model definitions in `~/.pi/agent/models.json` and stores credentials in `~/.pi/agent/auth.json`. It does not expose stored secrets to the webview.

## Scope

The first version supports:

- OpenAI Chat Completions (`openai-completions`)
- OpenAI Responses (`openai-responses`)
- Anthropic Messages (`anthropic-messages`)
- Custom provider ID, display name, base URL, API type, and standard authentication behavior
- API key storage in `auth.json`
- Custom request headers, including sensitive values stored in provider-scoped auth environment values
- Fetching model IDs from a compatible remote model-list endpoint
- Manual model creation when a provider does not expose a compatible endpoint
- Per-model editing for name, reasoning, input types, context window, max output tokens, and costs
- Immediate availability through the desktop model selector after a successful save and registry refresh

The first version does not include:

- A raw JSON editor
- OAuth token editing or management
- Generic bridging of all `ctx.ui` terminal dialogs into the webview
- Automatic inference of model capabilities beyond values returned by a compatible endpoint

## Entry Points

All entry points open the same model provider settings page:

- Settings -> Model Providers
- A settings icon in the model selector
- `/setup-model` entered in the desktop UI
- A Configure Provider action shown after a missing-auth model selection error

The terminal command may retain its existing behavior when entered directly in the terminal. The desktop input route intercepts `/setup-model` and opens the web page rather than starting a terminal-only interaction flow.

## Page Structure

The page is a work-focused settings view with a provider list on the left and the selected provider editor on the right.

The provider list shows:

- Display name or provider ID
- API type
- Number of configured models
- Authentication status

It never displays API keys or sensitive header values.

The provider editor contains:

- Provider ID, immutable after creation
- Display name
- Base URL
- API type selector
- Masked API key input; blank means retain the existing credential
- Standard bearer authentication toggle
- Custom header rows with name, value, sensitive toggle, and delete action
- Searchable model list with select all, add manually, remove, and fetch-from-provider actions
- Expandable model details
- Save, cancel, and delete provider actions

Provider deletion requires confirmation. A separate "also delete saved API key" checkbox is present and unchecked by default.

## Model Import

The fetch action uses unsaved form values, including a newly entered API key and header values, so users can verify a provider before storing credentials.

The default model-list URL is `{normalizedBaseUrl}/models`. Duplicate slashes are normalized without stripping a required path prefix from the base URL.

The parser accepts common OpenAI-compatible list shapes, primarily `{ data: [{ id }] }`, as well as a top-level array of model objects or IDs. Invalid entries without a non-empty ID are skipped and counted.

Anthropic-compatible providers are queried through the same configurable model-list path when available. Failure does not prevent manual model entry.

Imported models receive editable defaults:

- `contextWindow: 128000`
- `maxTokens: 16384`
- `input: ["text"]`
- `reasoning: false`
- all cost fields set to `0`

Existing model edits are retained when a fetched ID already exists.

## Backend Protocol

The extension and webview use explicit messages for this feature:

- `get-model-providers`: request redacted provider data and auth status
- `model-providers`: return redacted provider data
- `fetch-provider-models`: fetch model IDs using temporary form configuration
- `provider-models-result`: return fetched IDs, skipped count, or a sanitized error
- `save-model-provider`: validate and save one provider and its credentials
- `delete-model-provider`: remove one provider and optionally its credential
- `model-provider-result`: report save or delete status
- `open-model-settings`: instruct the webview to navigate to the provider page

Messages use request IDs so simultaneous or stale fetch/save responses can be matched safely.

## Storage

Provider definitions and model metadata are stored in `~/.pi/agent/models.json` under `providers[providerId]`.

API keys are stored in `~/.pi/agent/auth.json` as:

```json
{
  "provider-id": {
    "type": "api_key",
    "key": "secret"
  }
}
```

The provider's `models.json` entry omits `apiKey` when auth is supplied by `auth.json`.

Sensitive custom headers are stored in the credential's provider-scoped `env` map. The corresponding `models.json` header value is an environment reference:

```json
{
  "headers": {
    "cf-aig-authorization": "$PI_MODEL_PROVIDER_ID_CF_AIG_AUTHORIZATION"
  }
}
```

Non-sensitive header values are stored directly in `models.json`.

Saving reads both files again immediately before writing and merges only the selected provider and credential. Other providers and unrecognized fields are preserved. Writes use temporary files and atomic replacement. The implementation keeps in-memory snapshots and attempts rollback if one of the two file updates fails.

## Registry Refresh

After saving or deleting a provider, the extension refreshes the active model registry using a supported public API where available. If the installed pi version does not expose a direct refresh method, the extension uses the least disruptive supported reload path and reports that behavior clearly.

The current model remains selected when it is still valid. Removing the active model does not silently choose a replacement; the UI prompts the user to select another model.

Opening `/model` or the desktop model selector must reflect saved changes without restarting pi.

## Validation And Security

The backend is the security boundary. It validates all webview input and never trusts client-side checks alone.

Rules include:

- Provider IDs use a bounded conservative character set and length.
- Base URLs must parse and use `http:` or `https:`.
- API type must be one of the three supported values.
- Header names must be valid HTTP token names and bounded in length.
- Model IDs, names, numeric limits, and costs are bounded and type checked.
- Duplicate provider IDs, model IDs, and case-insensitive header names are handled deterministically.
- Fetches have a timeout and maximum response size.
- Redirect behavior is bounded and only HTTP(S) targets are accepted.
- API keys and sensitive values are never returned to the webview after storage.
- The webview receives only `configured: true | false` for stored authentication.
- Blank secret fields retain existing values. Clearing credentials requires a distinct confirmed action.
- Logs and errors redact request headers, credential-like strings, and submitted secret values.
- Remote response bodies are sanitized and truncated before display.
- OAuth credentials are not editable. OAuth-backed providers show status and direct users to `/login`.

## Error Handling

Errors stay in the settings workflow rather than appearing as chat messages.

- Invalid `models.json` or `auth.json`: show the path and parse error, keep the page read-only, and do not overwrite the file.
- Fetch timeout or network failure: retain form state and show a concise category-specific error.
- HTTP 401/403: identify authentication failure without echoing response credentials.
- Unsupported response shape: explain that manual model entry remains available.
- Oversized response: abort and report the configured limit.
- Partially invalid model list: import valid entries and report the skipped count.
- Save failure: report which stage failed and whether rollback succeeded.
- Registry refresh failure after successful persistence: report that configuration was saved and instruct the user to reopen `/model` or run `/reload`.

## Testing

Backend tests cover:

- Provider ID, URL, API type, header name, and model field validation
- Preservation of other providers and unknown JSON fields during merge
- API keys never being written into `models.json`
- Secrets never being returned in outbound window messages
- Blank credentials retaining old values
- Explicit credential clearing
- Provider deletion with credentials retained by default and optionally deleted
- Sensitive headers stored in auth-scoped environment values and referenced from `models.json`
- OpenAI-compatible and common alternate model-list response shapes
- Timeouts, oversized responses, invalid JSON, 401/403 responses, and error redaction
- Save rollback behavior
- Registry refresh behavior

Frontend tests cover:

- All four entry points opening the same page
- Provider create, edit, save, cancel, and delete states
- Fetching and merging model IDs
- Editable imported defaults
- Secret fields remaining blank for existing credentials
- Missing-auth errors linking to provider settings
- Long provider and model IDs not overflowing controls

Runtime verification includes desktop and mobile-sized screenshots, checking that dialogs, lists, form controls, and expanded model details do not overlap.
