# Settings and provider keys

> Provider registration, model selection, API key and OAuth wiring, dynamic provider updates, and 401 stale-provider recovery.

- Repository: PrimeIntellect-ai/prime-agent
- GitHub: https://github.com/PrimeIntellect-ai/prime-agent
- Human docs: https://grok-wiki.com/public/docs/primeintellect-ai-prime-agent-3367c32760b1
- Complete Markdown: https://grok-wiki.com/public/docs/primeintellect-ai-prime-agent-3367c32760b1/llms-full.txt

## Source Files

- `packages/coding-agent/examples/sdk/09-api-keys-and-oauth.ts`
- `packages/coding-agent/examples/sdk/02-custom-model.ts`
- `packages/ai/src/providers/amazon-bedrock.ts`
- `packages/coding-agent/test/agent-session-dynamic-provider.test.ts`
- `packages/coding-agent/test/suite/agent-session-model-extension.test.ts`
- `packages/coding-agent/test/suite/regressions/4491-provider-stale-after-401.test.ts`

---

---
title: "Settings and provider keys"
description: "Provider registration, model selection, API key and OAuth wiring, dynamic provider updates, and 401 stale-provider recovery."
---

Prime Agent resolves models and credentials through `AuthStorage`, `ModelRegistry`, and session settings. Credentials live in `~/.prime/agent/auth.json` (override with `PRIME_AGENT_CODING_AGENT_DIR` / `PI_CODING_AGENT_DIR`), custom providers and model overrides in `models.json`, and defaults such as `defaultProvider` / `defaultModel` in `settings.json`. Runtime paths include CLI flags (`--provider`, `--model`, `--api-key`, `--models`), interactive `/login` and `/model`, extension `pi.registerProvider()`, and the SDK `createAgentSession({ authStorage, modelRegistry, model })` surface.

Architecture is BYOK/BYOC: any provider that speaks a supported API can be wired with your own keys, OAuth, ambient cloud credentials, or a proxy. No hosted model vendor is required.

## Config surfaces

| Surface | Default path | Role |
|---------|--------------|------|
| Agent dir | `~/.prime/agent/` | Root for auth, models, global settings |
| Auth store | `~/.prime/agent/auth.json` | API keys and OAuth tokens (`0600`) |
| Models | `~/.prime/agent/models.json` | Custom providers, overrides, local endpoints |
| Global settings | `~/.prime/agent/settings.json` | Defaults including model/thinking/retry |
| Project settings | `.prime/agent/settings.json` | Project overrides (wins over global) |

Override the agent directory with `PRIME_AGENT_CODING_AGENT_DIR` or `PI_CODING_AGENT_DIR` (tilde expansion supported).

### Model-related settings

| Setting | Type | Default | Description |
|---------|------|---------|-------------|
| `defaultProvider` | string | — | Provider id used when resolving the default model |
| `defaultModel` | string | — | Model id paired with `defaultProvider` |
| `defaultThinkingLevel` | string | `"xhigh"` | `off`, `minimal`, `low`, `medium`, `high`, `xhigh` |
| `retry.enabled` | boolean | `true` | Agent-level auto-retry on transient errors |
| `retry.maxRetries` | number | `3` | Max agent-level retry attempts |
| `retry.baseDelayMs` | number | `2000` | Exponential backoff base (2s, 4s, 8s, …) |

`setModel()` persists the selection via `settingsManager.setDefaultModelAndProvider()` and appends a `model_change` session entry.

## Credential resolution

`AuthStorage.getApiKeyWithSourceToken()` and `ModelRegistry.getApiKeyAndHeaders()` combine several sources. Documented product order:

1. CLI `--api-key` (runtime override; not written to disk)
2. `auth.json` entry (API key or OAuth token)
3. Environment variable for that provider
4. Custom provider keys from `models.json` / registered providers (fallback)

Implementation details that matter in practice:

- **Runtime override** (`setRuntimeApiKey` / `--api-key`) always wins when present and not marked stale.
- **Prime Inference** (`prime-inference`) prefers env `PRIME_API_KEY`, then Prime CLI config (`prime_cli`), then `auth.json`.
- **Other providers** prefer `auth.json` over env vars.
- **Fallback resolver** supplies `models.json` / extension `apiKey` values after AuthStorage sources.
- Ambient cloud auth (Bedrock IAM/profile/bearer, Vertex ADC) is detected by `getEnvApiKey()` and reported as configured without exposing secret material.

```text
Request for provider P
  │
  ├─ runtime override (--api-key / setRuntimeApiKey)
  ├─ [prime-inference only] PRIME_API_KEY → Prime CLI config → auth.json
  ├─ [other providers] auth.json → env API key
  ├─ models.json / registerProvider apiKey fallback
  └─ ambient markers (Bedrock / Vertex) when applicable
```

### Auth status sources

`AuthStatus.source` values used by `getAuthStatus()` / `getProviderAuthStatus()`:

| Source | Meaning |
|--------|---------|
| `runtime` | In-process `--api-key` / `setRuntimeApiKey` |
| `stored` | Entry in `auth.json` |
| `environment` | Provider env var (or ambient credential presence) |
| `prime_cli` | Prime Inference via Prime CLI config |
| `fallback` | Resolver-backed key (typically `models.json`) |
| `models_json_key` / `models_json_command` | Request auth configured on the provider in models/registry |
| `stale` | Previously used credentials marked expired after auth failures |

`configured: true` means a durable or resolvable source exists without refreshing OAuth. `source: "stale", label: "expired"` means the active credential set failed auth and is blocked until replaced.

## API keys

### CLI

```bash
export ANTHROPIC_API_KEY=sk-ant-...
prime-agent --provider anthropic --model claude-sonnet-4-5

# One-shot runtime key (not persisted); requires a resolved model
prime-agent --provider anthropic --model claude-sonnet-4-5 --api-key sk-ant-...
```

`--api-key` without a model resolution path errors: it requires `--model`, `--provider`/`--model`, or `--models`.

### Interactive and auth file

- `/login` stores API keys or OAuth credentials in `auth.json`.
- `/logout` clears the provider entry (and Prime CLI credentials for `prime-inference` when enabled).

```json
{
  "anthropic": { "type": "api_key", "key": "sk-ant-..." },
  "openai": { "type": "api_key", "key": "sk-..." },
  "prime-inference": { "type": "api_key", "key": "..." }
}
```

The `key` field accepts:

| Form | Example |
|------|---------|
| Literal | `"sk-ant-..."` |
| Env var name | `"MY_ANTHROPIC_KEY"` |
| Shell command | `"!op read 'op://vault/item/credential'"` |

Command-backed keys in `auth.json` resolve via `resolveConfigValue` (process-lifetime caching for ordinary resolution; uncached re-resolve after a stale match).

### Representative env → provider map

| Provider id | Environment variable |
|-------------|----------------------|
| `anthropic` | `ANTHROPIC_OAUTH_TOKEN`, then `ANTHROPIC_API_KEY` |
| `openai` | `OPENAI_API_KEY` |
| `prime-inference` | `PRIME_API_KEY` |
| `google` | `GEMINI_API_KEY` |
| `openrouter` | `OPENROUTER_API_KEY` |
| `xai` | `XAI_API_KEY` |
| `azure-openai-responses` | `AZURE_OPENAI_API_KEY` |
| `github-copilot` | `COPILOT_GITHUB_TOKEN`, `GH_TOKEN`, `GITHUB_TOKEN` |

Full mapping lives in `packages/ai/src/env-api-keys.ts`.

### SDK

```typescript
import { AuthStorage, createAgentSession, ModelRegistry, SessionManager } from "@earendil-works/pi-coding-agent";

// Default paths: ~/.prime/agent/auth.json + models.json
const authStorage = AuthStorage.create();
const modelRegistry = ModelRegistry.create(authStorage);

// Custom paths
const customAuth = AuthStorage.create("/tmp/my-app/auth.json");
const customRegistry = ModelRegistry.create(customAuth, "/tmp/my-app/models.json");

// Runtime key (not persisted)
authStorage.setRuntimeApiKey("anthropic", "sk-my-temp-key");

// Built-ins only (no models.json)
const simpleRegistry = ModelRegistry.inMemory(authStorage);

await createAgentSession({
  sessionManager: SessionManager.inMemory(),
  authStorage,
  modelRegistry: simpleRegistry,
});
```

## OAuth

Subscription OAuth providers (ChatGPT Codex, Claude Pro/Max, GitHub Copilot, and extension-registered OAuth providers) use `/login` → browser or device-code flow → `auth.json` entry of type `oauth`.

- Tokens auto-refresh under a file lock so concurrent processes do not clobber each other.
- Failed refresh returns no key (provider skipped for discovery) while preserving credentials for a later `/login`.
- Extension OAuth registers via `pi.registerProvider(name, { oauth: { name, login, refreshToken, getApiKey, modifyModels? } })`. The OAuth provider `id` is forced to the registration name.

## Model selection

### Built-in catalog vs available models

| API | Behavior |
|-----|----------|
| `getModel(provider, id)` (`@earendil-works/pi-ai`) | Look up a built-in catalog entry |
| `modelRegistry.find(provider, id)` | Built-in + custom + extension models |
| `modelRegistry.getAvailable()` | Models with configured auth (no OAuth refresh) |
| `modelRegistry.getAll()` | Full registry regardless of auth |

### CLI and interactive

```bash
prime-agent --provider amazon-bedrock --model us.anthropic.claude-sonnet-4-20250514-v1:0
prime-agent --models "anthropic/*,openai/gpt-*"
```

- `/model` reloads `models.json` and lists models by `id` (display `name` used for matching and status text).
- Session `setModel(model)` requires configured auth and `canUseModel()` (Prime team gating for private Prime Inference models).
- Emits extension event `model_select` with `previousModel`, `model`, and `source` (`set` / cycle).
- `setModel(model, { waitForExtensions: false })` returns after state is saved without waiting for slow handlers; handlers still serialize across quick switches.
- `cycleModel()` walks scoped models (`--models` / enabled models) or all available models.

### SDK model pick

```typescript
import { getModel } from "@earendil-works/pi-ai";
import { AuthStorage, createAgentSession, ModelRegistry } from "@earendil-works/pi-coding-agent";

const authStorage = AuthStorage.create();
const modelRegistry = ModelRegistry.create(authStorage);

const opus = getModel("anthropic", "claude-opus-4-5");
const available = await modelRegistry.getAvailable();

const { session } = await createAgentSession({
  model: available[0],
  thinkingLevel: "medium", // off | low | medium | high (and model-supported levels)
  authStorage,
  modelRegistry,
});
```

## Provider registration

Two complementary paths; both are BYOK-friendly.

### 1. `models.json` (file-based)

```json
{
  "providers": {
    "ollama": {
      "baseUrl": "http://localhost:11434/v1",
      "api": "openai-completions",
      "apiKey": "ollama",
      "compat": {
        "supportsDeveloperRole": false,
        "supportsReasoningEffort": false
      },
      "models": [{ "id": "llama3.1:8b" }]
    }
  }
}
```

Supported `api` values for file config include `openai-completions`, `openai-responses`, `anthropic-messages`, and `google-generative-ai`. Extension registration additionally supports `azure-openai-responses`, `openai-codex-responses`, `mistral-conversations`, `google-vertex`, and `bedrock-converse-stream`.

Provider fields: `baseUrl`, `api`, `apiKey`, `headers`, `authHeader`, `models`, `modelOverrides`. `apiKey` / header values support literal, env-var name, or `!command` forms. Shell commands in `models.json` resolve at request time (no built-in TTL); wrap slow or rate-limited commands yourself. `/model` availability checks do **not** execute shell commands.

### 2. Extensions: `pi.registerProvider()`

```typescript
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";

export default function (pi: ExtensionAPI) {
  // Override-only: keep built-in models, change endpoint
  pi.registerProvider("anthropic", {
    baseUrl: "https://proxy.example.com",
  });

  // Full provider with models (replaces that provider's model list)
  pi.registerProvider("my-provider", {
    name: "My Provider",
    baseUrl: "https://api.example.com",
    apiKey: "MY_API_KEY",
    api: "openai-completions",
    authHeader: true,
    models: [
      {
        id: "my-model",
        name: "My Model",
        reasoning: false,
        input: ["text", "image"],
        cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
        contextWindow: 128000,
        maxTokens: 4096,
      },
    ],
  });
}
```

Validation rules when `models` is non-empty:

- `baseUrl` required
- `apiKey` or `oauth` required
- each model must resolve an `api` (provider- or model-level)
- `streamSimple` requires `api`

`pi.unregisterProvider(name)` removes dynamic models and restores built-ins overridden by that registration.

## Dynamic provider updates

`registerProvider` is safe after initial load: no `/reload` required. Overrides apply to the **active** session model immediately.

| Call site | Effect |
|-----------|--------|
| Top-level extension factory | Applied during resource load / session create |
| `session_start` handler | Applied when extensions bind; updates active model |
| Custom command handler | Applied on command run without reload |

Verified behaviors: `baseUrl` overrides appear on `session.model` and on the model object passed into the stream function for the next prompt.

`ModelRegistry.refresh()` reloads disk models, resets API/OAuth registries, re-applies still-registered dynamic providers, and reloads `auth.json` so credentials written by another process (for example a UI login while a daemon holds the session) become visible.

## Cloud ambient providers

### Amazon Bedrock

Auth markers (any one is enough for “configured”):

- `AWS_PROFILE`
- `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY`
- `AWS_BEARER_TOKEN_BEDROCK`
- ECS task role URIs / `AWS_WEB_IDENTITY_TOKEN_FILE` (IRSA)

Stream options support `region`, `profile`, `bearerToken`, thinking controls, and `requestMetadata`. Region resolution: explicit option → env → profile chain → default `us-east-1`. Bearer tokens skip SigV4 when set (unless `AWS_BEDROCK_SKIP_AUTH=1` for unauthenticated proxies). Optional: `AWS_ENDPOINT_URL_BEDROCK_RUNTIME`, `AWS_BEDROCK_FORCE_HTTP1`, `AWS_BEDROCK_FORCE_CACHE`.

### Google Vertex

ADC via `gcloud auth application-default login` or `GOOGLE_APPLICATION_CREDENTIALS`, plus `GOOGLE_CLOUD_PROJECT` / `GCLOUD_PROJECT` and `GOOGLE_CLOUD_LOCATION`.

## 401 stale-provider recovery

When a provider returns a concrete auth failure, the session marks the **credential source that was used** stale so the same dead key is not reused indefinitely.

### What counts as a concrete auth failure

- Structured diagnostic `provider_stream_failure` with `details.kind === "auth"` and status `401` or `403`
- Or error text matching `401`/`403` plus auth-related wording (including bare `"401 status code (no body)"`)

### Recovery loop

```mermaid
stateDiagram-v2
  [*] --> Streaming
  Streaming --> Retry: concrete 401/403 and retries remain
  Retry --> Streaming: backoff then re-prompt
  Retry --> Stale: max retries / cancel / retry disabled
  Streaming --> Stale: final non-auth error after prior auth failure
  Stale --> Guidance: errorMessage + "Run /login to update credentials."
  Guidance --> [*]
```

| Condition | Behavior |
|-----------|----------|
| Retry enabled, first auth failure | Capture auth source token; one (or more) auto-retries per `retry.maxRetries` |
| Repeated auth failure | Mark captured sources stale; emit `auth_stale`; append login guidance |
| Retry disabled | Mark stale immediately; no `auto_retry_start` |
| Retry cancelled mid-backoff | Mark captured sources stale; `auto_retry_end` with `"Retry cancelled"` |
| Credentials rotated during backoff | Mark each failed source token stale; new runtime key is a new identity |
| Auth failure then 500 on later attempt | Still marks prior auth sources stale; final error may be the 500 text plus login guidance |

Stale fingerprinting matches `source` + `identityFingerprint` + `valueFingerprint`. Updating credentials (new `/login`, new env value, new `setRuntimeApiKey`) produces a new fingerprint and becomes usable again. Stale status surfaces as:

```ts
authStorage.getAuthStatus(provider)
// { configured: false, source: "stale", label: "expired" }
```

Daemon and connection clients receive:

```ts
{ type: "auth_stale", provider: string, sourceTokens?: AuthSourceToken[] }
```

Recovery action: run `/login` (or supply a new key) for that provider, then continue the session.

## Error and guidance strings

| Situation | Message pattern |
|-----------|-----------------|
| No models with auth | `No models available.` + login help pointing at packaged `providers.md` / `models.md` |
| No model selected | `No model selected.` + login help + `/model` |
| Missing key for provider | `No API key found for <provider>.` |
| Auth failure after stale mark | Original error + `Run /login to update credentials.` |
| `setModel` without auth | throws `No API key for <provider>/<id>` |

## Verification checklist

<Steps>
  <Step title="Confirm credentials resolve">
    Set an env key or complete `/login`, then run `prime-agent model list` (or SDK `modelRegistry.getAvailable()`). Expect at least one model for the configured provider.
  </Step>
  <Step title="Confirm model selection persists">
    Select a model with `/model` or `session.setModel()`. Expect a `model_change` session entry and updated `defaultProvider` / `defaultModel` in settings.
  </Step>
  <Step title="Confirm dynamic overrides">
    From an extension, call `pi.registerProvider(provider, { baseUrl })` at load, `session_start`, or command time. The active model’s `baseUrl` should update for the next stream without restart.
  </Step>
  <Step title="Confirm stale recovery">
    Force a 401 (invalid key). With retries enabled, expect a single retry class of attempts, then `auth_stale`, `hasAuth === false` for that source, and login guidance on the assistant error. After `/login` or a new runtime key, `getAuthStatus` should leave `stale`.
  </Step>
</Steps>

## Related pages

<CardGroup cols={2}>
  <Card title="Authentication and providers" href="/authentication-providers">
    Login paths, multi-provider selection, OAuth examples, and BYOK boundaries.
  </Card>
  <Card title="Session configuration reference" href="/session-configuration">
    Full settings schema, defaults, reload behavior, and runtime config surfaces.
  </Card>
  <Card title="Extensions and custom tools" href="/extensions">
    Extension load path, allowlists, and provider registration from extensions.
  </Card>
  <Card title="Sessions and full control (SDK)" href="/sdk-sessions-control">
    SDK settings injection, session hooks, and full-control composition.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    Auth failures, provider 401s, network retry, and recovery probes.
  </Card>
  <Card title="Minimal SDK agent" href="/sdk-minimal">
    Bootstrap session creation with model and auth wiring.
  </Card>
</CardGroup>
