# Model key

The person's own key for a model provider (bring your own key): empty, it says what a key unlocks and what works without one; adding one asks for the provider (none is chosen for you when there are several) and the key, says where data goes once it's added, right beside Add key, and checks it with the provider before it's stored. While it checks, Add key says so and stays locked, and so does Cancel when replacing, saying why; a key the provider refuses says what to check. Once the provider accepts a key it says "Verified with …, saving…" until the app's `stored` changes, never "No key added" or the old key in between. Verified, it shows the provider, the part of the key that's safe to show, who added it and when, and Replace and Remove; the key itself is never shown again. Remove asks first, in place, and says what stops working and that the key isn't kept.

Focus follows the work: to the key field when the form opens, to the verified key's heading once a key is added, to Replace after a cancelled replace, to Remove after Keep it, and to the heading after a removal; a check or a removal that finishes after the person has moved focus out of the item leaves focus where it is and is announced. With `readOnly`, Add key, Replace and Remove stay in place, locked, saying why.

- Registry name: `model-key` (`@candor/model-key`)
- Tier: Pro. Installing it needs a Pro, Premium or Team licence key: the person runs `npx candor-ui key <key>` themselves; never ask for the key in chat or write it to a file.
- Docs: https://candor.themethodic.com/docs/model-key

## Install

```bash
npx candor-ui add model-key
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/model-key`.
It brings in: bidi, button, heading-level, heading-scope, input, locale, locked, radio-group, relative-time, tokens, use-announcer, use-focus-within, utils.
npm packages: lucide-react@^1.67.0.

## Example

```tsx
"use client"

import * as React from "react"
import { ModelKey, type StoredModelKey } from "@/components/candor/model-key"

export default function Example() {
  // A real app checks the key with the provider on its server and stores only the key's record; this one accepts any
  // key that starts with "pa-" and keeps its last four characters as the part that's safe to show.
  const [stored, setStored] = React.useState<StoredModelKey | null>(null)
  return (
    <ModelKey
      className="max-w-lg"
      providers={[
        { id: "a", name: "Provider A", placeholder: "pa-…" },
        { id: "b", name: "Provider B", placeholder: "pb-…" },
      ]}
      stored={stored}
      unlocks={["Chat and agents on your own model and account", "Usage billed by the provider, not by us"]}
      without={["Audits, reports and monitors", "Everything you’ve already set up"]}
      onAdd={async (provider, key) => {
        await new Promise((r) => setTimeout(r, 600))
        if (!key.startsWith(`p${provider}-`)) return { ok: false, reason: "The provider answered: invalid key." }
        setStored({ provider, hint: `p${provider}-…${key.slice(-4)}`, addedAt: new Date().toISOString(), addedBy: "Dana Ortiz" })
        return { ok: true }
      }}
      onRemove={async () => {
        await new Promise((r) => setTimeout(r, 400))
        setStored(null)
      }}
    />
  )
}
```

## <ModelKey>

Props:

- `providers: ModelKeyProvider[]`: The providers whose keys the app takes. With one, it's named and not asked for.
- `stored?: StoredModelKey | null | undefined`: The stored key; left out (or `null`) while there is none.
- `unlocks?: string[] | undefined` (default `[]`): What a key unlocks, in the app's words, shown while there is none.
- `without?: string[] | undefined` (default `[]`): What works without a key, in the app's words, shown while there is none.
- `onAdd?: ((provider: string, key: string) => Promise<ModelKeyCheck>) | undefined`: Shows Add key (and Replace). Called with the provider's id and the key; check it with the provider and store it, then resolve with what the check found, and update `stored` (until it changes, the key reads as saving). If it throws or rejects, the form says the key couldn't be checked and that nothing was stored.
- `onRemove?: (() => void | Promise<void>) | undefined`: Shows Remove. Called once the person confirms; the confirmation waits on a returned promise and says if it fails.
- `readOnly?: string | undefined`: Why this person can't add, replace or remove the key, e.g. "Only admins manage the model key."
- `headingLevel?: HeadingLevel | undefined`: The level of the heading. Defaults to one below the nearest `HeadingScope`, else 2 (it was 3 before 5.0); the item's own headings sit one below it.
- `className?: string | undefined`: Classes, merged with Candor's.

## Strings (`modelKey`)

The fixed words, English by default. Override any of them under `modelKey` in `<LocaleProvider strings={{ modelKey: { … } }}>`; the table is exported as `modelKeyStrings`.

- `heading`: Your model key
- `none`: No key added. Add one from your own account with a model provider.
- `unlocks`: A key unlocks
- `without`: Without one
- `provider`: Provider
- `key`: Key
- `keyFor`: {name} key
- `keyHint`: It’s checked with {provider}, then stored. It’s never shown again, here or anywhere.
- `keyHintNoProvider`: It’s checked with the provider, then stored. It’s never shown again, here or anywhere.
- `egress`: With this key added, chat and your agents can send data to {provider}.
- `add`: Add key
- `verifying`: Checking with {provider}…
- `cancel`: Cancel
- `needsProvider`: Choose a provider first.
- `needsKey`: Paste the key first.
- `waitVerify`: Wait for the check to finish.
- `cancelLocked`: The key is being checked, so the replace can’t be cancelled now. Once the check finishes, you can cancel.
- `invalid`: {provider} didn’t accept this key
- `invalidWhy`: The check failed without saying why.
- `check`: What to check
- `checkWhole`: The key is copied whole, with nothing added at either end.
- `checkProvider`: It’s a key for {provider}, not for another provider.
- `checkActive`: It’s still active in your {provider} account.
- `verifyFailed`: Couldn’t check the key. Nothing was stored; try again.
- `verified`: Verified with {provider}
- `saving`: Verified with {provider}, saving…
- `added`: Added {when}
- `addedBy`: Added by {name} {when}
- `neverShown`: The key itself is never shown again. To change it, replace it.
- `refused`: {provider} now refuses this key
- `replace`: Replace
- `remove`: Remove
- `removeTitle`: Remove the {provider} key?
- `removeBreaks`: Chat and agents that use it stop working with {provider} until a key is added again.
- `removeIrreversible`: The key isn’t kept anywhere once it’s removed: to use it again, paste it again.
- `keep`: Keep it
- `removeIt`: Remove key
- `removing`: Removing…
- `waitRemove`: Wait for the key to be removed.
- `keepLocked`: Removing is under way, so the key can’t be kept now. If it fails, you can keep it.
- `removeFailed`: Removing failed. Try again, or keep the key.
- `removed`: Key removed

## Types

### ModelKeyProvider

A model provider whose key the person can bring, as the app lists it.

```ts
{
  id: string;
  /** Its name, e.g. "Anthropic". */
  name: string;
  /** What its keys start with, shown as the field's placeholder, e.g. "sk-ant-…". */
  placeholder?: string | undefined;
  /** Where data goes once this provider's key is added, in the app's words; replaces the default line. */
  egress?: string | undefined;
  /** What to check when this provider refuses a key, in the app's words; replaces the default list. */
  check?: string[] | undefined;
}
```

### StoredModelKey

The stored key, as the app's server describes it. The key itself is never here: only the part safe to show.

```ts
{
  /** The provider's id, from `providers`. */
  provider: string;
  /** The part of the key that's safe to show, e.g. "sk-ant-…4f2a". */
  hint: string;
  /** When it was added, as an ISO time. */
  addedAt: string;
  /** Who added it, by name. Left out, no name is shown. */
  addedBy?: string | undefined;
  /** The provider refuses it now (revoked or expired there), in the app's or the provider's words. */
  problem?: string | undefined;
}
```

### ModelKeyCheck

What checking a key found, as the app's server reports it. On `ok`, update `stored` with the key's record.

```ts
{ ok: true } | { ok: false; reason?: string | undefined }
```

## Types from other items

The props above take these. Each is defined by another item; import it from the path shown.

### HeadingLevel

Import: `import type { HeadingLevel } from "@/lib/candor/heading-level"` (item `heading-level`, https://candor.themethodic.com/docs/heading-level.md)

A heading's level in the page's outline: `1` is `<h1>`, `6` is `<h6>`. Every Candor item that draws a heading takes its `headingLevel` as this type; the level is where the heading sits in the outline, never how large it looks.

```ts
1 | 2 | 3 | 4 | 5 | 6
```

## Rules for using Candor UI

- Components take plain typed data. Never pass model output as HTML or render it as a link; messages are typed blocks.
- A control that can't be used says why: wrap it in `<Locked reason="…">`, don't just disable it.
- Don't show a value you don't have: leave it out (it renders as —) rather than guess. An estimate is labelled as one.
- Nothing is pre-selected in a decision a person makes, and the agent is named from your app's data, never from model output.
- Your app's words go in `LocaleProvider`: its fixed strings in `strings`, its nouns in `nouns`; entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
