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:
@@ -33,15 +33,16 @@ async def cleanup() -> None:
|
||||
|
||||
# Map from registry model type → dispatch function key
|
||||
_TYPE_TO_BACKEND = {
|
||||
"claude_cli": "claude",
|
||||
"gemini_cli": "gemini",
|
||||
"gemini_api": "gemini", # gemini_api falls back to CLI in this context
|
||||
"local_openai": "local",
|
||||
"claude_cli": "claude",
|
||||
"gemini_cli": "gemini",
|
||||
"gemini_api": "gemini", # gemini_api falls back to CLI in this context
|
||||
"local_openai": "local",
|
||||
"anthropic_api": "anthropic_api",
|
||||
}
|
||||
|
||||
# Explicit UI toggle values (kept for backward compat)
|
||||
_EXPLICIT_BACKENDS = ("claude", "gemini", "local")
|
||||
_FALLBACK = {"claude": "gemini", "gemini": "claude", "local": "claude"}
|
||||
_FALLBACK = {"claude": "gemini", "gemini": "claude", "local": "claude", "anthropic_api": "claude"}
|
||||
|
||||
|
||||
async def complete(
|
||||
@@ -123,6 +124,8 @@ async def _dispatch(
|
||||
return await _gemini(system_prompt, messages)
|
||||
if backend == "local":
|
||||
return await _local(system_prompt, messages, model_cfg, attachment=attachment)
|
||||
if backend == "anthropic_api":
|
||||
return await _anthropic_api(system_prompt, messages, model_cfg)
|
||||
return await _claude(system_prompt, messages, model_cfg)
|
||||
|
||||
|
||||
@@ -254,6 +257,51 @@ async def _local(
|
||||
return text.strip()
|
||||
|
||||
|
||||
async def _anthropic_api(system_prompt: str, messages: list[dict], model_cfg: dict | None) -> str:
|
||||
"""Direct Anthropic API backend using the anthropic SDK."""
|
||||
try:
|
||||
import anthropic
|
||||
except ImportError:
|
||||
raise RuntimeError("anthropic SDK not installed — run: pip install 'anthropic>=0.40.0'")
|
||||
|
||||
cfg = model_cfg or {}
|
||||
api_key = cfg.get("api_key", "")
|
||||
model_name = cfg.get("model_name") or settings.default_model
|
||||
|
||||
if not api_key:
|
||||
raise RuntimeError("No Anthropic API key — add one at /settings/models")
|
||||
|
||||
client = anthropic.AsyncAnthropic(api_key=api_key)
|
||||
|
||||
msgs = [{"role": m["role"], "content": m["content"]} for m in messages]
|
||||
kwargs: dict = {
|
||||
"model": model_name,
|
||||
"max_tokens": 4096,
|
||||
"messages": msgs,
|
||||
}
|
||||
if system_prompt:
|
||||
kwargs["system"] = system_prompt
|
||||
|
||||
resp = await client.messages.create(**kwargs)
|
||||
|
||||
text = resp.content[0].text if resp.content else ""
|
||||
if not text.strip():
|
||||
raise RuntimeError("Anthropic API returned an empty response")
|
||||
|
||||
if resp.usage:
|
||||
import usage_tracker
|
||||
from persona import _user
|
||||
asyncio.create_task(usage_tracker.record(
|
||||
username=_user.get(),
|
||||
backend="anthropic_api",
|
||||
model_name=model_name,
|
||||
prompt_tokens=resp.usage.input_tokens,
|
||||
completion_tokens=resp.usage.output_tokens,
|
||||
))
|
||||
|
||||
return text.strip()
|
||||
|
||||
|
||||
async def _gemini(system_prompt: str, messages: list[dict]) -> str:
|
||||
# Gemini CLI spawns MCP child processes that keep stdout pipes open after responding.
|
||||
# start_new_session=True puts the whole tree in its own process group so
|
||||
|
||||
@@ -57,6 +57,7 @@ Types:
|
||||
"gemini_cli" — Gemini CLI subprocess
|
||||
"gemini_api" — Gemini API (google-genai SDK); account_id → api_key from providers.google
|
||||
"local_openai" — OpenAI-compatible endpoint; host_id → api_url/api_key from hosts[]
|
||||
"anthropic_api" — Anthropic SDK direct; credential_id → api_key from providers.anthropic.credentials
|
||||
|
||||
Built-in model IDs (always resolvable without a registry entry):
|
||||
"claude_cli" — resolves to the default Claude CLI model
|
||||
@@ -353,6 +354,16 @@ def _resolve_model(registry: dict, model_id: str) -> dict | None:
|
||||
logger.warning("model %s references missing account_id %s", model_id, account_id)
|
||||
return dict(model)
|
||||
|
||||
if model_type == "anthropic_api":
|
||||
credential_id = model.get("credential_id")
|
||||
if credential_id:
|
||||
creds = registry.get("providers", {}).get("anthropic", {}).get("credentials", [])
|
||||
cred = next((c for c in creds if c["id"] == credential_id), None)
|
||||
if cred and cred.get("api_key"):
|
||||
return {**model, "api_key": cred["api_key"]}
|
||||
logger.warning("model %s references missing/keyless credential_id %s", model_id, credential_id)
|
||||
return dict(model)
|
||||
|
||||
if model_type == "claude_cli":
|
||||
return dict(model)
|
||||
|
||||
@@ -606,6 +617,72 @@ def remove_google_account(username: str, account_id: str) -> bool:
|
||||
return len(data["providers"]["google"]["accounts"]) < before
|
||||
|
||||
|
||||
# ── Write API — Anthropic API keys ───────────────────────────────────────────
|
||||
|
||||
def get_anthropic_api_keys(username: str) -> list[dict]:
|
||||
"""Return Anthropic API key credentials (type='api_key') with key masked for display."""
|
||||
registry = _load(username)
|
||||
creds = registry.get("providers", {}).get("anthropic", {}).get("credentials", [])
|
||||
return [
|
||||
{
|
||||
"id": c["id"],
|
||||
"label": c.get("label", ""),
|
||||
"hint": (c.get("api_key") or "")[:8] + "…" if c.get("api_key") else "no key",
|
||||
}
|
||||
for c in creds
|
||||
if c.get("type") == "api_key"
|
||||
]
|
||||
|
||||
|
||||
def save_anthropic_api_key(username: str, key_id: str | None,
|
||||
label: str, api_key: str) -> str:
|
||||
"""Create or update an Anthropic API key credential. Returns the credential ID."""
|
||||
data = _load(username)
|
||||
creds = data["providers"]["anthropic"]["credentials"]
|
||||
|
||||
if key_id:
|
||||
for c in creds:
|
||||
if c["id"] == key_id and c.get("type") == "api_key":
|
||||
c["label"] = label.strip() or c.get("label", "API Key")
|
||||
if api_key.strip():
|
||||
c["api_key"] = api_key.strip()
|
||||
_save(username, data)
|
||||
return key_id
|
||||
|
||||
key_id = secrets.token_hex(4)
|
||||
creds.append({
|
||||
"id": key_id,
|
||||
"label": label.strip() or "API Key",
|
||||
"type": "api_key",
|
||||
"api_key": api_key.strip(),
|
||||
})
|
||||
_save(username, data)
|
||||
return key_id
|
||||
|
||||
|
||||
def remove_anthropic_api_key(username: str, key_id: str) -> bool:
|
||||
"""Remove an Anthropic API key credential. Clears model entries that reference it."""
|
||||
data = _load(username)
|
||||
creds = data["providers"]["anthropic"]["credentials"]
|
||||
before = len(creds)
|
||||
data["providers"]["anthropic"]["credentials"] = [
|
||||
c for c in creds if c["id"] != key_id
|
||||
]
|
||||
|
||||
removed_model_ids = {
|
||||
m["id"] for m in data.get("models", [])
|
||||
if m.get("credential_id") == key_id
|
||||
}
|
||||
data["models"] = [m for m in data.get("models", []) if m["id"] not in removed_model_ids]
|
||||
for role_cfg in data.get("roles", {}).values():
|
||||
for key in PRIORITY_KEYS:
|
||||
if role_cfg.get(key) in removed_model_ids:
|
||||
role_cfg[key] = None
|
||||
|
||||
_save(username, data)
|
||||
return len(data["providers"]["anthropic"]["credentials"]) < before
|
||||
|
||||
|
||||
# ── Write API — Hosts ─────────────────────────────────────────────────────────
|
||||
|
||||
def save_host(username: str, host_id: str | None,
|
||||
@@ -716,11 +793,19 @@ def save_cloud_model(username: str, model_id: str | None,
|
||||
|
||||
provider: "anthropic" | "google"
|
||||
account_id: Google only — references providers.google.accounts[].id
|
||||
credential_id: Anthropic only — e.g. "cli"
|
||||
credential_id: Anthropic only — "cli" for OAuth CLI, or a hex ID for an API key credential
|
||||
"""
|
||||
_TYPE = {"google": "gemini_api", "anthropic": "claude_cli"}
|
||||
entry_type = _TYPE.get(provider, "gemini_api")
|
||||
data = _load(username)
|
||||
|
||||
# Determine model type from credential (anthropic only)
|
||||
if provider == "anthropic":
|
||||
creds = data.get("providers", {}).get("anthropic", {}).get("credentials", [])
|
||||
cred = next((c for c in creds if c["id"] == credential_id), None) if credential_id else None
|
||||
entry_type = "anthropic_api" if (cred and cred.get("type") == "api_key") else "claude_cli"
|
||||
elif provider == "google":
|
||||
entry_type = "gemini_api"
|
||||
else:
|
||||
entry_type = "claude_cli"
|
||||
tags = tags or []
|
||||
|
||||
entry: dict = {
|
||||
|
||||
@@ -31,5 +31,5 @@ pywebpush>=2.0.0
|
||||
# MariaDB / MySQL connector — used by ae_db_query orchestrator tool
|
||||
pymysql>=1.1.0
|
||||
|
||||
# anthropic SDK not needed — using claude CLI subprocess for auth
|
||||
# anthropic>=0.40.0
|
||||
# Anthropic SDK — direct API key backend (alternative to CLI OAuth)
|
||||
anthropic>=0.40.0
|
||||
|
||||
@@ -2,17 +2,19 @@
|
||||
Model Registry settings — providers, hosts, models, and role assignments.
|
||||
|
||||
Routes:
|
||||
GET /settings/models → settings page (canonical)
|
||||
GET /settings/local → redirect to /settings/models
|
||||
POST /settings/local/host → save/create a local host
|
||||
POST /settings/local/host/{id}/remove → remove a host (and its models)
|
||||
POST /settings/local/google-account → save/create a Google account
|
||||
GET /settings/models → settings page (canonical)
|
||||
GET /settings/local → redirect to /settings/models
|
||||
POST /settings/local/host → save/create a local host
|
||||
POST /settings/local/host/{id}/remove → remove a host (and its models)
|
||||
POST /settings/local/google-account → save/create a Google account
|
||||
POST /settings/local/google-account/{id}/remove → remove a Google account
|
||||
POST /settings/local/models/add → add a model (any provider)
|
||||
POST /settings/local/models/{id}/edit → edit an existing model entry
|
||||
POST /settings/local/models/{id}/remove → remove a model
|
||||
POST /api/models/role → AJAX: set a role assignment
|
||||
GET /api/local-llm/fetch-models → proxy to host /api/models (JSON)
|
||||
POST /settings/local/anthropic-key → save/create an Anthropic API key
|
||||
POST /settings/local/anthropic-key/{id}/remove → remove an Anthropic API key
|
||||
POST /settings/local/models/add → add a model (any provider)
|
||||
POST /settings/local/models/{id}/edit → edit an existing model entry
|
||||
POST /settings/local/models/{id}/remove → remove a model
|
||||
POST /api/models/role → AJAX: set a role assignment
|
||||
GET /api/local-llm/fetch-models → proxy to host /api/models (JSON)
|
||||
"""
|
||||
import json as _json
|
||||
import logging
|
||||
@@ -141,11 +143,32 @@ def _render(username: str, success: str = "", error: str = "") -> str:
|
||||
for h in hosts
|
||||
)
|
||||
|
||||
# ── Anthropic API key rows ────────────────────────────────────────────────
|
||||
anthropic_api_keys = reg.get_anthropic_api_keys(username)
|
||||
anthropic_keys_js = _json.dumps(anthropic_api_keys)
|
||||
anthropic_key_rows = ""
|
||||
for c in anthropic_api_keys:
|
||||
hint = c.get("hint", "no key")
|
||||
anthropic_key_rows += f'''
|
||||
<div class="account-row">
|
||||
<div>
|
||||
<span class="account-label">{c.get("label") or "API Key"}</span>
|
||||
<span class="account-hint">{hint}</span>
|
||||
</div>
|
||||
<form method="POST" action="/settings/local/anthropic-key/{c["id"]}/remove"
|
||||
onsubmit="return confirm('Remove this Anthropic API key?')">
|
||||
<button type="submit" class="btn-link danger">Remove</button>
|
||||
</form>
|
||||
</div>'''
|
||||
if not anthropic_key_rows:
|
||||
anthropic_key_rows = '<p class="empty-note">No API keys configured. Add one below or use Claude CLI (OAuth).</p>'
|
||||
|
||||
# ── Model rows (all providers) ────────────────────────────────────────────
|
||||
_PROVIDER_BADGE = {
|
||||
"claude_cli": ('<span class="pbadge pb-anthropic">Anthropic</span>', "Claude CLI"),
|
||||
"gemini_api": ('<span class="pbadge pb-google">Google</span>', ""),
|
||||
"local_openai": ('<span class="pbadge pb-local">Local</span>', ""),
|
||||
"claude_cli": ('<span class="pbadge pb-anthropic">Anthropic</span>', "Claude CLI"),
|
||||
"anthropic_api": ('<span class="pbadge pb-anthropic">Anthropic</span>', "API Key"),
|
||||
"gemini_api": ('<span class="pbadge pb-google">Google</span>', ""),
|
||||
"local_openai": ('<span class="pbadge pb-local">Local</span>', ""),
|
||||
}
|
||||
model_rows = ""
|
||||
for m in models:
|
||||
@@ -201,6 +224,17 @@ def _render(username: str, success: str = "", error: str = "") -> str:
|
||||
f'<div class="field"><label>Google Account</label>'
|
||||
f'<select name="account_id">{acct_opts}</select></div>'
|
||||
)
|
||||
elif mtype == "anthropic_api":
|
||||
key_opts = "".join(
|
||||
f'<option value="{c["id"]}"'
|
||||
f'{" selected" if c["id"] == m.get("credential_id") else ""}>'
|
||||
f'{c.get("label","API Key")} ({c.get("hint","")})</option>'
|
||||
for c in anthropic_api_keys
|
||||
)
|
||||
extra_fields = (
|
||||
f'<div class="field"><label>API Key</label>'
|
||||
f'<select name="credential_id">{key_opts or "<option value=\"\">No API keys configured</option>"}</select></div>'
|
||||
)
|
||||
else:
|
||||
extra_fields = '<input type="hidden" name="credential_id" value="cli">'
|
||||
|
||||
@@ -379,19 +413,21 @@ def _render(username: str, success: str = "", error: str = "") -> str:
|
||||
|
||||
html = (_STATIC / "local_llm.html").read_text()
|
||||
replacements = {
|
||||
"{{ username }}": username,
|
||||
"{{ google_account_rows }}": google_account_rows,
|
||||
"{{ host_rows }}": host_rows,
|
||||
"{{ model_rows }}": model_rows,
|
||||
"{{ host_options }}": host_options,
|
||||
"{{ role_rows }}": role_rows,
|
||||
"{{ role_data_js }}": role_data_js,
|
||||
"{{ role_config_data_js }}": role_config_data_js,
|
||||
"{{ tool_categories_js }}": tool_categories_js,
|
||||
"{{ google_accounts_js }}": google_accounts_js,
|
||||
"{{ google_catalog_js }}": google_catalog_js,
|
||||
"{{ username }}": username,
|
||||
"{{ google_account_rows }}": google_account_rows,
|
||||
"{{ anthropic_key_rows }}": anthropic_key_rows,
|
||||
"{{ host_rows }}": host_rows,
|
||||
"{{ model_rows }}": model_rows,
|
||||
"{{ host_options }}": host_options,
|
||||
"{{ role_rows }}": role_rows,
|
||||
"{{ role_data_js }}": role_data_js,
|
||||
"{{ role_config_data_js }}": role_config_data_js,
|
||||
"{{ tool_categories_js }}": tool_categories_js,
|
||||
"{{ google_accounts_js }}": google_accounts_js,
|
||||
"{{ anthropic_keys_js }}": anthropic_keys_js,
|
||||
"{{ google_catalog_js }}": google_catalog_js,
|
||||
"{{ anthropic_catalog_js }}": anthropic_catalog_js,
|
||||
"{{ has_hosts }}": has_hosts,
|
||||
"{{ has_hosts }}": has_hosts,
|
||||
}
|
||||
for key, val in replacements.items():
|
||||
html = html.replace(key, val)
|
||||
@@ -442,6 +478,31 @@ async def remove_google_account(request: Request, account_id: str):
|
||||
return HTMLResponse(_render(username, success="Google account removed."))
|
||||
|
||||
|
||||
@router.post("/settings/local/anthropic-key", include_in_schema=False)
|
||||
async def save_anthropic_api_key(
|
||||
request: Request,
|
||||
key_id: str = Form(""),
|
||||
label: str = Form(""),
|
||||
api_key: str = Form(""),
|
||||
):
|
||||
username = _get_user(request)
|
||||
if not username:
|
||||
return RedirectResponse("/login", status_code=302)
|
||||
if not api_key.strip() and not key_id.strip():
|
||||
return HTMLResponse(_render(username, error="API key is required."))
|
||||
reg.save_anthropic_api_key(username, key_id or None, label, api_key)
|
||||
return HTMLResponse(_render(username, success="Anthropic API key saved."))
|
||||
|
||||
|
||||
@router.post("/settings/local/anthropic-key/{key_id}/remove", include_in_schema=False)
|
||||
async def remove_anthropic_api_key(request: Request, key_id: str):
|
||||
username = _get_user(request)
|
||||
if not username:
|
||||
return RedirectResponse("/login", status_code=302)
|
||||
reg.remove_anthropic_api_key(username, key_id)
|
||||
return HTMLResponse(_render(username, success="Anthropic API key removed."))
|
||||
|
||||
|
||||
@router.post("/settings/local/host", include_in_schema=False)
|
||||
async def save_host(
|
||||
request: Request,
|
||||
@@ -562,7 +623,7 @@ async def edit_model(
|
||||
reg.save_cloud_model(username, model_id, "google", model_name, label,
|
||||
account_id=account_id or None, context_k=context_k, tags=tag_list,
|
||||
max_rounds=max_rounds_, tools=tools_bool)
|
||||
elif mtype == "claude_cli":
|
||||
elif mtype in ("claude_cli", "anthropic_api"):
|
||||
reg.save_cloud_model(username, model_id, "anthropic", model_name, label,
|
||||
credential_id=credential_id or "cli", context_k=context_k, tags=tag_list,
|
||||
max_rounds=max_rounds_, tools=tools_bool)
|
||||
|
||||
@@ -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 catalog → uses 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.
|
||||
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
Reference in New Issue
Block a user