# Configure Polish

> Enable optional transcript polish: pick an LLMProvider (Groq recommended), store per-provider keys in Keychain, select rewriteModel, toggle correctionEnabled, and interpret PolishUIState (setup, on, paused, keyBroken). Onboarding Groq-only validation path.

- Repository: cartesia-ai/InkIt
- GitHub: https://github.com/cartesia-ai/InkIt
- Human docs: https://www.grok-wiki.com/public/docs/cartesia-ai-inkit-18975554254b
- Complete Markdown: https://www.grok-wiki.com/public/docs/cartesia-ai-inkit-18975554254b/llms-full.txt

## Source Files

- `README.md`
- `InkIt/LLMProvider.swift`
- `InkIt/SettingsStore.swift`
- `InkIt/TranscriptRewriter.swift`
- `InkIt/SettingsView.swift`
- `InkIt/OnboardingView.swift`

---

---
title: "Configure Polish"
description: "Enable optional transcript polish: pick an LLMProvider (Groq recommended), store per-provider keys in Keychain, select rewriteModel, toggle correctionEnabled, and interpret PolishUIState (setup, on, paused, keyBroken). Onboarding Groq-only validation path."
---

Polish is an optional post-STT rewrite pass: after Cartesia Ink-2 returns a raw transcript, `TranscriptRewriter` sends it to your chosen `LLMProvider` to strip fillers, fix ASR slips, and apply light punctuation — without paraphrasing. Configuration lives in **Settings → Polish** (`PolishSettingsView`), backed by `SettingsStore` (`correctionEnabled`, `rewriteProvider`, `rewriteModel`, per-provider Keychain keys). Polish is off by default; dictation works without it.

<Info>
Polish uses a separate API key from Cartesia STT. Transcription requires a Cartesia key; polish requires a key from Groq, Google Gemini, OpenAI, or Anthropic.
</Info>

## Where to configure

| Surface | When to use |
| --- | --- |
| **Settings → Polish** | Primary configuration: provider, model, API key, master toggle |
| **Home nudge** | Shown when `correctionEnabled` is false and `polishNudgeDismissed` is false; **Set up Polish** opens Settings on the Polish pane |
| **Home status card** | Appears when `polishIssue` is non-nil (invalid key or out of credits while polish is enabled) |

First-run onboarding does not include a dedicated Polish step. Optional polish is introduced after onboarding via the Home nudge or Settings. `GroqKeyValidator` exists as Groq-only advisory validation machinery intended for a focused onboarding path (pins Groq, hides the provider picker); provider switching and full validation run through `LLMKeyValidator` in Settings.

## Enable polish from Settings

<Steps>
<Step title="Open Settings → Polish">

From the Home gear button or menu bar, open Settings and select **Polish** in the sidebar.

</Step>
<Step title="Pick a provider">

Choose an `LLMProvider` in the **Provider** picker. Groq is marked **(Recommended)** (`LLMProvider.isRecommended`); defaults are `rewriteProvider = .groq` and `rewriteModel = "llama-3.3-70b-versatile"`.

</Step>
<Step title="Enter and verify an API key">

Paste the provider API key into **API key**. Keys persist in Keychain under account `llm.{provider}` (for example `llm.groq`). An empty value removes that Keychain item.

`LLMKeyValidator` debounces keystrokes (0.6s) and probes `GET` on each provider's models endpoint via `LLMProvider.validationRequest(key:)`. Validation is advisory — it never blocks typing or saving.

</Step>
<Step title="Confirm polish is on">

When validation reaches `.verified`, `PolishKeyField` calls `enablePolish(provider:)` if the pane is in **setup** or **keyBroken**. That sets `correctionEnabled = true`, aligns `rewriteModel` to the provider default if needed, and clears `polishKeyInvalid` / `polishOutOfCredits`.

</Step>
<Step title="Dictate and verify">

Hold or toggle your dictation hotkey, speak, and release. With polish on, the notch enters `.rewriting` while `TranscriptRewriter` runs, then pastes the polished text. History rows show a polish outcome indicator when rewrite succeeds.

</Step>
</Steps>

<Tip>
Groq's free tier needs no card (`LLMProvider.keyHint` for Groq: "Free tier, no card needed."). Key URLs: Groq `https://console.groq.com/keys`, Gemini `https://aistudio.google.com/apikey`, OpenAI `https://platform.openai.com/api-keys`, Anthropic `https://console.anthropic.com/settings/keys`.
</Tip>

## PolishUIState

`SettingsStore.polishUIState` drives the Polish settings pane layout. The API key is the gate: no key means setup; a stored key unlocks on, paused, or key-broken.

```mermaid
stateDiagram-v2
    [*] --> setup: no key for rewriteProvider
    setup --> on: verified key + enablePolish
    on --> paused: pausePolish (toggle off)
    paused --> on: toggle on
    on --> keyBroken: rewrite 401/403
    keyBroken --> on: verified key re-entered
```

| State | Condition | UI behavior |
| --- | --- | --- |
| `setup` | `!hasRewriteKey` | Provider picker, model row, key field — no master toggle |
| `on` | Key present, `correctionEnabled`, `!polishKeyInvalid` | Master toggle on; provider/key always editable |
| `paused` | Key present, `!correctionEnabled` | Master toggle off; key retained via `pausePolish()` |
| `keyBroken` | Key present, `correctionEnabled`, `polishKeyInvalid` | Warning banner; master toggle disabled until key is re-entered |

<ParamField body="correctionEnabled" type="boolean">
Master polish switch, persisted in UserDefaults. When false, `AppCoordinator.correctedTranscript` skips rewrite and pastes raw STT output. Default: false.
</ParamField>

<ParamField body="rewriteProvider" type="LLMProvider">
Selected LLM provider (`groq`, `gemini`, `openai`, `anthropic`). Changing provider clears `polishKeyInvalid` and `polishOutOfCredits` so the new provider starts clean.
</ParamField>

<ParamField body="rewriteModel" type="string">
Model ID sent to the provider chat endpoint. Shown read-only in Settings (one curated model per provider today). Auto-reset to `provider.defaultModel` when the stored model is not in `provider.models`.
</ParamField>

<ParamField body="llmAPIKeys" type="[String: String]">
In-memory map keyed by `LLMProvider.rawValue`. Each non-empty value syncs to Keychain account `llm.{provider}`.
</ParamField>

## Provider and model defaults

| Provider | Display name | Default model | Rewrite timeout |
| --- | --- | --- | --- |
| `groq` | Groq (Recommended) | `llama-3.3-70b-versatile` | 1.0s |
| `gemini` | Google Gemini | `gemini-2.5-flash-lite` | 2.0s |
| `openai` | OpenAI | `gpt-4.1-nano` | 2.0s |
| `anthropic` | Anthropic | `claude-haiku-4-5-20251001` | 2.0s |

Groq, Gemini, and OpenAI use OpenAI-compatible `/chat/completions` endpoints. Anthropic uses the native Messages API (`LLMProvider.isOpenAICompatible` is false for Anthropic).

## Key storage and validation

```text
SettingsStore
├── UserDefaults: correctionEnabled, rewriteProvider, rewriteModel
│                  polishKeyInvalid, polishOutOfCredits, polishNudgeDismissed
└── Keychain (account per provider)
    ├── llm.groq
    ├── llm.gemini
    ├── llm.openai
    └── llm.anthropic
```

On signed release builds, secrets stay in Keychain (`Keychain.usesKeychain`). Ad-hoc local builds fall back to namespaced UserDefaults (`secretFallback.llm.{provider}`) because ad-hoc re-signing orphans Keychain items.

### Validation probes

| Validator | Scope | Probe |
| --- | --- | --- |
| `LLMKeyValidator` | Settings Polish pane | `provider.validationRequest(key:)` → `GET` models list |
| `GroqKeyValidator` | Groq-only onboarding path (infrastructure) | `GET https://api.groq.com/openai/v1/models` with Bearer token |

`APIKeyValidator.State` values: `idle`, `checking`, `verified`, `invalidKey`, `couldNotVerify`. HTTP 2xx → verified; 400/401/403 → invalid key; transport errors → could not verify.

<Warning>
A verified key in Settings auto-enables polish from **setup** or **keyBroken** only. It does not auto-resume a deliberate **paused** state — turn the master toggle back on manually.
</Warning>

## Runtime behavior

When `correctionEnabled` is true and a key exists for `rewriteProvider`, the dictation pipeline runs polish after STT finalizes:

```mermaid
sequenceDiagram
    participant User
    participant AppCoordinator
    participant TranscriptRewriter
    participant LLM as LLM provider API

    User->>AppCoordinator: hotkey press
    AppCoordinator->>TranscriptRewriter: prewarm() (HEAD to endpoint)
    User->>AppCoordinator: speak, release
    AppCoordinator->>AppCoordinator: STT finalize
    AppCoordinator->>TranscriptRewriter: rewriteWithoutContext(raw)
    TranscriptRewriter->>LLM: POST chat completion
    LLM-->>TranscriptRewriter: polished text
    AppCoordinator->>User: paste polished transcript
```

- **Prewarm**: At hotkey press, `AppCoordinator` creates a `TranscriptRewriter` and calls `prewarm()` so TLS/TCP setup overlaps recording. Skipped for the onboarding Try It box (which never polishes).
- **Fallback**: On any `RewriteFailure`, the raw transcript is pasted. Persistent auth/billing failures set `polishKeyInvalid` (401/403) or `polishOutOfCredits` (402).
- **Success clears flags**: A polished outcome clears both `polishKeyInvalid` and `polishOutOfCredits`.

`enablePolish(provider:)` and `pausePolish()` centralize the provider/model/enabled trio so Settings and search results stay consistent.

## Service issues and Home surfacing

`SettingsStore.polishIssue` returns a `ServiceIssue` only when polish is enabled and a key exists:

| Issue | Persisted flag | Home card |
| --- | --- | --- |
| `keyInvalid` | `polishKeyInvalid` | "Polish is paused" — invalid key CTA opens Settings → Polish |
| `outOfCredits` | `polishOutOfCredits` | "Polish is paused" — billing CTA opens `rewriteProvider.billingURL` |

Unlike Cartesia transcription failures (full-width banner), polish problems are calm rail cards because raw text still pastes.

## Groq-only onboarding validation path

`GroqKeyValidator` subclasses `APIKeyValidator` with a fixed Groq `GET /openai/v1/models` probe. Comments in `LLMProvider.swift` describe the design intent: onboarding would pin Groq as the recommended provider, hide the provider picker, and validate with this focused subclass rather than the generic `LLMKeyValidator`.

Current first-run flow (`OnboardingStep`: welcome → permissions → apiKey → tryIt → done) does not wire `GroqKeyValidator`. Post-onboarding polish setup uses the Home nudge (Groq-preselected in Settings defaults) or **Settings → Polish** with the full four-provider picker and `LLMKeyValidator`.

## Verification checklist

<Check>
After configuration, confirm: `settings.polishUIState == .on`, the Polish master toggle is on, and a test dictation shows the notch briefly in rewriting before paste.
</Check>

<Check>
History records `polish: .polished` on success. If rewrite fails transiently (rate limit, timeout, offline), raw text still pastes and the row may show `polish: .failed` with a tooltip reason.
</Check>

<Check>
Toggling polish off sets `correctionEnabled = false` but retains the Keychain key. Re-enable via the master toggle without re-entering the key.
</Check>

## Related pages

<CardGroup>
<Card title="Dictation pipeline" href="/dictation-pipeline">
End-to-end flow from hotkey through STT, optional polish, paste, and history persistence.
</Card>
<Card title="LLM providers reference" href="/llm-providers-reference">
Endpoints, default models, timeouts, validation probes, and RewriteFailure reason codes.
</Card>
<Card title="Settings reference" href="/settings-reference">
All persisted SettingsStore keys including Keychain accounts and derived issue properties.
</Card>
<Card title="Polish troubleshooting" href="/polish-troubleshooting">
Rewrite failures, rate limits, timeout fallbacks, and history-row failure tooltips.
</Card>
</CardGroup>
