/** * API Proxy Service Page * Provides service control, model mapping, and usage examples */ import { createFileRoute } from '@tanstack/react-router'; import { useTranslation } from 'react-i18next'; import { useQuery } from '@tanstack/react-query'; import { ipc } from '@/ipc/manager'; import { useState, useEffect } 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Loader2, Copy, CheckCircle, Zap, Cpu, Sparkles, BrainCircuit, Code, Terminal, Eye, EyeOff, } from 'lucide-react'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; function ProxyPage() { const { t } = useTranslation(); 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); setSelectedIp(recommended?.address || localIps[0].address); } }, [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 (e) { // If status check fails, just use config value setProxyConfig(config.proxy); } }; syncServerStatus(); } }, [config]); // 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 [selectedProtocol, setSelectedProtocol] = useState<'openai' | 'anthropic'>('openai'); const [activeModelTab, setActiveModelTab] = useState('gemini-2.5-flash'); const [copied, setCopied] = useState(null); // Models list for examples const models = [ { id: 'gemini-2.5-flash', name: 'Gemini 2.5 Flash', icon: }, { id: 'gemini-2.5-pro', name: 'Gemini 2.5 Pro', icon: }, { id: 'gemini-3-flash', name: 'Gemini 3 Flash', icon: }, { id: 'gemini-3-pro-high', name: 'Gemini 3 Pro (High)', icon: }, { id: 'claude-sonnet-4-5-thinking', name: 'Claude Sonnet 4.5', icon: }, { id: 'claude-opus-4-5-thinking', name: 'Claude Opus 4.5', icon: }, ]; // Computed values for examples const apiKey = proxyConfig?.api_key || 'YOUR_API_KEY'; const baseUrl = `http://localhost:${proxyConfig?.port || 8045}`; const copyToClipboard = (text: string, type: string) => { navigator.clipboard.writeText(text); setCopied(type); setTimeout(() => setCopied(null), 2000); }; 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', 'API Proxy')}

{t('proxy.description', 'Manage the local API proxy service.')}

{/* Local Access Info Banner */} {/* Local Access Info Banner */} {proxyConfig?.enabled && (
{t('proxy.config.local_access', 'Local Access:')}
http://{selectedIp || 'localhost'}:{proxyConfig.port}/v1 {/* IP Selector Dropdown */} {localIps && localIps.length > 1 && ( )}
{!proxyConfig.api_key && (
{t( 'proxy.config.no_token_warning', '⚠️ No API Key set. Service is open to the public network!', )}
)}
)}
{/* Service Control Card */}
{t('proxy.service.title', 'Service Status')} {t('proxy.service.description', 'Control the local API proxy server.')}
{proxyConfig.enabled ? t('proxy.service.running', 'Running') : t('proxy.service.stopped', '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 */}
{t('proxy.regenerateConfirm.title', 'Regenerate API Key?')} {t( 'proxy.regenerateConfirm.description', 'This will invalidate the current API key immediately. Any applications using the old key will stop working.', )}
{/* Auto Start Toggle */}

{t('proxy.config.auto_start_desc', 'Start proxy service when application launches')}

updateProxyConfig({ ...proxyConfig, auto_start: checked }) } />
{/* Model Mapping Card */} {t('proxy.mapping.title', 'Model Mapping')} {t('proxy.mapping.description', 'Map Claude models to Gemini models for routing.')}
{/* Sonnet 4.5 Card */}

Claude Sonnet 4.5

{t('proxy.mapping.maps_to', 'Maps to')}

{/* Opus 4.5 Card */}

Claude Opus 4.5

{t('proxy.mapping.maps_to', 'Maps to')}

{/* Haiku 4.5 Card */}

Claude Haiku 4.5

{t('proxy.mapping.maps_to', 'Maps to')}

{/* Usage Examples Card */} {t('proxy.examples.title', 'Usage Examples')} {t('proxy.examples.description', 'Example commands to call the local API proxy.')} {/* Protocol Selector Cards */}
{/* OpenAI Protocol Card */}
setSelectedProtocol('openai')} >
OpenAI Protocol
POST /v1/chat/completions

💡 Cursor, Windsurf, NextChat

{/* Anthropic Protocol Card */}
setSelectedProtocol('anthropic')} >
Anthropic Protocol
POST /v1/messages

💡 Claude Code CLI

{/* Model Tabs */}
{models.map((model) => ( ))}
{/* cURL Example */}
cURL
              {getCurlExample(activeModelTab)}
            
{/* Python Example */}
Python
              {getPythonExample(activeModelTab)}
            
); } export const Route = createFileRoute('/proxy')({ component: ProxyPage, });