From 8b27f69a6fa1141edaf615cd48be857dfe6c3869 Mon Sep 17 00:00:00 2001 From: 7418 <7418@openclaw.ai> Date: Fri, 6 Feb 2026 23:17:49 +0800 Subject: [PATCH] feat: add optional API configuration for custom Anthropic endpoints Add a new "API Configuration" section in settings for users who use custom Anthropic-compatible APIs (e.g. via ai-claude-start). Users can optionally set ANTHROPIC_AUTH_TOKEN and ANTHROPIC_BASE_URL in CodePilot's settings UI. These are stored in SQLite and injected into the Claude Code subprocess env only when configured. Existing authentication methods (claude login, shell env vars) are not affected - the API config is purely additive. Co-Authored-By: Claude Opus 4.6 --- src/app/api/settings/app/route.ts | 64 +++++++++++++++++ src/app/settings/page.tsx | 112 +++++++++++++++++++++++++++++- src/lib/claude-client.ts | 20 ++++-- 3 files changed, 190 insertions(+), 6 deletions(-) create mode 100644 src/app/api/settings/app/route.ts diff --git a/src/app/api/settings/app/route.ts b/src/app/api/settings/app/route.ts new file mode 100644 index 0000000..47ccfdd --- /dev/null +++ b/src/app/api/settings/app/route.ts @@ -0,0 +1,64 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getSetting, setSetting } from '@/lib/db'; + +/** + * CodePilot app-level settings (stored in SQLite, separate from ~/.claude/settings.json). + * Used for API configuration (ANTHROPIC_AUTH_TOKEN, ANTHROPIC_BASE_URL, etc.) + */ + +const ALLOWED_KEYS = [ + 'anthropic_auth_token', + 'anthropic_base_url', +]; + +export async function GET() { + try { + const result: Record = {}; + for (const key of ALLOWED_KEYS) { + const value = getSetting(key); + if (value !== undefined) { + // Mask token for security (only return last 8 chars) + if (key === 'anthropic_auth_token' && value.length > 8) { + result[key] = '***' + value.slice(-8); + } else { + result[key] = value; + } + } + } + return NextResponse.json({ settings: result }); + } catch (error) { + const message = error instanceof Error ? error.message : 'Failed to read app settings'; + return NextResponse.json({ error: message }, { status: 500 }); + } +} + +export async function PUT(request: NextRequest) { + try { + const body = await request.json(); + const { settings } = body; + + if (!settings || typeof settings !== 'object') { + return NextResponse.json({ error: 'Invalid settings data' }, { status: 400 }); + } + + for (const [key, value] of Object.entries(settings)) { + if (!ALLOWED_KEYS.includes(key)) continue; + const strValue = String(value ?? '').trim(); + if (strValue) { + // Don't overwrite token if user sent the masked version back + if (key === 'anthropic_auth_token' && strValue.startsWith('***')) { + continue; + } + setSetting(key, strValue); + } else { + // Empty value = remove the setting + setSetting(key, ''); + } + } + + return NextResponse.json({ success: true }); + } catch (error) { + const message = error instanceof Error ? error.message : 'Failed to save app settings'; + return NextResponse.json({ error: message }, { status: 500 }); + } +} diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index 4f0bf98..d60e5bf 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -60,6 +60,112 @@ export default function SettingsPage() { ); } +// --- API Configuration Section (CodePilot app settings, stored in SQLite) --- +function ApiConfigSection() { + const [token, setToken] = useState(""); + const [baseUrl, setBaseUrl] = useState(""); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [status, setStatus] = useState<"idle" | "saved" | "error">("idle"); + + useEffect(() => { + fetch("/api/settings/app") + .then((r) => r.json()) + .then((data) => { + const s = data.settings || {}; + setToken(s.anthropic_auth_token || ""); + setBaseUrl(s.anthropic_base_url || ""); + }) + .catch(() => {}) + .finally(() => setLoading(false)); + }, []); + + const handleSave = async () => { + setSaving(true); + setStatus("idle"); + try { + const res = await fetch("/api/settings/app", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + settings: { + anthropic_auth_token: token, + anthropic_base_url: baseUrl, + }, + }), + }); + if (res.ok) { + setStatus("saved"); + setTimeout(() => setStatus("idle"), 2000); + } else { + setStatus("error"); + } + } catch { + setStatus("error"); + } finally { + setSaving(false); + } + }; + + if (loading) return null; + + return ( +
+
+ +

+ Optional. Configure a custom Anthropic-compatible API for Claude Code. + Leave empty to use the default authentication (claude login / shell environment). +

+
+
+
+ + setBaseUrl(e.target.value)} + className="mt-1 font-mono text-sm" + /> +
+
+ + setToken(e.target.value)} + className="mt-1 font-mono text-sm" + /> +
+
+
+ + {status === "saved" && ( + Saved + )} + {status === "error" && ( + Failed to save + )} +
+
+ ); +} + +// --- Claude CLI Settings Section (manages ~/.claude/settings.json) --- function SettingsPageInner() { const [settings, setSettings] = useState({}); const [originalSettings, setOriginalSettings] = useState({}); @@ -168,12 +274,14 @@ function SettingsPageInner() {

Settings

- Manage your Claude CLI settings (~/.claude/settings.json) + Manage CodePilot and Claude CLI settings

-
+
+ + {loading ? (
diff --git a/src/lib/claude-client.ts b/src/lib/claude-client.ts index 76ff14a..09b4d95 100644 --- a/src/lib/claude-client.ts +++ b/src/lib/claude-client.ts @@ -14,6 +14,7 @@ import type { } from '@anthropic-ai/claude-agent-sdk'; import type { ClaudeStreamOptions, SSEEvent, TokenUsage, MCPServerConfig, PermissionRequestEvent } from '@/types'; import { registerPendingPermission } from './permission-registry'; +import { getSetting } from './db'; import { execFileSync } from 'child_process'; import os from 'os'; import path from 'path'; @@ -132,15 +133,26 @@ export function streamClaude(options: ClaudeStreamOptions): ReadableStream({ async start(controller) { try { + // Build env for the Claude Code subprocess. + // Start with process.env (includes user shell env from Electron's loadUserShellEnv). + // Then overlay any API config the user set in CodePilot settings (optional). + const sdkEnv: Record = { ...process.env as Record }; + + const appToken = getSetting('anthropic_auth_token'); + const appBaseUrl = getSetting('anthropic_base_url'); + if (appToken) { + sdkEnv.ANTHROPIC_AUTH_TOKEN = appToken; + } + if (appBaseUrl) { + sdkEnv.ANTHROPIC_BASE_URL = appBaseUrl; + } + const queryOptions: Options = { cwd: workingDirectory || process.cwd(), abortController, includePartialMessages: true, permissionMode: (permissionMode as Options['permissionMode']) || 'acceptEdits', - // Explicitly pass the full process.env to the Claude Code subprocess. - // In packaged Electron, this includes user shell env vars (API keys, etc.) - // loaded by the main process via loadUserShellEnv(). - env: process.env as Record, + env: sdkEnv, }; // Find claude binary for packaged app where PATH is limited