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 <noreply@anthropic.com>
This commit is contained in:
7418andClaude Opus 4.6 committed 2026-02-06 23:17:49 +08:00
1 parent e4ae2154af
commit 8b27f69a6f
3 files changed
+190 -6

No files matched your search

+64
View File
@@ -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<string, string> = {};
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 });
}
}
+110 -2
View File
@@ -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 (
<div className="rounded-lg border border-border/50 p-4 space-y-4">
<div>
<Label className="text-sm font-medium">API Configuration</Label>
<p className="text-xs text-muted-foreground">
Optional. Configure a custom Anthropic-compatible API for Claude Code.
Leave empty to use the default authentication (claude login / shell environment).
</p>
</div>
<div className="space-y-3">
<div>
<Label htmlFor="api-base-url" className="text-xs text-muted-foreground">
API Base URL
</Label>
<Input
id="api-base-url"
placeholder="https://api.anthropic.com"
value={baseUrl}
onChange={(e) => setBaseUrl(e.target.value)}
className="mt-1 font-mono text-sm"
/>
</div>
<div>
<Label htmlFor="api-token" className="text-xs text-muted-foreground">
API Token (ANTHROPIC_AUTH_TOKEN)
</Label>
<Input
id="api-token"
type="password"
placeholder="sk-ant-..."
value={token}
onChange={(e) => setToken(e.target.value)}
className="mt-1 font-mono text-sm"
/>
</div>
</div>
<div className="flex items-center gap-3">
<Button onClick={handleSave} disabled={saving} size="sm" className="gap-2">
{saving ? (
<HugeiconsIcon icon={Loading02Icon} className="h-4 w-4 animate-spin" />
) : (
<HugeiconsIcon icon={FloppyDiskIcon} className="h-4 w-4" />
)}
{saving ? "Saving..." : "Save API Config"}
</Button>
{status === "saved" && (
<span className="text-sm text-green-600 dark:text-green-400">Saved</span>
)}
{status === "error" && (
<span className="text-sm text-destructive">Failed to save</span>
)}
</div>
</div>
);
}
// --- Claude CLI Settings Section (manages ~/.claude/settings.json) ---
function SettingsPageInner() {
const [settings, setSettings] = useState<SettingsData>({});
const [originalSettings, setOriginalSettings] = useState<SettingsData>({});
@@ -168,12 +274,14 @@ function SettingsPageInner() {
<div className="border-b border-border/50 px-6 pt-4 pb-4">
<h1 className="text-xl font-semibold">Settings</h1>
<p className="text-sm text-muted-foreground">
Manage your Claude CLI settings (~/.claude/settings.json)
Manage CodePilot and Claude CLI settings
</p>
</div>
<div className="flex-1 overflow-auto p-6">
<div className="max-w-3xl">
<div className="max-w-3xl space-y-6">
<ApiConfigSection />
{loading ? (
<div className="flex items-center justify-center py-12">
<HugeiconsIcon icon={Loading02Icon} className="h-5 w-5 animate-spin text-muted-foreground" />
+16 -4
View File
@@ -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<strin
return new ReadableStream<string>({
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<string, string> = { ...process.env as Record<string, string> };
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<string, string>,
env: sdkEnv,
};
// Find claude binary for packaged app where PATH is limited