/** * API Proxy Service Page * Provides service control, model mapping, and usage examples */ import { createFileRoute, Link } from '@tanstack/react-router'; import { useTranslation } from 'react-i18next'; import { useQuery } from '@tanstack/react-query'; import { ipc } from '@/ipc/manager'; import { useEffect, useState, useMemo } from 'react'; import { useAppConfig } from '@/hooks/useAppConfig'; import { ProxyConfig } from '@/types/config'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Switch } from '@/components/ui/switch'; import { Badge } from '@/components/ui/badge'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { useToast } from '@/components/ui/use-toast'; import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Loader2, Copy, Code, Terminal, Eye, EyeOff, ServerOff, ChevronDown, ChevronUp, } from 'lucide-react'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { useCloudAccounts } from '@/hooks/useCloudAccounts'; import { ToolsTab } from '@/components/proxy/advanced/ToolsTab'; import { flatMap, sortBy, uniq } from 'lodash-es'; type ProxyProtocol = 'openai' | 'anthropic'; const DEFAULT_MODELS = [ 'gemini-3-flash', 'gemini-3.1-pro-low', 'gemini-3.1-pro-high', 'claude-sonnet-4-6-thinking', 'claude-opus-4-6-thinking', ]; const ANTHROPIC_ROUTE_OPTIONS = [ 'claude-sonnet-4-6-thinking', 'claude-opus-4-6-thinking', 'gemini-3-flash', 'gemini-3.1-pro-low', 'gemini-3.1-pro-high', ] as const; const DEFAULT_ANTHROPIC_MAPPING: Record = { 'claude-sonnet-4-6-20260219': 'claude-sonnet-4-6-thinking', 'claude-sonnet-4-5-20250929': 'claude-sonnet-4-6-thinking', 'claude-opus-4-6-20260201': 'claude-opus-4-6-thinking', opus: 'claude-opus-4-6-thinking', }; function resolveAnthropicMappingValue( anthropicMapping: Record, keys: string[], fallback: string, ): string { for (const key of keys) { const value = anthropicMapping[key]; if (value) { return value; } } return fallback; } function ProxyPage() { const { t } = useTranslation(); const { toast } = useToast(); const { config, isLoading, saveConfig } = useAppConfig(); // Query all available local IPs const { data: localIps } = useQuery({ queryKey: ['system', 'localIps'], queryFn: async () => { try { const ips = await ipc.client.system.get_local_ips(); return ips as { address: string; name: string; isRecommended: boolean }[]; } catch (e) { console.error('Failed to get local IPs:', e); return [{ address: '127.0.0.1', name: 'localhost', isRecommended: false }]; } }, staleTime: Infinity, retry: 3, }); // Selected IP for display (defaults to first recommended or first available) const [selectedIp, setSelectedIp] = useState(''); // Set default selected IP when IPs are loaded useEffect(() => { if (localIps && localIps.length > 0 && !selectedIp) { const recommended = localIps.find((ip) => ip.isRecommended); setTimeout(() => setSelectedIp(recommended?.address || localIps[0].address), 0); } }, [localIps, selectedIp]); // Local state for proxyConfig editing const [proxyConfig, setProxyConfig] = useState(undefined); const [isRegenerateDialogOpen, setIsRegenerateDialogOpen] = useState(false); const [showKey, setShowKey] = useState(false); // Sync config.proxy to local state when loaded, and check actual server status useEffect(() => { if (config) { // Check actual server status and sync with config const syncServerStatus = async () => { try { const status = await ipc.client.gateway.status(); const actualEnabled = status.running; // If config says enabled but server not running, or vice versa, sync if (config.proxy.enabled !== actualEnabled) { const syncedConfig = { ...config.proxy, enabled: actualEnabled }; setProxyConfig(syncedConfig); // Also save the corrected state await saveConfig({ ...config, proxy: syncedConfig }); } else { setProxyConfig(config.proxy); } } catch { // If status check fails, just use config value setProxyConfig(config.proxy); } }; syncServerStatus(); } }, [config, saveConfig]); // Helper to update proxyConfig and auto-save const updateProxyConfig = async (newProxyConfig: ProxyConfig) => { setProxyConfig(newProxyConfig); if (config) { await saveConfig({ ...config, proxy: newProxyConfig }); } }; // ===== Usage Examples State ===== const { data: accounts } = useCloudAccounts(); const [selectedProtocol, setSelectedProtocol] = useState('openai'); const [activeModelTab, setActiveModelTab] = useState('gemini-3.1-pro-high'); const [showCurl, setShowCurl] = useState(true); const [showPython, setShowPython] = useState(true); const { data: metricsData } = useQuery({ queryKey: ['proxyAdvanced', 'metrics'], queryFn: () => ipc.client.proxyAdvanced.getProxyMetrics(), refetchInterval: 30000, enabled: proxyConfig?.enabled ?? false, }); const availableModelIds: string[] = useMemo(() => { if (!accounts || accounts.length === 0) { return DEFAULT_MODELS; } const modelNames = flatMap(accounts, (account) => { if (!account.quota?.models) return []; return Object.keys(account.quota.models); }); const uniqueModels = sortBy(uniq(modelNames)); return uniqueModels.length > 0 ? uniqueModels : DEFAULT_MODELS; }, [accounts]); const groupedModels = useMemo(() => { const groups: Record = { Anthropic: [], Google: [], OpenAI: [], Other: [], }; availableModelIds.forEach((id) => { const lower = id.toLowerCase(); if (lower.includes('gemini') || lower.includes('learnlm') || lower.includes('gemma')) { groups['Google'].push(id); } else if (lower.includes('claude')) { groups['Anthropic'].push(id); } else if ( lower.includes('gpt') || lower.includes('o1') || lower.includes('o3') || lower.includes('dall-e') ) { groups['OpenAI'].push(id); } else { groups['Other'].push(id); } }); return groups; }, [availableModelIds]); useEffect(() => { if (!availableModelIds.includes(activeModelTab) && availableModelIds.length > 0) { setTimeout(() => setActiveModelTab(availableModelIds[0]), 0); } }, [availableModelIds, activeModelTab]); // Computed values for examples const apiKey = proxyConfig?.api_key || 'YOUR_API_KEY'; const baseUrl = `http://localhost:${proxyConfig?.port || 8045}`; const updateAnthropicMapping = (mappingPatch: Record) => { if (!proxyConfig) { return; } updateProxyConfig({ ...proxyConfig, anthropic_mapping: { ...proxyConfig.anthropic_mapping, ...mappingPatch, }, }); }; const copyToClipboard = (text: string) => { navigator.clipboard.writeText(text); toast({ title: t('proxy.copied') }); }; const getCurlExample = (modelId: string) => { if (selectedProtocol === 'anthropic') { return `curl ${baseUrl}/v1/messages \\ -H "Content-Type: application/json" \\ -H "x-api-key: ${apiKey}" \\ -H "anthropic-version: 2023-06-01" \\ -d '{ "model": "${modelId}", "max_tokens": 1024, "messages": [{"role": "user", "content": "Hello"}] }'`; } return `curl ${baseUrl}/v1/chat/completions \\ -H "Content-Type: application/json" \\ -H "Authorization: Bearer ${apiKey}" \\ -d '{ "model": "${modelId}", "messages": [{"role": "user", "content": "Hello"}] }'`; }; const getPythonExample = (modelId: string) => { if (selectedProtocol === 'anthropic') { return `from anthropic import Anthropic client = Anthropic( base_url="${baseUrl}", api_key="${apiKey}" ) response = client.messages.create( model="${modelId}", max_tokens=1024, messages=[{"role": "user", "content": "Hello"}] ) print(response.content[0].text)`; } return `from openai import OpenAI client = OpenAI( base_url="${baseUrl}/v1", api_key="${apiKey}" ) response = client.chat.completions.create( model="${modelId}", messages=[{"role": "user", "content": "Hello"}] ) print(response.choices[0].message.content)`; }; if (isLoading || !proxyConfig) { return (
); } return (

{t('proxy.title')}

{t('proxy.description')}

Status Routing Tools API Docs {/* Status Tab */} {/* Service Control Card */}
{t('proxy.service.title')} {t('proxy.service.description')}
{proxyConfig.enabled ? t('proxy.service.running') : t('proxy.service.stopped')}
{/* Start/Stop Button */}
{/* Port & Timeout Configuration */}
updateProxyConfig({ ...proxyConfig, port: parseInt(e.target.value) || 8045 }) } disabled={proxyConfig.enabled} />
updateProxyConfig({ ...proxyConfig, request_timeout: parseInt(e.target.value) || 120, }) } />
{/* API Key */}
{(!proxyConfig.api_key || proxyConfig.api_key.length < 10) && (
{proxyConfig.api_key ? 'Weak key' : 'No API key'} {proxyConfig.api_key ? 'API key is too short for secure operation' : 'Generate or set an API key to secure the proxy'}
)} {t('proxy.regenerateConfirm.title')} {t('proxy.regenerateConfirm.description')}
{/* Auto Start Toggle */}

{t('proxy.config.auto_start_desc')}

updateProxyConfig({ ...proxyConfig, auto_start: checked }) } />
{/* Account Quota Context */} Account Pool Quota and routing context {accounts && accounts.length > 0 ? (

Primary Account

{accounts[0].email}

{accounts.length > 1 && ( Pool Mode: Active ({accounts.length} accounts) )}
) : (

No accounts configured

)}
{proxyConfig.enabled ? ( <> {/* Local Access Info Banner */}
{t('proxy.config.local_access')}
http://{selectedIp || 'localhost'}:{proxyConfig.port}/v1 {localIps && localIps.length > 1 && ( )}
{!proxyConfig.api_key && (
{t('proxy.config.no_token_warning')}
)}
{/* Health Metrics */}
{(() => { const metrics = metricsData?.success ? metricsData.data : undefined; const items = [ { label: t('proxy.advanced.metrics.requestsPerMinute'), value: metrics?.requestsPerMinute ?? 0, suffix: '', }, { label: t('proxy.advanced.metrics.avgLatency'), value: metrics?.avgLatency ?? 0, suffix: 'ms', }, { label: t('proxy.advanced.metrics.errorRate'), value: metrics?.errorRate ?? 0, suffix: '%', }, { label: t('proxy.advanced.metrics.activeConnections'), value: metrics?.activeConnections ?? 0, suffix: '', }, ]; return items.map((metric) => (

{metric.label}

{metric.value} {metric.suffix}
)); })()}
) : (

Proxy is not running

Start the proxy to enable local API access

)}
{/* Routing Tab */} {t('proxy.mapping.title')} {t('proxy.mapping.description')}
Requested Model
Resolved Model
Provider
Claude Sonnet 4.6 (Thinking)
Anthropic
Claude Opus 4.6 (Thinking)
Anthropic
{/* Current Mappings */} {t('proxy.routing.currentMappings')} {t('proxy.routing.modelMapping')} {(() => { const mappingEntries = Object.entries(proxyConfig.anthropic_mapping || {}); const customEntries = Object.entries(proxyConfig.custom_mapping || {}); return mappingEntries.length === 0 && customEntries.length === 0 ? (

{t('proxy.routing.noMapping')}

) : (
{t('proxy.mapping.maps_to')}
{t('settings.modelMapping.targetGemini')}
Provider
{mappingEntries.map(([key, value]) => (
{key}
{value}
Anthropic
))} {customEntries.map(([key, value]) => (
{key}
{value}
Custom
))}
); })()}
{/* Upstream Proxy Status */} {t('proxy.routing.upstreamProxy')} {t('proxy.routing.upstreamProxy')}
{t('proxy.routing.status')} {proxyConfig.upstream_proxy?.enabled ? t('proxy.routing.enabled') : t('proxy.routing.disabled')}
{proxyConfig.upstream_proxy?.enabled && (
{t('proxy.routing.url')} {proxyConfig.upstream_proxy?.url || '—'}
)}
{/* API Docs Tab */} {t('proxy.examples.title')} {t('proxy.examples.description')} {/* Compact Protocol Selector */}
{/* Model Selector & Copy API ID */}
{/* Collapsible Code Examples */}
{/* cURL */}
setShowCurl(!showCurl)} > cURL
{showCurl ? : }
{showCurl && (
                      {getCurlExample(activeModelTab)}
                    
)}
{/* Python */}
setShowPython(!showPython)} > Python
{showPython ? : }
{showPython && (
                      {getPythonExample(activeModelTab)}
                    
)}
{/* Tools Tab */}
); } export const Route = createFileRoute('/proxy')({ component: ProxyPage, });