diff --git a/apps/backend/runners/insights_runner.py b/apps/backend/runners/insights_runner.py index a2de9f9408..c688853cc6 100644 --- a/apps/backend/runners/insights_runner.py +++ b/apps/backend/runners/insights_runner.py @@ -9,6 +9,7 @@ import argparse import asyncio import json +import os import sys from pathlib import Path @@ -132,7 +133,7 @@ async def run_with_sdk( project_dir: str, message: str, history: list, - model: str = "claude-sonnet-4-5-20250929", + model: str = None, thinking_level: str = "medium", ) -> None: """Run the chat using Claude SDK with streaming.""" @@ -152,6 +153,10 @@ async def run_with_sdk( # Ensure SDK can find the token ensure_claude_code_oauth_token() + # Use environment variable for model if not provided + if model is None: + model = os.getenv("AUTO_BUILD_MODEL", "claude-sonnet-4-5-20250929") + system_prompt = build_system_prompt(project_dir) project_path = Path(project_dir).resolve() @@ -336,8 +341,8 @@ def main(): ) parser.add_argument( "--model", - default="claude-sonnet-4-5-20250929", - help="Claude model ID (default: claude-sonnet-4-5-20250929)", + default=None, + help="Claude model ID (default: from AUTO_BUILD_MODEL env var or claude-sonnet-4-5-20250929)", ) parser.add_argument( "--thinking-level", diff --git a/apps/frontend/src/main/ipc-handlers/settings-handlers.ts b/apps/frontend/src/main/ipc-handlers/settings-handlers.ts index 3714b7f1d2..823c8754e4 100644 --- a/apps/frontend/src/main/ipc-handlers/settings-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/settings-handlers.ts @@ -1,5 +1,5 @@ import { ipcMain, dialog, app, shell } from 'electron'; -import { existsSync, writeFileSync, mkdirSync, statSync } from 'fs'; +import { existsSync, writeFileSync, mkdirSync, statSync, readFileSync } from 'fs'; import { execFileSync } from 'node:child_process'; import path from 'path'; import { is } from '@electron-toolkit/utils'; @@ -442,4 +442,109 @@ export function registerSettingsHandlers( } } ); + + // ============================================ + // Custom API Settings + // ============================================ + + ipcMain.handle( + IPC_CHANNELS.SETTINGS_SAVE_CUSTOM_API, + async ( + _, + settings: { baseUrl: string; authToken: string; model?: string } + ): Promise => { + try { + // Load current settings to get autoBuildPath + const savedSettings = readSettingsFile(); + const currentSettings = { ...DEFAULT_APP_SETTINGS, ...savedSettings }; + let autoBuildPath = currentSettings.autoBuildPath || detectAutoBuildSourcePath(); + + // Fallback: try development paths if detected path doesn't exist + if (!autoBuildPath || !existsSync(autoBuildPath)) { + const devPaths = [ + path.resolve(process.cwd(), 'apps', 'backend'), + path.resolve(__dirname, '..', '..', '..', 'backend'), + path.resolve(__dirname, '..', '..', '..', '..', 'apps', 'backend') + ]; + + for (const p of devPaths) { + if (existsSync(p) && existsSync(path.join(p, 'runners', 'spec_runner.py'))) { + autoBuildPath = p; + console.log('[SAVE_CUSTOM_API_SETTINGS] Using fallback path:', p); + break; + } + } + } + + if (!autoBuildPath || !existsSync(autoBuildPath)) { + return { + success: false, + error: 'Auto Claude backend path not found. Searched paths:\n' + + `- ${currentSettings.autoBuildPath || 'not configured'}\n` + + `- ${process.cwd()}/apps/backend\n` + + 'Please configure backend path in settings.' + }; + } + + const envPath = path.join(autoBuildPath, '.env'); + console.log('[SAVE_CUSTOM_API_SETTINGS] Writing to:', envPath); + + // Read existing .env file or create new content + let envContent = ''; + if (existsSync(envPath)) { + envContent = readFileSync(envPath, 'utf-8'); + } else { + console.log('[SAVE_CUSTOM_API_SETTINGS] .env file does not exist, creating new one'); + } + + // Helper function to escape special regex characters + const escapeRegex = (str: string): string => { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + }; + + // Update or add settings with proper escaping + const updateEnvVar = (content: string, key: string, value: string): string => { + const escapedKey = escapeRegex(key); + const regex = new RegExp(`^${escapedKey}=.*$`, 'm'); + if (regex.test(content)) { + // Use function replacement to avoid $ interpretation in value + return content.replace(regex, () => `${key}=${value}`); + } else { + return content + `\n${key}=${value}`; + } + }; + + envContent = updateEnvVar(envContent, 'ANTHROPIC_BASE_URL', settings.baseUrl); + envContent = updateEnvVar(envContent, 'ANTHROPIC_AUTH_TOKEN', settings.authToken); + + try { + const hostname = new URL(settings.baseUrl).hostname; + envContent = updateEnvVar(envContent, 'NO_PROXY', hostname); + } catch { + // If URL parsing fails, skip NO_PROXY + console.warn('[SAVE_CUSTOM_API_SETTINGS] Failed to parse URL for NO_PROXY'); + } + + envContent = updateEnvVar(envContent, 'DISABLE_TELEMETRY', 'true'); + envContent = updateEnvVar(envContent, 'DISABLE_COST_WARNINGS', 'true'); + envContent = updateEnvVar(envContent, 'API_TIMEOUT_MS', '600000'); + + if (settings.model) { + envContent = updateEnvVar(envContent, 'AUTO_BUILD_MODEL', settings.model); + } + + // Write back to .env file + writeFileSync(envPath, envContent.trim() + '\n'); + console.log('[SAVE_CUSTOM_API_SETTINGS] Successfully saved settings to:', envPath); + + return { success: true }; + } catch (error) { + console.error('[SAVE_CUSTOM_API_SETTINGS] Error:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to save custom API settings' + }; + } + } + ); } diff --git a/apps/frontend/src/preload/api/settings-api.ts b/apps/frontend/src/preload/api/settings-api.ts index 263c32d084..bf415d5c3c 100644 --- a/apps/frontend/src/preload/api/settings-api.ts +++ b/apps/frontend/src/preload/api/settings-api.ts @@ -24,6 +24,13 @@ export interface SettingsAPI { // App Info getAppVersion: () => Promise; + // Custom API Settings + saveCustomApiSettings: (settings: { + baseUrl: string; + authToken: string; + model?: string; + }) => Promise; + // Auto-Build Source Environment getSourceEnv: () => Promise>; updateSourceEnv: (config: { claudeOAuthToken?: string }) => Promise; @@ -51,6 +58,14 @@ export const createSettingsAPI = (): SettingsAPI => ({ getAppVersion: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.APP_VERSION), + // Custom API Settings + saveCustomApiSettings: (settings: { + baseUrl: string; + authToken: string; + model?: string; + }): Promise => + ipcRenderer.invoke(IPC_CHANNELS.SETTINGS_SAVE_CUSTOM_API, settings), + // Auto-Build Source Environment getSourceEnv: (): Promise> => ipcRenderer.invoke(IPC_CHANNELS.AUTOBUILD_SOURCE_ENV_GET), diff --git a/apps/frontend/src/renderer/components/onboarding/OAuthStep.tsx b/apps/frontend/src/renderer/components/onboarding/OAuthStep.tsx index 7584f864ea..577db81017 100644 --- a/apps/frontend/src/renderer/components/onboarding/OAuthStep.tsx +++ b/apps/frontend/src/renderer/components/onboarding/OAuthStep.tsx @@ -60,6 +60,14 @@ export function OAuthStep({ onNext, onBack, onSkip }: OAuthStepProps) { const [showManualToken, setShowManualToken] = useState(false); const [savingTokenProfileId, setSavingTokenProfileId] = useState(null); + // Custom API settings state + const [showCustomApi, setShowCustomApi] = useState(false); + const [customBaseUrl, setCustomBaseUrl] = useState(''); + const [customAuthToken, setCustomAuthToken] = useState(''); + const [customModel, setCustomModel] = useState(''); + const [showCustomAuthToken, setShowCustomAuthToken] = useState(false); + const [savingCustomApi, setSavingCustomApi] = useState(false); + // Error state const [error, setError] = useState(null); @@ -281,6 +289,36 @@ export function OAuthStep({ onNext, onBack, onSkip }: OAuthStepProps) { } }; + const handleSaveCustomApi = async () => { + if (!customBaseUrl.trim() || !customAuthToken.trim()) { + setError('Base URL and Auth Token are required'); + return; + } + + setSavingCustomApi(true); + setError(null); + try { + // Save to .env file via backend + const result = await window.electronAPI.saveCustomApiSettings({ + baseUrl: customBaseUrl.trim(), + authToken: customAuthToken.trim(), + model: customModel.trim() || undefined + }); + + if (result.success) { + alert('✅ Custom API settings saved successfully!\n\nPlease restart the app for changes to take effect.'); + setShowCustomApi(false); + } else { + alert(`Failed to save settings: ${result.error || 'Please try again.'}`); + } + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to save custom API settings'); + alert('Failed to save settings. Please try again.'); + } finally { + setSavingCustomApi(false); + } + }; + const handleContinue = () => { onNext(); }; @@ -594,6 +632,108 @@ export function OAuthStep({ onNext, onBack, onSkip }: OAuthStepProps) { )} + {/* Custom API Settings Section */} +
+ + + {showCustomApi && ( +
+

+ {t('onboarding:oauth.customApi.description')} +

+ +
+ + setCustomBaseUrl(e.target.value)} + className="font-mono text-xs h-9" + /> +
+ +
+ +
+ setCustomAuthToken(e.target.value)} + className="pr-10 font-mono text-xs h-9" + /> + +
+
+ +
+ + setCustomModel(e.target.value)} + className="font-mono text-xs h-9" + /> +

+ {t('onboarding:oauth.customApi.modelDescription')} +

+
+ +
+ + +
+
+ )} +
+ {/* Add new account input */}
Promise; /** Set OAuth token for a profile (used when capturing from terminal) */ setClaudeProfileToken: (profileId: string, token: string, email?: string) => Promise; + /** Save custom API settings to .env file */ + saveCustomApiSettings: (settings: { + baseUrl: string; + authToken: string; + model?: string; + }) => Promise; /** Get auto-switch settings */ getAutoSwitchSettings: () => Promise>; /** Update auto-switch settings */