From c0837b922ce89259721cf483a4b9d22c906b7bd1 Mon Sep 17 00:00:00 2001 From: Rodion Kuprin <468468@gmail.com> Date: Thu, 8 Jan 2026 10:52:07 +0200 Subject: [PATCH 1/3] feat: add Anthropic API base URL configuration support - Added support for configuring custom Anthropic API base URLs to enable enterprise deployments and custom endpoints. - Enhanced the Credentials interface to include optional baseUrls field for storing custom API endpoints. - Updated the Claude provider to support ANTHROPIC_BASE_URL environment variable for the Anthropic SDK. - Modified the SettingsService to handle base URLs alongside API keys with proper persistence. - Updated setup API routes to accept and store base URL configuration from the UI. - Combined API key and base URL inputs into a single cohesive configuration section in the setup UI. - Enhanced the HTTP client and Electron API to pass base URL parameters through the save flow. - Updated all type definitions to ensure type safety for the new base URL functionality. This allows users to configure custom Anthropic API endpoints while maintaining backward compatibility for existing users who don't need this feature. Closes #378 --- apps/server/src/index.ts | 2 +- apps/server/src/providers/claude-provider.ts | 1 + apps/server/src/routes/setup/index.ts | 5 +-- .../src/routes/setup/routes/store-api-key.ts | 28 +++++++++++++-- apps/server/src/services/settings-service.ts | 13 +++++-- .../views/setup-view/hooks/use-token-save.ts | 6 ++-- .../setup-view/steps/claude-setup-step.tsx | 36 +++++++++++++++---- apps/ui/src/lib/electron.ts | 12 +++++-- apps/ui/src/lib/http-api-client.ts | 5 +-- libs/types/src/settings.ts | 8 +++++ 10 files changed, 94 insertions(+), 22 deletions(-) diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 9ba53ed86..294d7a65a 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -206,7 +206,7 @@ app.use('/api/auto-mode', createAutoModeRoutes(autoModeService)); app.use('/api/enhance-prompt', createEnhancePromptRoutes(settingsService)); app.use('/api/worktree', createWorktreeRoutes()); app.use('/api/git', createGitRoutes()); -app.use('/api/setup', createSetupRoutes()); +app.use('/api/setup', createSetupRoutes(settingsService)); app.use('/api/suggestions', createSuggestionsRoutes(events, settingsService)); app.use('/api/models', createModelsRoutes()); app.use('/api/spec-regeneration', createSpecRegenerationRoutes(events, settingsService)); diff --git a/apps/server/src/providers/claude-provider.ts b/apps/server/src/providers/claude-provider.ts index 50e378beb..7c6363ba9 100644 --- a/apps/server/src/providers/claude-provider.ts +++ b/apps/server/src/providers/claude-provider.ts @@ -22,6 +22,7 @@ import type { // Only these vars are passed - nothing else from process.env leaks through. const ALLOWED_ENV_VARS = [ 'ANTHROPIC_API_KEY', + 'ANTHROPIC_BASE_URL', 'PATH', 'HOME', 'SHELL', diff --git a/apps/server/src/routes/setup/index.ts b/apps/server/src/routes/setup/index.ts index 6c9f42a2f..1bf092288 100644 --- a/apps/server/src/routes/setup/index.ts +++ b/apps/server/src/routes/setup/index.ts @@ -3,6 +3,7 @@ */ import { Router } from 'express'; +import type { SettingsService } from '../../services/settings-service.js'; import { createClaudeStatusHandler } from './routes/claude-status.js'; import { createInstallClaudeHandler } from './routes/install-claude.js'; import { createAuthClaudeHandler } from './routes/auth-claude.js'; @@ -24,13 +25,13 @@ import { createGetExampleConfigHandler, } from './routes/cursor-config.js'; -export function createSetupRoutes(): Router { +export function createSetupRoutes(settingsService: SettingsService): Router { const router = Router(); router.get('/claude-status', createClaudeStatusHandler()); router.post('/install-claude', createInstallClaudeHandler()); router.post('/auth-claude', createAuthClaudeHandler()); - router.post('/store-api-key', createStoreApiKeyHandler()); + router.post('/store-api-key', createStoreApiKeyHandler(settingsService)); router.post('/delete-api-key', createDeleteApiKeyHandler()); router.get('/api-keys', createApiKeysHandler()); router.get('/platform', createPlatformHandler()); diff --git a/apps/server/src/routes/setup/routes/store-api-key.ts b/apps/server/src/routes/setup/routes/store-api-key.ts index e77a697e8..4e0c67140 100644 --- a/apps/server/src/routes/setup/routes/store-api-key.ts +++ b/apps/server/src/routes/setup/routes/store-api-key.ts @@ -5,15 +5,17 @@ import type { Request, Response } from 'express'; import { setApiKey, persistApiKeyToEnv, getErrorMessage, logError } from '../common.js'; import { createLogger } from '@automaker/utils'; +import type { SettingsService } from '../../../services/settings-service.js'; const logger = createLogger('Setup'); -export function createStoreApiKeyHandler() { +export function createStoreApiKeyHandler(settingsService: SettingsService) { return async (req: Request, res: Response): Promise => { try { - const { provider, apiKey } = req.body as { + const { provider, apiKey, baseUrl } = req.body as { provider: string; apiKey: string; + baseUrl?: string; }; if (!provider || !apiKey) { @@ -29,6 +31,28 @@ export function createStoreApiKeyHandler() { process.env.ANTHROPIC_API_KEY = apiKey; await persistApiKeyToEnv('ANTHROPIC_API_KEY', apiKey); logger.info('[Setup] Stored API key as ANTHROPIC_API_KEY'); + + // Handle custom base URL if provided + if (baseUrl && baseUrl.trim()) { + process.env.ANTHROPIC_BASE_URL = baseUrl.trim(); + await persistApiKeyToEnv('ANTHROPIC_BASE_URL', baseUrl.trim()); + logger.info('[Setup] Stored custom base URL as ANTHROPIC_BASE_URL'); + } else if (process.env.ANTHROPIC_BASE_URL) { + // Clear existing base URL if not provided + delete process.env.ANTHROPIC_BASE_URL; + // Note: we don't remove from .env file as that's more complex + logger.info('[Setup] Cleared ANTHROPIC_BASE_URL from environment'); + } + + // Persist base URL to settings service + if (baseUrl !== undefined) { + await settingsService.updateCredentials({ + baseUrls: { + anthropic: baseUrl.trim() || undefined, + }, + }); + logger.info('[Setup] Persisted base URL to settings service'); + } } else { res.status(400).json({ success: false, diff --git a/apps/server/src/services/settings-service.ts b/apps/server/src/services/settings-service.ts index 94bdce249..72e0caefb 100644 --- a/apps/server/src/services/settings-service.ts +++ b/apps/server/src/services/settings-service.ts @@ -334,7 +334,7 @@ export class SettingsService { /** * Update credentials with partial changes * - * Updates individual API keys. Uses deep merge for apiKeys object. + * Updates individual API keys. Uses deep merge for apiKeys and baseUrls objects. * Creates dataDir if needed. Credentials are written atomically. * WARNING: Use only in secure contexts - keys are unencrypted. * @@ -360,6 +360,14 @@ export class SettingsService { }; } + // Deep merge base URLs if provided + if (updates.baseUrls) { + updated.baseUrls = { + ...current.baseUrls, + ...updates.baseUrls, + }; + } + await atomicWriteJson(credentialsPath, updated); logger.info('Credentials updated'); @@ -376,7 +384,7 @@ export class SettingsService { * @returns Promise resolving to masked credentials object with each provider's status */ async getMaskedCredentials(): Promise<{ - anthropic: { configured: boolean; masked: string }; + anthropic: { configured: boolean; masked: string; baseUrl?: string }; }> { const credentials = await this.getCredentials(); @@ -389,6 +397,7 @@ export class SettingsService { anthropic: { configured: !!credentials.apiKeys.anthropic, masked: maskKey(credentials.apiKeys.anthropic), + baseUrl: credentials.baseUrls?.anthropic, }, }; } diff --git a/apps/ui/src/components/views/setup-view/hooks/use-token-save.ts b/apps/ui/src/components/views/setup-view/hooks/use-token-save.ts index 8168f4454..b16c5b9fc 100644 --- a/apps/ui/src/components/views/setup-view/hooks/use-token-save.ts +++ b/apps/ui/src/components/views/setup-view/hooks/use-token-save.ts @@ -14,7 +14,7 @@ export function useTokenSave({ provider, onSuccess }: UseTokenSaveOptions) { const [isSaving, setIsSaving] = useState(false); const saveToken = useCallback( - async (tokenValue: string) => { + async (tokenValue: string, baseUrl?: string) => { if (!tokenValue.trim()) { toast.error('Please enter a valid token'); return false; @@ -26,11 +26,11 @@ export function useTokenSave({ provider, onSuccess }: UseTokenSaveOptions) { const setupApi = api.setup; if (setupApi?.storeApiKey) { - const result = await setupApi.storeApiKey(provider, tokenValue); + const result = await setupApi.storeApiKey(provider, tokenValue, baseUrl); logger.info(`Store result for ${provider}:`, result); if (result.success) { - const tokenType = provider.includes('oauth') ? 'subscription token' : 'API key'; + const tokenType = provider.includes('oauth') ? 'subscription token' : 'configuration'; toast.success(`${tokenType} saved successfully`); onSuccess?.(); return true; diff --git a/apps/ui/src/components/views/setup-view/steps/claude-setup-step.tsx b/apps/ui/src/components/views/setup-view/steps/claude-setup-step.tsx index 529cfc026..867701963 100644 --- a/apps/ui/src/components/views/setup-view/steps/claude-setup-step.tsx +++ b/apps/ui/src/components/views/setup-view/steps/claude-setup-step.tsx @@ -55,6 +55,7 @@ export function ClaudeSetupStep({ onNext, onBack, onSkip }: ClaudeSetupStepProps const { setApiKeys, apiKeys } = useAppStore(); const [apiKey, setApiKey] = useState(''); + const [baseUrl, setBaseUrl] = useState(''); // CLI Verification state const [cliVerificationStatus, setCliVerificationStatus] = useState('idle'); @@ -111,7 +112,7 @@ export function ClaudeSetupStep({ onNext, onBack, onSkip }: ClaudeSetupStepProps apiKeyValid: true, }); setApiKeys({ ...apiKeys, anthropic: apiKey }); - toast.success('API key saved successfully!'); + toast.success('Anthropic configuration saved successfully!'); }, }); @@ -220,6 +221,7 @@ export function ClaudeSetupStep({ onNext, onBack, onSkip }: ClaudeSetupStepProps if (result.success) { // Clear local state setApiKey(''); + setBaseUrl(''); setApiKeys({ ...apiKeys, anthropic: '' }); setApiKeyVerificationStatus('idle'); setApiKeyVerificationError(null); @@ -228,12 +230,13 @@ export function ClaudeSetupStep({ onNext, onBack, onSkip }: ClaudeSetupStepProps method: 'none', hasCredentialsFile: claudeAuthStatus?.hasCredentialsFile || false, }); - toast.success('API key deleted successfully'); + toast.success('Anthropic configuration deleted successfully'); } else { - toast.error(result.error || 'Failed to delete API key'); + toast.error(result.error || 'Failed to delete configuration'); } } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Failed to delete API key'; + const errorMessage = + error instanceof Error ? error.message : 'Failed to delete configuration'; toast.error(errorMessage); } finally { setIsDeletingApiKey(false); @@ -531,7 +534,7 @@ export function ClaudeSetupStep({ onNext, onBack, onSkip }: ClaudeSetupStepProps - {/* API Key Input */} + {/* API Key Configuration */}
+
+ + setBaseUrl(e.target.value)} + className="bg-input border-border text-foreground" + data-testid="anthropic-base-url-input" + /> +

+ Optional: Set a custom API base URL for enterprise deployments or custom + endpoints +

+
+
{hasApiKey && ( diff --git a/apps/ui/src/lib/electron.ts b/apps/ui/src/lib/electron.ts index d81b46b6e..95511cab7 100644 --- a/apps/ui/src/lib/electron.ts +++ b/apps/ui/src/lib/electron.ts @@ -649,7 +649,8 @@ export interface ElectronAPI { }>; storeApiKey: ( provider: string, - apiKey: string + apiKey: string, + baseUrl?: string ) => Promise<{ success: boolean; error?: string }>; deleteApiKey: ( provider: string @@ -1219,7 +1220,11 @@ interface SetupAPI { message?: string; output?: string; }>; - storeApiKey: (provider: string, apiKey: string) => Promise<{ success: boolean; error?: string }>; + storeApiKey: ( + provider: string, + apiKey: string, + baseUrl?: string + ) => Promise<{ success: boolean; error?: string }>; getApiKeys: () => Promise<{ success: boolean; hasAnthropicKey: boolean; @@ -1295,8 +1300,9 @@ function createMockSetupAPI(): SetupAPI { }; }, - storeApiKey: async (provider: string, apiKey: string) => { + storeApiKey: async (provider: string, apiKey: string, baseUrl?: string) => { console.log('[Mock] Storing API key for:', provider); + console.log('[Mock] Base URL:', baseUrl); // In mock mode, we just pretend to store it (it's already in the app store) return { success: true }; }, diff --git a/apps/ui/src/lib/http-api-client.ts b/apps/ui/src/lib/http-api-client.ts index 9bf58d8e1..3c73e5cba 100644 --- a/apps/ui/src/lib/http-api-client.ts +++ b/apps/ui/src/lib/http-api-client.ts @@ -1011,11 +1011,12 @@ export class HttpApiClient implements ElectronAPI { storeApiKey: ( provider: string, - apiKey: string + apiKey: string, + baseUrl?: string ): Promise<{ success: boolean; error?: string; - }> => this.post('/api/setup/store-api-key', { provider, apiKey }), + }> => this.post('/api/setup/store-api-key', { provider, apiKey, baseUrl }), deleteApiKey: ( provider: string diff --git a/libs/types/src/settings.ts b/libs/types/src/settings.ts index 0220da3aa..c6d1b8952 100644 --- a/libs/types/src/settings.ts +++ b/libs/types/src/settings.ts @@ -506,6 +506,11 @@ export interface Credentials { /** OpenAI API key (for compatibility or alternative providers) */ openai: string; }; + /** Custom base URLs for API providers (optional) */ + baseUrls?: { + /** Custom Anthropic API base URL (sets ANTHROPIC_BASE_URL) */ + anthropic?: string; + }; } /** @@ -685,6 +690,9 @@ export const DEFAULT_CREDENTIALS: Credentials = { google: '', openai: '', }, + baseUrls: { + anthropic: undefined, + }, }; /** Default project settings (empty - all settings are optional and fall back to global) */ From f651c3b1fdae8df9ac3cab40a7420ccaeac0d9d0 Mon Sep 17 00:00:00 2001 From: Rodion Kuprin <468468@gmail.com> Date: Thu, 8 Jan 2026 22:05:53 +0200 Subject: [PATCH 2/3] fix: enhance CLI verification by managing API key and base URL environment variables - Updated the verification handler to clear both the API key and base URL when using CLI authentication. - Restored the original base URL after verification, ensuring proper environment management for different authentication methods. - Improved logging to reflect changes in environment variable handling during CLI verification. --- .../src/routes/setup/routes/verify-claude-auth.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/apps/server/src/routes/setup/routes/verify-claude-auth.ts b/apps/server/src/routes/setup/routes/verify-claude-auth.ts index c202ff968..b0737a2a9 100644 --- a/apps/server/src/routes/setup/routes/verify-claude-auth.ts +++ b/apps/server/src/routes/setup/routes/verify-claude-auth.ts @@ -91,13 +91,16 @@ export function createVerifyClaudeAuthHandler() { // Save original env values const originalAnthropicKey = process.env.ANTHROPIC_API_KEY; + const originalBaseUrl = process.env.ANTHROPIC_BASE_URL; try { // Configure environment based on auth method if (authMethod === 'cli') { // For CLI verification, remove any API key so it uses CLI credentials only delete process.env.ANTHROPIC_API_KEY; - logger.info('[Setup] Cleared API key environment for CLI verification'); + // CLI auth only works with standard Anthropic API, not custom endpoints + delete process.env.ANTHROPIC_BASE_URL; + logger.info('[Setup] Cleared API key and base URL for CLI verification'); } else if (authMethod === 'api_key') { // For API key verification, use provided key, stored key, or env var (in order of priority) if (apiKey) { @@ -285,6 +288,13 @@ export function createVerifyClaudeAuthHandler() { // If we cleared it and there was no original, keep it cleared delete process.env.ANTHROPIC_API_KEY; } + // Restore base URL + if (originalBaseUrl !== undefined) { + process.env.ANTHROPIC_BASE_URL = originalBaseUrl; + } else if (authMethod === 'cli') { + // If we cleared it and there was no original, keep it cleared + delete process.env.ANTHROPIC_BASE_URL; + } } logger.info('[Setup] Verification result:', { From 77cf672d80f0b2e7603121fd231576dbbf920918 Mon Sep 17 00:00:00 2001 From: Rodion Kuprin <468468@gmail.com> Date: Fri, 9 Jan 2026 09:18:27 +0200 Subject: [PATCH 3/3] feat: implement environment variable management for SDK - Added a function to build an environment object containing explicitly allowed environment variables for the SDK. - This ensures that only specified variables are passed to the SDK, enhancing security and control over the environment. - Updated the verification handler to utilize the new environment management function during authentication. --- .../routes/setup/routes/verify-claude-auth.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/apps/server/src/routes/setup/routes/verify-claude-auth.ts b/apps/server/src/routes/setup/routes/verify-claude-auth.ts index b0737a2a9..8a1736e55 100644 --- a/apps/server/src/routes/setup/routes/verify-claude-auth.ts +++ b/apps/server/src/routes/setup/routes/verify-claude-auth.ts @@ -10,6 +10,33 @@ import { getApiKey } from '../common.js'; const logger = createLogger('Setup'); +// Allowed environment variables to pass to the SDK +// Must be passed explicitly - SDK doesn't inherit from process.env +const ALLOWED_ENV_VARS = [ + 'ANTHROPIC_API_KEY', + 'ANTHROPIC_BASE_URL', + 'PATH', + 'HOME', + 'SHELL', + 'TERM', + 'USER', + 'LANG', + 'LC_ALL', +]; + +/** + * Build environment for the SDK with only explicitly allowed variables + */ +function buildEnv(): Record { + const env: Record = {}; + for (const key of ALLOWED_ENV_VARS) { + if (process.env[key]) { + env[key] = process.env[key]; + } + } + return env; +} + // Known error patterns that indicate auth failure const AUTH_ERROR_PATTERNS = [ 'OAuth token revoked', @@ -131,6 +158,7 @@ export function createVerifyClaudeAuthHandler() { maxTurns: 1, allowedTools: [], abortController, + env: buildEnv(), }, });