{% extends "admin/base.html" %} {% block title %}Add LLM key — cmdop admin{% endblock %} {% block content %} {# Plan 61/2b W2 — Add LLM key wizard. Five steps, single Alpine component: 1. Pick provider — tile picker 2. Paste API key — masked input + format hint 3. Inline verify — live probe (POST /api/admin/wizards/add-llm-key/verify) 4. Set as default + spend cap 5. Done — success screen Reuses Wave 2a substrate macros (page_header step/step_total, card kind='roomy', tooltip). State lives in component-local memory + Redis (server-side, 30min TTL via the wizard cookie). #} {% from 'admin/components/ui.html' import card, input, page_header, tooltip %} {# tojson is HTML-safe by default (escapes " to "), so the attribute parses cleanly. |safe strips that escaping and breaks Alpine for the whole page. #}
{{ page_header( title='Add an LLM key', subtitle='Five quick steps. The key is encrypted before it ever touches disk.', eyebrow='Wizards', step=1, step_total=5, help_anchor='architecture', actions=[ {'label': 'Back to Model routing', 'kind': 'ghost', 'icon': 'arrow-left', 'href': '/admin/llm?tab=keys'}, ] ) }} {# Step indicator strip — the page_header `step`/`step_total` only prints "Step N of M"; we still want a horizontal progress bar with all five label dots. #}
{# Step 1 — Provider tile picker. ──────────────────────────────────── #}
{% call card(kind='roomy') %}

Pick a provider

Where will the gateway dispatch requests? {{ tooltip('Each provider supplies one API key; the same provider can hold multiple keys (e.g. dev + prod), one of them flagged as default.') }}

{% endcall %}
{# Step 2 — Paste API key. ─────────────────────────────────────────── #}
{% call card(kind='roomy') %}

Paste the key

{{ input("api_key", label="API key", type="password", required=true, attrs='x-model="form.api_key" :placeholder="currentProvider.key_format" autocomplete="off" spellcheck="false"') }}

Saved encrypted via Fernet at rest. You won't see the full value again after submit.

{{ input("name", label="Name", placeholder="primary", required=true, attrs='x-model="form.name"') }}

Human-readable label. Multiple keys per provider allowed.

{{ input("base_url", label="Base URL (optional)", attrs='x-model="form.base_url" :placeholder="currentProvider.default_base_url"') }}

Leave empty to use the provider default.

Where do I find this key? →

{% endcall %}
{# Step 3 — Verify. ─────────────────────────────────────────────────── #}
{% call card(kind='roomy') %}

Verify the key

We'll make one cheap live call to confirm the key reaches the provider. No request is recorded against your spend ledger.

{% endcall %}
{# Step 4 — Defaults + spend cap. ──────────────────────────────────── #}
{% call card(kind='roomy') %}

Default and spend cap

Optional tweaks. Either can be changed later from Provider keys.

{% endcall %}
{# Step 5 — Done. ──────────────────────────────────────────────────── #}
{% call card(kind='roomy') %}

Key saved

The gateway picked it up — health probe will refresh automatically.

{% endcall %}
{% endblock %}