From 9cd9c423e929579c86da3f409d74927c3c7dedc1 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Tue, 27 Jan 2026 20:50:24 -0500 Subject: [PATCH 1/3] fix: replace hardcoded provider validation arrays with CLIPROXY_PROFILES import Closes #382 Replace duplicate hardcoded provider validation arrays across three route files with a single import from CLIPROXY_PROFILES constant. This DRY fix eliminates code duplication and ensures provider validation is consistent across all routes. Files updated: - account-routes.ts - cliproxy-auth-routes.ts - cliproxy-stats-routes.ts Reduces code by 21 lines while improving maintainability. --- src/web-server/routes/account-routes.ts | 13 +++---------- src/web-server/routes/cliproxy-auth-routes.ts | 13 +++---------- src/web-server/routes/cliproxy-stats-routes.ts | 13 +++---------- 3 files changed, 9 insertions(+), 30 deletions(-) diff --git a/src/web-server/routes/account-routes.ts b/src/web-server/routes/account-routes.ts index d87d6592..cfc3ef34 100644 --- a/src/web-server/routes/account-routes.ts +++ b/src/web-server/routes/account-routes.ts @@ -17,20 +17,13 @@ import { soloAccount, } from '../../cliproxy/account-manager'; import type { CLIProxyProvider } from '../../cliproxy/types'; +import { CLIPROXY_PROFILES } from '../../auth/profile-detector'; const router = Router(); const registry = new ProfileRegistry(); -/** Valid CLIProxy providers */ -const VALID_PROVIDERS: CLIProxyProvider[] = [ - 'gemini', - 'codex', - 'agy', - 'qwen', - 'iflow', - 'kiro', - 'ghcp', -]; +/** Valid CLIProxy providers - derived from canonical CLIPROXY_PROFILES */ +const VALID_PROVIDERS: CLIProxyProvider[] = [...CLIPROXY_PROFILES]; /** Check if provider is valid */ function isValidProvider(provider: string): provider is CLIProxyProvider { diff --git a/src/web-server/routes/cliproxy-auth-routes.ts b/src/web-server/routes/cliproxy-auth-routes.ts index 5485b178..b72418d3 100644 --- a/src/web-server/routes/cliproxy-auth-routes.ts +++ b/src/web-server/routes/cliproxy-auth-routes.ts @@ -35,19 +35,12 @@ import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader'; import { tryKiroImport } from '../../cliproxy/auth/kiro-import'; import { getProviderTokenDir } from '../../cliproxy/auth/token-manager'; import type { CLIProxyProvider } from '../../cliproxy/types'; +import { CLIPROXY_PROFILES } from '../../auth/profile-detector'; const router = Router(); -// Valid providers list -const validProviders: CLIProxyProvider[] = [ - 'gemini', - 'codex', - 'agy', - 'qwen', - 'iflow', - 'kiro', - 'ghcp', -]; +// Valid providers list - derived from canonical CLIPROXY_PROFILES +const validProviders: CLIProxyProvider[] = [...CLIPROXY_PROFILES]; /** * GET /api/cliproxy/auth - Get auth status for built-in CLIProxy profiles diff --git a/src/web-server/routes/cliproxy-stats-routes.ts b/src/web-server/routes/cliproxy-stats-routes.ts index ebd77966..032a86a0 100644 --- a/src/web-server/routes/cliproxy-stats-routes.ts +++ b/src/web-server/routes/cliproxy-stats-routes.ts @@ -14,6 +14,7 @@ import { } from '../../cliproxy/stats-fetcher'; import { fetchAccountQuota } from '../../cliproxy/quota-fetcher'; import type { CLIProxyProvider } from '../../cliproxy/types'; +import { CLIPROXY_PROFILES } from '../../auth/profile-detector'; import { getCliproxyWritablePath, getCliproxyConfigPath, @@ -517,16 +518,8 @@ router.put('/models/:provider', async (req: Request, res: Response): Promise => { const { provider, accountId } = req.params; - // Validate provider - const validProviders: CLIProxyProvider[] = [ - 'agy', - 'gemini', - 'codex', - 'qwen', - 'iflow', - 'kiro', - 'ghcp', - ]; + // Validate provider - use canonical CLIPROXY_PROFILES + const validProviders: CLIProxyProvider[] = [...CLIPROXY_PROFILES]; if (!validProviders.includes(provider as CLIProxyProvider)) { res.status(400).json({ error: 'Invalid provider', From 4a2abc74cac93e17ee12fab3fcf8fc0693552347 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Tue, 27 Jan 2026 21:02:34 -0500 Subject: [PATCH 2/3] fix: add claude provider to statsProviderMap, UI types, and provider arrays Fixes remaining edge cases from #382: - Add anthropic/claude mapping to statsProviderMap in cliproxy-auth-routes - Add 'claude' case to provider-refreshers switch statement - Add 'claude' to 4 UI type definitions in api-client.ts - Add 'claude' to cliproxy-dialog providers array and options - Add 'claude' to use-cliproxy-auth-flow VALID_PROVIDERS - Fix wizard index.tsx type cast to include all 8 providers --- src/cliproxy/auth/provider-refreshers/index.ts | 1 + src/web-server/routes/cliproxy-auth-routes.ts | 2 ++ ui/src/components/cliproxy/cliproxy-dialog.tsx | 3 ++- ui/src/components/setup/wizard/index.tsx | 10 +++++++++- ui/src/hooks/use-cliproxy-auth-flow.ts | 2 +- ui/src/lib/api-client.ts | 8 ++++---- 6 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/cliproxy/auth/provider-refreshers/index.ts b/src/cliproxy/auth/provider-refreshers/index.ts index 232fe8c9..89df2ab2 100644 --- a/src/cliproxy/auth/provider-refreshers/index.ts +++ b/src/cliproxy/auth/provider-refreshers/index.ts @@ -35,6 +35,7 @@ export async function refreshToken( case 'iflow': case 'kiro': case 'ghcp': + case 'claude': return { success: false, error: `Token refresh not yet implemented for ${provider}`, diff --git a/src/web-server/routes/cliproxy-auth-routes.ts b/src/web-server/routes/cliproxy-auth-routes.ts index b72418d3..295d7070 100644 --- a/src/web-server/routes/cliproxy-auth-routes.ts +++ b/src/web-server/routes/cliproxy-auth-routes.ts @@ -71,6 +71,8 @@ router.get('/', async (_req: Request, res: Response): Promise => { iflow: 'iflow', kiro: 'kiro', copilot: 'ghcp', // CLIProxyAPI returns 'copilot', we map to 'ghcp' + anthropic: 'claude', // CLIProxyAPI returns 'anthropic', we map to 'claude' + claude: 'claude', }; // Update lastUsedAt for providers with recent activity diff --git a/ui/src/components/cliproxy/cliproxy-dialog.tsx b/ui/src/components/cliproxy/cliproxy-dialog.tsx index fa8fc3fb..3a848e0b 100644 --- a/ui/src/components/cliproxy/cliproxy-dialog.tsx +++ b/ui/src/components/cliproxy/cliproxy-dialog.tsx @@ -14,7 +14,7 @@ import { Label } from '@/components/ui/label'; import { useCreateVariant, useCliproxyAuth } from '@/hooks/use-cliproxy'; import { usePrivacy } from '@/contexts/privacy-context'; -const providers = ['gemini', 'codex', 'agy', 'qwen', 'iflow', 'kiro', 'ghcp'] as const; +const providers = ['gemini', 'codex', 'agy', 'qwen', 'iflow', 'kiro', 'ghcp', 'claude'] as const; const schema = z.object({ name: z @@ -41,6 +41,7 @@ const providerOptions = [ { value: 'iflow', label: 'iFlow' }, { value: 'kiro', label: 'Kiro (AWS)' }, { value: 'ghcp', label: 'GitHub Copilot (OAuth)' }, + { value: 'claude', label: 'Claude (Anthropic)' }, ]; export function CliproxyDialog({ open, onClose }: CliproxyDialogProps) { diff --git a/ui/src/components/setup/wizard/index.tsx b/ui/src/components/setup/wizard/index.tsx index 29e98e76..6c21af05 100644 --- a/ui/src/components/setup/wizard/index.tsx +++ b/ui/src/components/setup/wizard/index.tsx @@ -136,7 +136,15 @@ export function QuickSetupWizard({ open, onClose }: QuickSetupWizardProps) { try { await createMutation.mutateAsync({ name: variantName, - provider: selectedProvider as 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow', + provider: selectedProvider as + | 'gemini' + | 'codex' + | 'agy' + | 'qwen' + | 'iflow' + | 'kiro' + | 'ghcp' + | 'claude', model: modelName || undefined, account: selectedAccount?.id, }); diff --git a/ui/src/hooks/use-cliproxy-auth-flow.ts b/ui/src/hooks/use-cliproxy-auth-flow.ts index a91a05c3..4867c3a8 100644 --- a/ui/src/hooks/use-cliproxy-auth-flow.ts +++ b/ui/src/hooks/use-cliproxy-auth-flow.ts @@ -14,7 +14,7 @@ interface AuthFlowState { error: string | null; } -const VALID_PROVIDERS = ['gemini', 'codex', 'agy', 'qwen', 'iflow', 'kiro', 'ghcp']; +const VALID_PROVIDERS = ['gemini', 'codex', 'agy', 'qwen', 'iflow', 'kiro', 'ghcp', 'claude']; export function useCliproxyAuthFlow() { const [state, setState] = useState({ diff --git a/ui/src/lib/api-client.ts b/ui/src/lib/api-client.ts index 6f052997..db85b48e 100644 --- a/ui/src/lib/api-client.ts +++ b/ui/src/lib/api-client.ts @@ -47,7 +47,7 @@ export interface UpdateProfile { export interface Variant { name: string; - provider: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp'; + provider: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp' | 'claude'; settings: string; account?: string; port?: number; @@ -56,13 +56,13 @@ export interface Variant { export interface CreateVariant { name: string; - provider: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp'; + provider: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp' | 'claude'; model?: string; account?: string; } export interface UpdateVariant { - provider?: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp'; + provider?: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp' | 'claude'; model?: string; account?: string; } @@ -72,7 +72,7 @@ export interface OAuthAccount { id: string; email?: string; nickname?: string; - provider: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp'; + provider: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp' | 'claude'; isDefault: boolean; tokenFile: string; createdAt: string; From 5a4c8e009ce1cc355d6fa2f05001cab6c9b684c4 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Tue, 27 Jan 2026 21:07:26 -0500 Subject: [PATCH 3/3] refactor(ui): centralize provider list in provider-config.ts (DRY) - Add CLIPROXY_PROVIDERS array as single source of truth for UI - Add CLIProxyProvider type and isValidProvider() helper - Update api-client.ts to use CLIProxyProvider type - Update cliproxy-dialog.tsx to import from provider-config - Update cliproxy-header.tsx to use CLIPROXY_PROVIDERS - Update use-cliproxy-auth-flow.ts to use isValidProvider() - Update wizard constants to use typed provider info Adding a new provider now only requires updating: - Backend: src/auth/profile-detector.ts (CLIPROXY_PROFILES) - UI: ui/src/lib/provider-config.ts (CLIPROXY_PROVIDERS) - UI wizard: ui/src/components/setup/wizard/constants.ts (PROVIDER_INFO) --- .../components/cliproxy/cliproxy-dialog.tsx | 19 +++------ .../components/cliproxy/cliproxy-header.tsx | 15 +++---- ui/src/components/setup/wizard/constants.ts | 40 ++++++++++++++----- ui/src/components/setup/wizard/index.tsx | 11 +---- ui/src/hooks/use-cliproxy-auth-flow.ts | 5 +-- ui/src/lib/api-client.ts | 10 +++-- ui/src/lib/provider-config.ts | 28 ++++++++++++- 7 files changed, 79 insertions(+), 49 deletions(-) diff --git a/ui/src/components/cliproxy/cliproxy-dialog.tsx b/ui/src/components/cliproxy/cliproxy-dialog.tsx index 3a848e0b..6ad1cce9 100644 --- a/ui/src/components/cliproxy/cliproxy-dialog.tsx +++ b/ui/src/components/cliproxy/cliproxy-dialog.tsx @@ -13,15 +13,14 @@ import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { useCreateVariant, useCliproxyAuth } from '@/hooks/use-cliproxy'; import { usePrivacy } from '@/contexts/privacy-context'; - -const providers = ['gemini', 'codex', 'agy', 'qwen', 'iflow', 'kiro', 'ghcp', 'claude'] as const; +import { CLIPROXY_PROVIDERS, getProviderDisplayName } from '@/lib/provider-config'; const schema = z.object({ name: z .string() .min(1, 'Name is required') .regex(/^[a-zA-Z][a-zA-Z0-9._-]*$/, 'Invalid variant name'), - provider: z.enum(providers, { message: 'Provider is required' }), + provider: z.enum(CLIPROXY_PROVIDERS, { message: 'Provider is required' }), model: z.string().optional(), account: z.string().optional(), }); @@ -33,16 +32,10 @@ interface CliproxyDialogProps { onClose: () => void; } -const providerOptions = [ - { value: 'gemini', label: 'Google Gemini' }, - { value: 'codex', label: 'OpenAI Codex' }, - { value: 'agy', label: 'Antigravity' }, - { value: 'qwen', label: 'Alibaba Qwen' }, - { value: 'iflow', label: 'iFlow' }, - { value: 'kiro', label: 'Kiro (AWS)' }, - { value: 'ghcp', label: 'GitHub Copilot (OAuth)' }, - { value: 'claude', label: 'Claude (Anthropic)' }, -]; +const providerOptions = CLIPROXY_PROVIDERS.map((id) => ({ + value: id, + label: getProviderDisplayName(id), +})); export function CliproxyDialog({ open, onClose }: CliproxyDialogProps) { const createMutation = useCreateVariant(); diff --git a/ui/src/components/cliproxy/cliproxy-header.tsx b/ui/src/components/cliproxy/cliproxy-header.tsx index 6b5bb51a..a2bcf5e5 100644 --- a/ui/src/components/cliproxy/cliproxy-header.tsx +++ b/ui/src/components/cliproxy/cliproxy-header.tsx @@ -10,6 +10,7 @@ import { RefreshCw, Loader2, AlertTriangle } from 'lucide-react'; import { useCliproxyAuth } from '@/hooks/use-cliproxy'; import { useCliproxyAuthFlow } from '@/hooks/use-cliproxy-auth-flow'; import { cn } from '@/lib/utils'; +import { CLIPROXY_PROVIDERS, getProviderDisplayName } from '@/lib/provider-config'; interface VersionInfo { currentVersion: string; @@ -135,16 +136,10 @@ export function CliproxyHeader({ .catch(() => {}); // Silently fail }, []); - const providers = [ - { id: 'claude', displayName: 'Claude' }, - { id: 'gemini', displayName: 'Gemini' }, - { id: 'codex', displayName: 'Codex' }, - { id: 'agy', displayName: 'Agy' }, - { id: 'qwen', displayName: 'Qwen' }, - { id: 'iflow', displayName: 'iFlow' }, - { id: 'kiro', displayName: 'Kiro' }, - { id: 'ghcp', displayName: 'GitHub Copilot' }, - ]; + const providers = CLIPROXY_PROVIDERS.map((id) => ({ + id, + displayName: getProviderDisplayName(id), + })); const getProviderStatus = (providerId: string) => { const status = authData?.authStatus.find((s) => s.provider === providerId); diff --git a/ui/src/components/setup/wizard/constants.ts b/ui/src/components/setup/wizard/constants.ts index ba0fda30..96c9d633 100644 --- a/ui/src/components/setup/wizard/constants.ts +++ b/ui/src/components/setup/wizard/constants.ts @@ -1,20 +1,42 @@ /** * Constants for Quick Setup Wizard + * Provider display info with custom ordering for wizard UI. + * Provider IDs must match CLIPROXY_PROVIDERS from provider-config.ts */ import type { ProviderOption } from './types'; +import type { CLIProxyProvider } from '@/lib/provider-config'; -export const PROVIDERS: ProviderOption[] = [ - { id: 'agy', name: 'Antigravity', description: 'Antigravity AI models' }, - { id: 'claude', name: 'Claude (Anthropic)', description: 'Claude Opus/Sonnet models' }, - { id: 'gemini', name: 'Google Gemini', description: 'Gemini Pro/Flash models' }, - { id: 'codex', name: 'OpenAI Codex', description: 'GPT-4 and codex models' }, - { id: 'qwen', name: 'Alibaba Qwen', description: 'Qwen Code models' }, - { id: 'iflow', name: 'iFlow', description: 'iFlow AI models' }, - { id: 'kiro', name: 'Kiro (AWS)', description: 'AWS CodeWhisperer models' }, - { id: 'ghcp', name: 'GitHub Copilot (OAuth)', description: 'GitHub Copilot via OAuth' }, +/** Provider display info for wizard - ordered by recommendation */ +const PROVIDER_INFO: Record = { + agy: { name: 'Antigravity', description: 'Antigravity AI models' }, + claude: { name: 'Claude (Anthropic)', description: 'Claude Opus/Sonnet models' }, + gemini: { name: 'Google Gemini', description: 'Gemini Pro/Flash models' }, + codex: { name: 'OpenAI Codex', description: 'GPT-4 and codex models' }, + qwen: { name: 'Alibaba Qwen', description: 'Qwen Code models' }, + iflow: { name: 'iFlow', description: 'iFlow AI models' }, + kiro: { name: 'Kiro (AWS)', description: 'AWS CodeWhisperer models' }, + ghcp: { name: 'GitHub Copilot (OAuth)', description: 'GitHub Copilot via OAuth' }, +}; + +/** Wizard display order - most recommended first */ +const WIZARD_PROVIDER_ORDER: CLIProxyProvider[] = [ + 'agy', + 'claude', + 'gemini', + 'codex', + 'qwen', + 'iflow', + 'kiro', + 'ghcp', ]; +export const PROVIDERS: ProviderOption[] = WIZARD_PROVIDER_ORDER.map((id) => ({ + id, + name: PROVIDER_INFO[id].name, + description: PROVIDER_INFO[id].description, +})); + export const ALL_STEPS = ['provider', 'auth', 'variant', 'success']; export function getStepProgress(step: string): number { diff --git a/ui/src/components/setup/wizard/index.tsx b/ui/src/components/setup/wizard/index.tsx index 6c21af05..0b894eff 100644 --- a/ui/src/components/setup/wizard/index.tsx +++ b/ui/src/components/setup/wizard/index.tsx @@ -22,6 +22,7 @@ import { useCancelAuth, } from '@/hooks/use-cliproxy'; import type { AuthStatus, OAuthAccount } from '@/lib/api-client'; +import type { CLIProxyProvider } from '@/lib/provider-config'; import { applyDefaultPreset } from '@/lib/preset-utils'; import { usePrivacy } from '@/contexts/privacy-context'; import { toast } from 'sonner'; @@ -136,15 +137,7 @@ export function QuickSetupWizard({ open, onClose }: QuickSetupWizardProps) { try { await createMutation.mutateAsync({ name: variantName, - provider: selectedProvider as - | 'gemini' - | 'codex' - | 'agy' - | 'qwen' - | 'iflow' - | 'kiro' - | 'ghcp' - | 'claude', + provider: selectedProvider as CLIProxyProvider, model: modelName || undefined, account: selectedAccount?.id, }); diff --git a/ui/src/hooks/use-cliproxy-auth-flow.ts b/ui/src/hooks/use-cliproxy-auth-flow.ts index 4867c3a8..bc9c46af 100644 --- a/ui/src/hooks/use-cliproxy-auth-flow.ts +++ b/ui/src/hooks/use-cliproxy-auth-flow.ts @@ -7,6 +7,7 @@ import { useState, useCallback, useRef, useEffect, useMemo } from 'react'; import { useQueryClient } from '@tanstack/react-query'; import { toast } from 'sonner'; import { api } from '@/lib/api-client'; +import { isValidProvider } from '@/lib/provider-config'; interface AuthFlowState { provider: string | null; @@ -14,8 +15,6 @@ interface AuthFlowState { error: string | null; } -const VALID_PROVIDERS = ['gemini', 'codex', 'agy', 'qwen', 'iflow', 'kiro', 'ghcp', 'claude']; - export function useCliproxyAuthFlow() { const [state, setState] = useState({ provider: null, @@ -35,7 +34,7 @@ export function useCliproxyAuthFlow() { const startAuth = useCallback( async (provider: string) => { - if (!VALID_PROVIDERS.includes(provider)) { + if (!isValidProvider(provider)) { setState({ provider: null, isAuthenticating: false, diff --git a/ui/src/lib/api-client.ts b/ui/src/lib/api-client.ts index db85b48e..77b73065 100644 --- a/ui/src/lib/api-client.ts +++ b/ui/src/lib/api-client.ts @@ -3,6 +3,8 @@ * Phase 03: REST API Routes & CRUD */ +import type { CLIProxyProvider } from './provider-config'; + const BASE_URL = '/api'; async function request(url: string, options?: RequestInit): Promise { @@ -47,7 +49,7 @@ export interface UpdateProfile { export interface Variant { name: string; - provider: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp' | 'claude'; + provider: CLIProxyProvider; settings: string; account?: string; port?: number; @@ -56,13 +58,13 @@ export interface Variant { export interface CreateVariant { name: string; - provider: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp' | 'claude'; + provider: CLIProxyProvider; model?: string; account?: string; } export interface UpdateVariant { - provider?: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp' | 'claude'; + provider?: CLIProxyProvider; model?: string; account?: string; } @@ -72,7 +74,7 @@ export interface OAuthAccount { id: string; email?: string; nickname?: string; - provider: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp' | 'claude'; + provider: CLIProxyProvider; isDefault: boolean; tokenFile: string; createdAt: string; diff --git a/ui/src/lib/provider-config.ts b/ui/src/lib/provider-config.ts index 72cb9808..0486b973 100644 --- a/ui/src/lib/provider-config.ts +++ b/ui/src/lib/provider-config.ts @@ -1,8 +1,34 @@ /** * Provider Configuration - * Shared constants for provider branding and assets + * Shared constants for CLIProxy providers - SINGLE SOURCE OF TRUTH for UI + * + * When adding a new provider, update CLIPROXY_PROVIDERS array and related mappings. */ +/** + * Canonical list of CLIProxy provider IDs + * This is the UI's single source of truth for valid providers. + * Must stay in sync with backend's CLIPROXY_PROFILES in src/auth/profile-detector.ts + */ +export const CLIPROXY_PROVIDERS = [ + 'gemini', + 'codex', + 'agy', + 'qwen', + 'iflow', + 'kiro', + 'ghcp', + 'claude', +] as const; + +/** Union type for CLIProxy provider IDs */ +export type CLIProxyProvider = (typeof CLIPROXY_PROVIDERS)[number]; + +/** Check if a string is a valid CLIProxy provider */ +export function isValidProvider(provider: string): provider is CLIProxyProvider { + return CLIPROXY_PROVIDERS.includes(provider as CLIProxyProvider); +} + // Map provider names to asset filenames (only providers with actual logos) export const PROVIDER_ASSETS: Record = { gemini: '/assets/providers/gemini-color.svg',