# Model Provider Settings Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Add a secure desktop model-provider settings page for arbitrary OpenAI- and Anthropic-compatible gateways, with credentials in `auth.json`, models in `models.json`, remote model discovery, and immediate registry refresh.

**Architecture:** Put validation, redaction, model-list parsing, and `models.json` merge logic in a small dependency-free module so it can be tested directly. Keep privileged filesystem, auth storage, network, and registry operations in `index.ts`; keep view state and rendering in `web/app.js`. The webview receives only redacted provider data and communicates through explicit request-ID-based bridge messages.

**Tech Stack:** TypeScript pi extension, Node.js filesystem/fetch APIs, pi `ModelRegistry` and `AuthStorage` public APIs, vanilla JavaScript webview, inline HTML/CSS, Node assertion-based verification scripts.

---

### Task 1: Provider Configuration Validation And Redaction

**Files:**
- Create: `model-provider-utils.js`
- Create: `scripts/test-model-provider-utils.mjs`
- Modify: `package.json`

**Step 1: Write the failing utility tests**

Create tests covering:

```js
import assert from "node:assert/strict";
import {
  validateProviderDraft,
  redactProviderConfig,
  sensitiveHeaderEnvName,
} from "../model-provider-utils.js";

assert.equal(validateProviderDraft({
  id: "my-proxy",
  baseUrl: "https://proxy.example/v1",
  api: "openai-completions",
  headers: [],
  models: [{ id: "gpt-test", contextWindow: 128000, maxTokens: 16384 }],
}).ok, true);

assert.equal(validateProviderDraft({ id: "../bad", baseUrl: "file:///tmp/x", api: "bad", headers: [], models: [] }).ok, false);
assert.equal(sensitiveHeaderEnvName("my-proxy", "cf-aig-authorization"), "PI_MODEL_MY_PROXY_CF_AIG_AUTHORIZATION");
assert.deepEqual(redactProviderConfig("my-proxy", {
  baseUrl: "https://proxy.example/v1",
  api: "openai-completions",
  headers: { Authorization: "$SECRET", "x-region": "us" },
  models: [{ id: "gpt-test" }],
}, { configured: true, source: "stored" }), {
  id: "my-proxy",
  baseUrl: "https://proxy.example/v1",
  api: "openai-completions",
  headers: [
    { name: "Authorization", value: "", sensitive: true, configured: true },
    { name: "x-region", value: "us", sensitive: false, configured: true },
  ],
  models: [{ id: "gpt-test" }],
  auth: { configured: true, source: "stored" },
});
```

Also test duplicate model IDs, case-insensitive duplicate headers, negative costs, oversized strings, invalid numeric limits, and all three supported API values.

**Step 2: Run the test to verify it fails**

Run: `node scripts/test-model-provider-utils.mjs`

Expected: FAIL because `model-provider-utils.js` does not exist.

**Step 3: Implement minimal validation and redaction helpers**

Export:

```js
export const SUPPORTED_MODEL_APIS = new Set([
  "openai-completions",
  "openai-responses",
  "anthropic-messages",
]);

export function validateProviderDraft(draft) { /* return { ok, value?, errors } */ }
export function sensitiveHeaderEnvName(providerId, headerName) { /* normalized bounded env name */ }
export function redactProviderConfig(providerId, config, authStatus) { /* no stored secrets */ }
```

Use `new URL()`, allow only `http:` and `https:`, use an HTTP token regex for header names, and return field-keyed errors suitable for inline display. Preserve supported optional model fields while applying defaults only to missing fields.

**Step 4: Add the test command and run it**

Add `verify:model-provider-utils` to `package.json` and include it in `check`.

Run: `npm run verify:model-provider-utils`

Expected: PASS.

**Step 5: Commit**

```bash
git add model-provider-utils.js scripts/test-model-provider-utils.mjs package.json
git commit -m "feat: validate model provider configuration"
```

### Task 2: Model List Parsing And Sanitized Fetch Errors

**Files:**
- Modify: `model-provider-utils.js`
- Modify: `scripts/test-model-provider-utils.mjs`

**Step 1: Write failing tests**

Add tests for:

```js
assert.deepEqual(parseModelListResponse({ data: [{ id: "gpt-a" }, { id: "gpt-b" }] }), {
  ids: ["gpt-a", "gpt-b"],
  skipped: 0,
});
assert.deepEqual(parseModelListResponse([{ id: "claude-a" }, "claude-b", {}]), {
  ids: ["claude-a", "claude-b"],
  skipped: 1,
});
assert.equal(buildModelsUrl("https://proxy.example/v1"), "https://proxy.example/v1/models");
assert.equal(sanitizeProviderError("Bearer sk-secret-value failed", ["sk-secret-value"]), "Bearer [REDACTED] failed");
```

Test deduplication, sorting, unsupported shapes, path-prefix preservation, trailing slash handling, long response truncation, API-key redaction, and authorization/header-value redaction.

**Step 2: Run tests and confirm failure**

Run: `npm run verify:model-provider-utils`

Expected: FAIL on missing parser/fetch helper exports.

**Step 3: Implement helpers**

Export:

```js
export function buildModelsUrl(baseUrl) { /* URL-safe append of models */ }
export function parseModelListResponse(payload) { /* { ids, skipped } */ }
export function sanitizeProviderError(value, secrets = []) { /* redact and truncate */ }
export function mergeFetchedModels(existing, ids) { /* retain edits, default new models */ }
```

New models receive the approved defaults; existing IDs retain all edited fields.

**Step 4: Run tests**

Run: `npm run verify:model-provider-utils`

Expected: PASS.

**Step 5: Commit**

```bash
git add model-provider-utils.js scripts/test-model-provider-utils.mjs
git commit -m "feat: parse compatible provider model lists"
```

### Task 3: Safe `models.json` Merge And Persistence Helpers

**Files:**
- Modify: `model-provider-utils.js`
- Modify: `scripts/test-model-provider-utils.mjs`

**Step 1: Write failing tests**

Test that a provider merge:

- Preserves top-level unknown fields.
- Preserves other providers unchanged.
- Preserves unknown fields on the edited provider where they do not conflict with form-owned fields.
- Never writes `apiKey` from a draft.
- Writes non-sensitive header values directly.
- Writes sensitive headers as `$PI_MODEL_...` references.
- Produces a provider-scoped auth `env` patch without exposing it in redacted output.
- Leaves an existing API key unchanged when the submitted key is blank.
- Removes only the selected provider on delete.

Use temporary in-memory objects for this task; filesystem behavior remains in `index.ts`.

**Step 2: Run tests and confirm failure**

Run: `npm run verify:model-provider-utils`

Expected: FAIL on missing merge exports.

**Step 3: Implement pure merge helpers**

Export:

```js
export function mergeProviderDocument(document, draft) {
  // returns { document, sensitiveEnv }
}
export function removeProviderFromDocument(document, providerId) { /* immutable merge */ }
```

Only these fields are form-owned: `baseUrl`, `api`, `authHeader`, `headers`, `models`, and optional display metadata chosen by the implementation. Keep unknown compatibility fields unless the UI explicitly owns them.

**Step 4: Run tests**

Run: `npm run verify:model-provider-utils`

Expected: PASS.

**Step 5: Commit**

```bash
git add model-provider-utils.js scripts/test-model-provider-utils.mjs
git commit -m "feat: merge model provider documents safely"
```

### Task 4: Backend Provider Read And Fetch Protocol

**Files:**
- Modify: `index.ts`
- Create: `scripts/verify-model-provider-backend.mjs`
- Modify: `package.json`

**Step 1: Write a failing source-level/backend verification script**

Verify that `index.ts` handles `get-model-providers` and `fetch-provider-models`, imports the utility module, uses `modelRegistry.modelsJsonPath`, uses `authStorage.getAuthStatus`, applies an abort timeout, limits the response body, and never serializes `authStorage.get()` or `getAll()` to the window.

Also test extracted pure request construction through `model-provider-utils.js` where practical.

**Step 2: Run verification and confirm failure**

Run: `node scripts/verify-model-provider-backend.mjs`

Expected: FAIL because the handlers are absent.

**Step 3: Implement redacted provider loading**

In `handleWindowMessage`:

```ts
case "get-model-providers": {
  const registry = lastCtx?.modelRegistry;
  const modelsPath = registry?.modelsJsonPath;
  // Parse document, report readOnly parse error, redact each provider,
  // attach registry.authStorage.getAuthStatus(providerId), send model-providers.
}
```

If the file does not exist, return an empty writable document. If parsing fails, return the path and sanitized error with `readOnly: true`.

**Step 4: Implement bounded remote fetch**

For `fetch-provider-models`:

- Validate the temporary draft.
- Resolve a submitted API key without storing it.
- Build standard and custom headers.
- Use `AbortSignal.timeout()` or an `AbortController` timer.
- Stream/read with an explicit byte cap instead of unbounded `response.text()`.
- Parse JSON and model IDs.
- Return request ID, IDs, skipped count, and sanitized errors.

Do not log request headers or draft secrets.

**Step 5: Run checks**

Run:

```bash
node scripts/verify-model-provider-backend.mjs
npm run check
```

Expected: both PASS.

**Step 6: Commit**

```bash
git add index.ts scripts/verify-model-provider-backend.mjs package.json
git commit -m "feat: expose provider discovery backend"
```

### Task 5: Backend Save, Delete, Rollback, And Registry Refresh

**Files:**
- Modify: `index.ts`
- Modify: `scripts/verify-model-provider-backend.mjs`

**Step 1: Extend failing verification**

Require handlers for `save-model-provider`, `delete-model-provider`, and explicit credential clearing. Verify use of:

```ts
lastCtx.modelRegistry.authStorage.set(providerId, credential);
lastCtx.modelRegistry.authStorage.remove(providerId);
lastCtx.modelRegistry.refresh();
```

Require temporary-file writes, restrictive permissions for newly created config files, and cleanup/rollback branches.

**Step 2: Run verification and confirm failure**

Run: `node scripts/verify-model-provider-backend.mjs`

Expected: FAIL on absent save/delete behavior.

**Step 3: Implement atomic model document writes**

Add focused local helpers in `index.ts` for:

```ts
function readJsonDocument(path: string): unknown;
function writeJsonAtomic(path: string, value: unknown): void;
```

Create parent directories as needed. Write a sibling temp file with mode `0o600`, flush/close it, then rename over the destination. Preserve the original content in memory for rollback.

**Step 4: Implement credential updates through `AuthStorage`**

Use `authStorage.get(providerId)` only inside the privileged backend to preserve existing API-key `env` values. Rules:

- Non-empty submitted key replaces the existing API-key credential key.
- Blank key retains it.
- Explicit clear removes the credential only after confirmation from the UI.
- OAuth credentials are rejected as non-editable.
- Sensitive header values update provider-scoped `env`.
- Blank sensitive values retain existing env values.

**Step 5: Implement save/delete and refresh**

After persistence:

```ts
lastCtx.modelRegistry.authStorage.reload();
lastCtx.modelRegistry.refresh();
```

Return a redacted result and refreshed provider list. If refresh fails after persistence, return `saved: true`, `refreshed: false`, and a sanitized recovery message. If one persistence stage fails, attempt rollback and report rollback status.

Delete provider config first; remove credentials only when `deleteCredential === true`. If the deleted provider/model is active, send a non-destructive prompt to choose another model.

**Step 6: Run checks**

Run:

```bash
node scripts/verify-model-provider-backend.mjs
npm run check
```

Expected: PASS.

**Step 7: Commit**

```bash
git add index.ts scripts/verify-model-provider-backend.mjs
git commit -m "feat: persist desktop model providers"
```

### Task 6: Frontend Provider State And Main Settings Entry

**Files:**
- Modify: `web/app.js`
- Create: `scripts/test-model-provider-view-state.mjs`
- Modify: `package.json`

**Step 1: Write failing state tests**

Extract/export browser-independent helpers following the existing utility-test pattern. Test:

- Creating a blank provider draft.
- Loading a redacted provider without populating secret inputs.
- Dirty-state detection.
- Merging fetched IDs while retaining edits.
- Save response matching by request ID.
- Provider delete state and default `deleteCredential: false`.

**Step 2: Run and confirm failure**

Run: `node scripts/test-model-provider-view-state.mjs`

Expected: FAIL because helpers are absent.

**Step 3: Implement model provider view state**

Add state fields for providers, selected provider, draft, loading, fetching, saving, errors, request IDs, model search, and expanded model IDs. Keep secrets only in transient draft state and clear them after save/cancel/navigation.

**Step 4: Add settings entry and initial load**

Update `renderSettingsView()` with an unframed Model Providers row/button using a settings/tune icon. Clicking it switches to the provider page and sends `get-model-providers`.

Add English and Chinese i18n keys for page labels, field names, statuses, errors, confirmations, and actions.

**Step 5: Run tests**

Run:

```bash
node scripts/test-model-provider-view-state.mjs
npm run verify:i18n
npm run verify:desktop-language-data
```

Expected: PASS.

**Step 6: Commit**

```bash
git add web/app.js scripts/test-model-provider-view-state.mjs package.json
git commit -m "feat: add model provider settings state"
```

### Task 7: Provider Page UI And Editing Workflow

**Files:**
- Modify: `web/app.js`
- Modify: `web/index.html`
- Modify: `scripts/test-model-provider-view-state.mjs`

**Step 1: Add failing frontend verification cases**

Require rendering and event paths for:

- Provider list and create button
- Provider fields
- API type segmented/select control
- Masked API key input
- Bearer auth toggle
- Header rows with sensitive checkbox
- Model search, fetch, manual add, select/remove
- Expandable model detail fields
- Save/cancel/delete
- Read-only parse-error state

**Step 2: Run tests and confirm failure**

Run: `node scripts/test-model-provider-view-state.mjs`

Expected: FAIL on missing UI hooks.

**Step 3: Implement the responsive page shell**

Use a two-column provider list/editor layout on wide windows and a stacked layout on narrow windows. Keep sections unframed; use compact bordered rows for repeated providers/models. Ensure fixed control heights, bounded columns, wrapping model IDs, and no nested cards.

Use existing Material Symbols icons with tooltips for add, refresh/fetch, settings, expand, and delete actions.

**Step 4: Implement provider and header editing**

Provider ID becomes disabled after creation. Secret fields are always blank after loading stored config. Sensitive headers show configured state without their saved values. Validate locally for quick feedback but treat backend errors as authoritative.

**Step 5: Implement model editing**

Fetched IDs merge into the current draft. Model rows support selection and expansion. Expanded details edit `name`, `reasoning`, text/image input, context window, max tokens, and four cost fields without resizing adjacent rows.

**Step 6: Implement save/delete result handling**

Match responses by request ID, keep the form on errors, clear transient secrets on success, and refresh the displayed list. Delete confirmation includes an unchecked "also delete saved API key" checkbox.

**Step 7: Run frontend and full checks**

Run:

```bash
node scripts/test-model-provider-view-state.mjs
npm run verify:runtime-html
npm run check
```

Expected: PASS.

**Step 8: Commit**

```bash
git add web/app.js web/index.html scripts/test-model-provider-view-state.mjs
git commit -m "feat: build model provider settings page"
```

### Task 8: `/setup-model`, Model Selector, And Missing-Auth Entry Points

**Files:**
- Modify: `web/app.js`
- Modify: `index.ts`
- Modify: `scripts/test-desktop-command-utils.mjs`
- Modify: `scripts/verify-model-provider-backend.mjs`

**Step 1: Write failing entry-point tests**

Test that:

- Desktop input `/setup-model` opens the provider page and does not send a prompt to the agent.
- `/setup-model anything` is rejected or opens the same page consistently.
- The model selector includes a model-provider settings icon.
- Missing-auth model errors include provider ID and a Configure Provider action.
- The action preselects that provider when it exists.

**Step 2: Run tests and confirm failure**

Run:

```bash
node scripts/test-desktop-command-utils.mjs
node scripts/verify-model-provider-backend.mjs
```

Expected: FAIL on missing routes.

**Step 3: Route `/setup-model` in the desktop command path**

Intercept only GUI-originated command submission. Send `open-model-settings` or directly switch the frontend view. Do not alter direct terminal command behavior.

Add `setup-model` to the displayed command metadata if it is not already discovered dynamically.

**Step 4: Add model-selector settings action**

Add an icon button to the selector header. Closing the selector and opening provider settings must be one deterministic action.

**Step 5: Add missing-auth recovery action**

Change the model-selection failure payload to include a stable error code and provider ID rather than parsing human-readable text. Render a Configure Provider button that opens and preselects the provider draft.

**Step 6: Run checks**

Run:

```bash
node scripts/test-desktop-command-utils.mjs
node scripts/verify-model-provider-backend.mjs
npm run check
```

Expected: PASS.

**Step 7: Commit**

```bash
git add web/app.js index.ts scripts/test-desktop-command-utils.mjs scripts/verify-model-provider-backend.mjs
git commit -m "feat: connect model setup entry points"
```

### Task 9: Runtime Visual And Security Verification

**Files:**
- Modify: `scripts/verify-runtime-html.mjs`
- Modify: `README.md`

**Step 1: Extend runtime verification**

Assert that generated runtime HTML contains the provider settings hooks and contains no test secret/API key values. Add checks that sensitive values are absent from serialized initial data.

**Step 2: Run the complete automated suite**

Run: `npm run check`

Expected: PASS with TypeScript, syntax, i18n, utility, backend, frontend, asset, and runtime checks all succeeding.

**Step 3: Start the desktop development workflow**

Use the repository's existing development launcher. If it cannot open in the current environment, start the supported local preview path documented by the repository and report the limitation.

**Step 4: Verify with Playwright at desktop and narrow viewports**

Capture screenshots at approximately `1440x900`, `1024x768`, and `390x844`. Verify:

- Provider list/editor alignment
- No overlapping labels, inputs, dialogs, or action bars
- Long provider/model IDs wrap or truncate with a tooltip
- Expanded model controls remain usable
- Delete confirmation fits narrow screens
- Light and dark themes remain legible
- English and Chinese labels fit controls

Exercise create, fetch error, manual model add, save validation, cancel, and delete-confirmation states without using real credentials in screenshots.

**Step 5: Perform security checks**

Use a disposable test provider and key. Confirm:

- Key appears in `auth.json` only.
- Key does not appear in `models.json`, runtime HTML, console output, screenshots, or bridge responses.
- Sensitive header values appear only in auth-scoped `env`.
- Deleting without the checkbox retains auth.
- Deleting with the checkbox removes auth.

Remove the disposable provider after verification without touching unrelated user configuration.

**Step 6: Update README**

Document the four entry points, supported APIs, storage locations, secret handling, remote model fetch behavior, and manual model fallback. Do not include real credentials.

**Step 7: Run final verification**

Run:

```bash
npm run check
git status --short
git diff --check
```

Expected: all checks PASS; status contains only intentional feature changes and pre-existing unrelated user changes.

**Step 8: Commit**

```bash
git add README.md scripts/verify-runtime-html.mjs
git commit -m "docs: document desktop model provider setup"
```

### Task 10: Final Review

**Files:**
- Review all files changed by Tasks 1-9

**Step 1: Invoke required review skill**

Use `superpowers:requesting-code-review` and review against `docs/plans/2026-07-12-model-provider-settings-design.md`.

**Step 2: Address findings test-first**

For each valid issue, add or strengthen a failing test, implement the smallest correction, rerun the focused test, then rerun `npm run check`.

**Step 3: Verify completion evidence**

Use `superpowers:verification-before-completion`. Record the final `npm run check` result, screenshot viewport coverage, and security verification outcome.

**Step 4: Commit review fixes**

```bash
git add <only reviewed feature files>
git commit -m "fix: address model provider settings review"
```

Skip the commit if review produces no changes.
