diff --git a/ui/src/components/add-account-dialog.tsx b/ui/src/components/add-account-dialog.tsx index f9a80af7..fa01fa4e 100644 --- a/ui/src/components/add-account-dialog.tsx +++ b/ui/src/components/add-account-dialog.tsx @@ -1,6 +1,7 @@ /** * Add Account Dialog Component * Triggers OAuth flow server-side to add another account to a provider + * Applies default preset when adding first account */ import { useState } from 'react'; @@ -16,15 +17,25 @@ import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Loader2, ExternalLink, User } from 'lucide-react'; import { useStartAuth } from '@/hooks/use-cliproxy'; +import { applyDefaultPreset } from '@/lib/preset-utils'; +import { toast } from 'sonner'; interface AddAccountDialogProps { open: boolean; onClose: () => void; provider: string; displayName: string; + /** Whether this is the first account being added (triggers preset application) */ + isFirstAccount?: boolean; } -export function AddAccountDialog({ open, onClose, provider, displayName }: AddAccountDialogProps) { +export function AddAccountDialog({ + open, + onClose, + provider, + displayName, + isFirstAccount = false, +}: AddAccountDialogProps) { const [nickname, setNickname] = useState(''); const startAuthMutation = useStartAuth(); @@ -32,7 +43,16 @@ export function AddAccountDialog({ open, onClose, provider, displayName }: AddAc startAuthMutation.mutate( { provider, nickname: nickname.trim() || undefined }, { - onSuccess: () => { + onSuccess: async () => { + // Apply default preset if this is the first account + if (isFirstAccount) { + const result = await applyDefaultPreset(provider); + if (result.success && result.presetName) { + toast.success(`Applied "${result.presetName}" preset`); + } else if (!result.success) { + toast.warning('Account added, but failed to apply default preset'); + } + } setNickname(''); onClose(); }, diff --git a/ui/src/components/cliproxy/provider-editor.tsx b/ui/src/components/cliproxy/provider-editor.tsx index a1c637e8..0d633340 100644 --- a/ui/src/components/cliproxy/provider-editor.tsx +++ b/ui/src/components/cliproxy/provider-editor.tsx @@ -57,6 +57,7 @@ import { useDeletePreset, } from '@/hooks/use-cliproxy'; import { cn } from '@/lib/utils'; +import { CLIPROXY_PORT } from '@/lib/preset-utils'; import { GlobalEnvIndicator } from '@/components/global-env-indicator'; import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context'; @@ -298,7 +299,7 @@ export function ProviderEditor({ }; // Always include BASE_URL and AUTH_TOKEN for CLIProxy providers updateEnvValues({ - ANTHROPIC_BASE_URL: `http://127.0.0.1:8317/api/provider/${provider}`, + ANTHROPIC_BASE_URL: `http://127.0.0.1:${CLIPROXY_PORT}/api/provider/${provider}`, ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed', ANTHROPIC_MODEL: mapping.default, ANTHROPIC_DEFAULT_OPUS_MODEL: mapping.opus, @@ -323,7 +324,7 @@ export function ProviderEditor({ onClick={() => { // Always include BASE_URL and AUTH_TOKEN for CLIProxy providers updateEnvValues({ - ANTHROPIC_BASE_URL: `http://127.0.0.1:8317/api/provider/${provider}`, + ANTHROPIC_BASE_URL: `http://127.0.0.1:${CLIPROXY_PORT}/api/provider/${provider}`, ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed', ANTHROPIC_MODEL: preset.default, ANTHROPIC_DEFAULT_OPUS_MODEL: preset.opus, @@ -663,7 +664,7 @@ export function ProviderEditor({ onApply={(values, presetName) => { // Always include BASE_URL and AUTH_TOKEN for CLIProxy providers updateEnvValues({ - ANTHROPIC_BASE_URL: `http://127.0.0.1:8317/api/provider/${provider}`, + ANTHROPIC_BASE_URL: `http://127.0.0.1:${CLIPROXY_PORT}/api/provider/${provider}`, ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed', ANTHROPIC_MODEL: values.default, ANTHROPIC_DEFAULT_OPUS_MODEL: values.opus, diff --git a/ui/src/components/quick-setup-wizard.tsx b/ui/src/components/quick-setup-wizard.tsx index 1fc36d70..930446ab 100644 --- a/ui/src/components/quick-setup-wizard.tsx +++ b/ui/src/components/quick-setup-wizard.tsx @@ -39,8 +39,10 @@ import { import { useCliproxyAuth, useCreateVariant, useStartAuth } from '@/hooks/use-cliproxy'; import type { AuthStatus, OAuthAccount } from '@/lib/api-client'; import { MODEL_CATALOGS } from '@/lib/model-catalogs'; +import { applyDefaultPreset } from '@/lib/preset-utils'; import { cn } from '@/lib/utils'; import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context'; +import { toast } from 'sonner'; interface QuickSetupWizardProps { open: boolean; @@ -118,10 +120,23 @@ export function QuickSetupWizard({ open, onClose }: QuickSetupWizardProps) { }; const handleStartAuth = () => { + // Check if this is the first account before auth + const isFirstAccount = (providerAuth?.accounts?.length || 0) === 0; + startAuthMutation.mutate( { provider: selectedProvider }, { - onSuccess: (data) => { + onSuccess: async (data) => { + // Apply default preset if this was the first account + if (isFirstAccount) { + const result = await applyDefaultPreset(selectedProvider); + if (result.success && result.presetName) { + toast.success(`Applied "${result.presetName}" preset`); + } else if (!result.success) { + toast.warning('Account added, but failed to apply default preset'); + } + } + // Account created, select it and advance to variant step if (data.account) { setSelectedAccount(data.account as OAuthAccount); diff --git a/ui/src/lib/model-catalogs.ts b/ui/src/lib/model-catalogs.ts index 7cce9894..06f78c71 100644 --- a/ui/src/lib/model-catalogs.ts +++ b/ui/src/lib/model-catalogs.ts @@ -16,21 +16,45 @@ export const MODEL_CATALOGS: Record = { id: 'gemini-claude-opus-4-5-thinking', name: 'Claude Opus 4.5 Thinking', description: 'Most capable, extended thinking', + presetMapping: { + default: 'gemini-claude-opus-4-5-thinking', + opus: 'gemini-claude-opus-4-5-thinking', + sonnet: 'gemini-claude-sonnet-4-5-thinking', + haiku: 'gemini-claude-sonnet-4-5', + }, }, { id: 'gemini-claude-sonnet-4-5-thinking', name: 'Claude Sonnet 4.5 Thinking', description: 'Balanced with extended thinking', + presetMapping: { + default: 'gemini-claude-sonnet-4-5-thinking', + opus: 'gemini-claude-opus-4-5-thinking', + sonnet: 'gemini-claude-sonnet-4-5-thinking', + haiku: 'gemini-claude-sonnet-4-5', + }, }, { id: 'gemini-claude-sonnet-4-5', name: 'Claude Sonnet 4.5', description: 'Fast and capable', + presetMapping: { + default: 'gemini-claude-sonnet-4-5', + opus: 'gemini-claude-opus-4-5-thinking', + sonnet: 'gemini-claude-sonnet-4-5', + haiku: 'gemini-claude-sonnet-4-5', + }, }, { id: 'gemini-3-pro-preview', name: 'Gemini 3 Pro', description: 'Google latest model via Antigravity', + presetMapping: { + default: 'gemini-3-pro-preview', + opus: 'gemini-3-pro-preview', + sonnet: 'gemini-3-pro-preview', + haiku: 'gemini-2.5-flash', + }, }, ], }, @@ -44,11 +68,23 @@ export const MODEL_CATALOGS: Record = { name: 'Gemini 3 Pro', tier: 'paid', description: 'Latest model, requires paid Google account', + presetMapping: { + default: 'gemini-3-pro-preview', + opus: 'gemini-3-pro-preview', + sonnet: 'gemini-3-pro-preview', + haiku: 'gemini-2.5-flash', + }, }, { id: 'gemini-2.5-pro', name: 'Gemini 2.5 Pro', description: 'Stable, works with free Google account', + presetMapping: { + default: 'gemini-2.5-pro', + opus: 'gemini-2.5-pro', + sonnet: 'gemini-2.5-pro', + haiku: 'gemini-2.5-flash', + }, }, ], }, diff --git a/ui/src/lib/preset-utils.ts b/ui/src/lib/preset-utils.ts new file mode 100644 index 00000000..7b74ed6b --- /dev/null +++ b/ui/src/lib/preset-utils.ts @@ -0,0 +1,57 @@ +/** + * Preset Utilities + * Shared functions for applying default presets to provider settings + */ + +import { MODEL_CATALOGS } from './model-catalogs'; + +/** CLIProxy port - should match the backend configuration */ +export const CLIPROXY_PORT = 8317; + +/** + * Apply default preset for a provider to its settings + * Uses the first model's presetMapping or falls back to using defaultModel for all tiers + * + * @param provider - The provider ID (e.g., 'gemini', 'codex', 'agy') + * @returns Object with success status and applied preset name + */ +export async function applyDefaultPreset( + provider: string +): Promise<{ success: boolean; presetName?: string }> { + const catalog = MODEL_CATALOGS[provider]; + if (!catalog) return { success: false }; + + // Get the first (recommended) model's preset mapping + const firstModel = catalog.models[0]; + const mapping = firstModel?.presetMapping || { + default: catalog.defaultModel, + opus: catalog.defaultModel, + sonnet: catalog.defaultModel, + haiku: catalog.defaultModel, + }; + + const settings = { + env: { + ANTHROPIC_BASE_URL: `http://127.0.0.1:${CLIPROXY_PORT}/api/provider/${provider}`, + ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed', + ANTHROPIC_MODEL: mapping.default, + ANTHROPIC_DEFAULT_OPUS_MODEL: mapping.opus, + ANTHROPIC_DEFAULT_SONNET_MODEL: mapping.sonnet, + ANTHROPIC_DEFAULT_HAIKU_MODEL: mapping.haiku, + }, + }; + + try { + const res = await fetch(`/api/settings/${provider}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ settings }), + }); + return { + success: res.ok, + presetName: firstModel?.name || catalog.defaultModel, + }; + } catch { + return { success: false }; + } +} diff --git a/ui/src/pages/cliproxy.tsx b/ui/src/pages/cliproxy.tsx index 80d013fb..2a5a526d 100644 --- a/ui/src/pages/cliproxy.tsx +++ b/ui/src/pages/cliproxy.tsx @@ -188,6 +188,7 @@ export function CliproxyPage() { const [addAccountProvider, setAddAccountProvider] = useState<{ provider: string; displayName: string; + isFirstAccount: boolean; } | null>(null); const providers = authData?.authStatus || []; @@ -341,6 +342,7 @@ export function CliproxyPage() { setAddAccountProvider({ provider: selectedVariantData.provider, displayName: parentAuthForVariant.displayName, + isFirstAccount: (parentAuthForVariant.accounts?.length || 0) === 0, }) } onSetDefault={(accountId) => @@ -367,6 +369,7 @@ export function CliproxyPage() { setAddAccountProvider({ provider: selectedStatus.provider, displayName: selectedStatus.displayName, + isFirstAccount: (selectedStatus.accounts?.length || 0) === 0, }) } onSetDefault={(accountId) => @@ -395,6 +398,7 @@ export function CliproxyPage() { onClose={() => setAddAccountProvider(null)} provider={addAccountProvider?.provider || ''} displayName={addAccountProvider?.displayName || ''} + isFirstAccount={addAccountProvider?.isFirstAccount || false} /> );