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:
1 parent
e4ae2154af
commit
8b27f69a6f
3 files changed
+190
-6
No files matched your search
@@ -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
@@ -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" />
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user