feat: Anthropic SDK backend — API key alternative to Claude CLI OAuth

Adds `anthropic_api` model type so users can authenticate with a direct
Anthropic API key instead of (or alongside) the CLI OAuth session.

- model_registry.py: `anthropic_api` type; `save/get/remove_anthropic_api_key()`
  mirroring the Google account pattern; `save_cloud_model()` now picks type
  based on credential type (cli → claude_cli, api_key → anthropic_api);
  `_resolve_model()` merges api_key from the credential entry
- llm_client.py: `_anthropic_api()` backend (AsyncAnthropic SDK); dispatch
  and fallback wiring; usage tracking
- routers/local_llm.py: Anthropic API key management routes
  (POST /settings/local/anthropic-key, /anthropic-key/{id}/remove);
  `anthropic_api` badge and edit-form credential selector
- static/local_llm.html: Anthropic Cloud Provider block now shows API key
  management (add/remove); Add Model → Anthropic tab has credential selector
  (CLI vs API key)
- requirements.txt: enable anthropic>=0.40.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-05-13 21:30:56 -04:00
parent 70665fadff
commit a92fd90f0d
9 changed files with 309 additions and 63 deletions

View File

@@ -229,7 +229,9 @@ Configure which AI models are available and which handles each task type.
Do this before adding models — models need a provider account or local host to attach to.
**Anthropic (Claude):** Nothing to configure. Claude uses your existing CLI OAuth session. If Claude isn't working, run `claude auth login` in a terminal.
**Anthropic (Claude):** Two options:
- **CLI (OAuth):** Nothing to configure — uses your existing `claude auth login` session. If Claude isn't working, run `claude auth login` in a terminal.
- **Direct API key:** Scroll to **Cloud Providers → Anthropic** → click **+ Add API key**. Enter a label and your `sk-ant-…` key from [console.anthropic.com/keys](https://console.anthropic.com/keys). When you add a model using an API key credential, it routes through the Anthropic SDK instead of the CLI.
**Google (Gemini):** Add one entry per API key you want to use:
1. Scroll to **Cloud Providers → Google** → click **+ Add Google account**
@@ -258,7 +260,7 @@ Scroll to **Add Model**. Select the provider tab, fill in the details, click **A
|---|---|
| **Local** | Select a host (from Step 1) → enter model name, or use **Fetch from host** to pick from a live list |
| **Google** | Select a Gemini model from the catalog → select a Google account (from Step 1) |
| **Anthropic** | Select a Claude model from the cataloguses your CLI session automatically |
| **Anthropic** | Select a credential (CLI OAuth or an API key added in Step 1) → select a Claude model from the catalog |
The label and context window size auto-fill from the catalog — edit them if you want. Tags are optional.

View File

@@ -319,16 +319,44 @@
<div class="provider-icon pi-anthropic">A</div>
<div>
<div class="provider-title">Anthropic</div>
<div class="provider-subtitle">Claude via CLI (OAuth) — no API key needed</div>
<div class="provider-subtitle">Claude via CLI (OAuth) or direct API key</div>
</div>
</div>
<p class="section-note" style="margin-bottom:0">
Claude models are accessed through the Claude CLI using your existing OAuth login.
Run <code style="font-family:monospace;color:#94a3b8">claude auth login</code> to authenticate.
</p>
<div id="claude-auth-status" class="auth-status" style="margin-top:0.6rem">
<span class="dot"></span><span id="claude-auth-msg">Checking…</span>
<div style="margin-bottom:1rem">
<p class="section-note" style="margin-bottom:0.3rem">
<strong>CLI (OAuth):</strong> Uses your existing Claude CLI login — no API key needed.
Run <code style="font-family:monospace;color:#94a3b8">claude auth login</code> to authenticate.
</p>
<div id="claude-auth-status" class="auth-status">
<span class="dot"></span><span id="claude-auth-msg">Checking…</span>
</div>
</div>
<p class="section-note" style="margin-bottom:0.4rem"><strong>API Keys:</strong></p>
{{ anthropic_key_rows }}
<details style="margin-top:0.5rem">
<summary>+ Add API key</summary>
<div>
<form method="POST" action="/settings/local/anthropic-key">
<input type="hidden" name="key_id" value="">
<div class="field-row">
<div class="field">
<label>Label <span style="color:#475569;font-weight:400">(e.g. Personal, Work)</span></label>
<input type="text" name="label" placeholder="Personal"
autocomplete="off" data-form-type="other">
</div>
<div class="field" style="flex:2">
<label>API Key</label>
<input type="password" name="api_key" placeholder="sk-ant-…"
autocomplete="new-password" data-1p-ignore data-lpignore="true"
data-form-type="other" required>
</div>
</div>
<button type="submit" class="btn btn-secondary btn-sm">Save</button>
</form>
</div>
</details>
</div>
<div class="provider-block" style="border-top:1px solid #2d3148; padding-top:1.25rem">
@@ -474,16 +502,22 @@
<!-- ANTHROPIC fields -->
<div id="pf-anthropic" style="display:none">
<div class="field">
<label>Credential</label>
<select id="add-anthropic-cred">
<!-- populated by JS from ANTHROPIC_API_KEYS -->
</select>
</div>
<div class="field">
<label>Claude model</label>
<select id="add-claude-model"></select>
</div>
<p class="section-note" style="margin-top:-0.25rem">Uses Claude CLI (OAuth)</p>
</div>
<!-- Hidden: cloud model name (set by JS from catalog pickers) -->
<input type="hidden" id="cloud-model-name" name="cloud_model_name" value="">
<input type="hidden" name="credential_id" value="cli">
<!-- credential_id is set by JS when Anthropic tab is active -->
<input type="hidden" id="add-credential-id" name="credential_id" value="cli">
<!-- Shared fields -->
<div class="field-row" style="margin-top:0.75rem">
@@ -559,6 +593,7 @@
const ROLE_CONFIG_DATA = {{ role_config_data_js }};
const TOOL_CATEGORIES = {{ tool_categories_js }};
const GOOGLE_ACCOUNTS = {{ google_accounts_js }};
const ANTHROPIC_API_KEYS = {{ anthropic_keys_js }};
const GOOGLE_CATALOG = {{ google_catalog_js }};
const ANTHROPIC_CATALOG = {{ anthropic_catalog_js }};
const HAS_HOSTS = {{ has_hosts }};
@@ -742,6 +777,10 @@
el.style.display = key === p ? '' : 'none';
}
fetchBtn.style.display = p === 'local' ? '' : 'none';
// Sync credential_id when switching to/from Anthropic
if (p === 'anthropic') {
credIdInput.value = anthropicCredSel.value || 'cli';
}
});
});
@@ -758,13 +797,27 @@
});
}
const geminiSel = document.getElementById('add-gemini-model');
const claudeSel = document.getElementById('add-claude-model');
const gAcctSel = document.getElementById('add-google-account');
const geminiSel = document.getElementById('add-gemini-model');
const claudeSel = document.getElementById('add-claude-model');
const gAcctSel = document.getElementById('add-google-account');
const anthropicCredSel = document.getElementById('add-anthropic-cred');
const credIdInput = document.getElementById('add-credential-id');
populateSelect(geminiSel, GOOGLE_CATALOG, 'id', 'label');
populateSelect(claudeSel, ANTHROPIC_CATALOG, 'id', 'label');
// Populate Anthropic credential selector (CLI + any configured API keys)
anthropicCredSel.innerHTML = '<option value="cli">Claude CLI (OAuth)</option>';
ANTHROPIC_API_KEYS.forEach(k => {
const opt = document.createElement('option');
opt.value = k.id;
opt.textContent = (k.label || 'API Key') + (k.hint ? ` (${k.hint})` : '');
anthropicCredSel.appendChild(opt);
});
anthropicCredSel.addEventListener('change', () => {
credIdInput.value = anthropicCredSel.value || 'cli';
});
if (GOOGLE_ACCOUNTS.length) {
gAcctSel.innerHTML = '<option value="">— select account —</option>';
GOOGLE_ACCOUNTS.forEach(a => {