refactor(cliproxy): DRY provider lists into single source of truth

- unify CLIPROXY_SUPPORTED_PROVIDERS to import from CLIPROXY_PROVIDER_IDS
- replace 4 inline union types with CLIProxyProvider import
- replace hardcoded provider arrays in migration-manager and proxy-routes
- remove duplicate PROVIDER_DISPLAY_NAMES, use getProviderDisplayName()
- sync test now imports from ui/src/lib/provider-config instead of hardcoded array

Adding a new CLIProxy provider no longer requires updating 14+ hardcoded lists.
This commit is contained in:
Tam Nhu Tran
2026-02-17 17:02:45 +07:00
parent 905b35eea5
commit 94b03c7f75
6 changed files with 20 additions and 50 deletions
+2 -18
View File
@@ -6,6 +6,7 @@
import * as fs from 'fs';
import * as path from 'path';
import { CLIProxyProvider, ProviderConfig } from '../types';
import { getProviderDisplayName } from '../provider-capabilities';
import { getModelMappingFromConfig } from '../base-config-loader';
import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader';
import { getEffectiveApiKey, getEffectiveManagementSecret } from '../auth-token-manager';
@@ -46,34 +47,17 @@ const DEFAULT_ANTIGRAVITY_ALIASES: Array<{ name: string; alias: string; fork?: b
{ name: 'claude-opus-4-6-thinking', alias: 'gemini-claude-opus-4-6-thinking', fork: true },
];
/** Provider display names (static metadata) */
const PROVIDER_DISPLAY_NAMES: Record<CLIProxyProvider, string> = {
gemini: 'Gemini',
codex: 'Codex',
agy: 'Antigravity',
qwen: 'Qwen Code',
iflow: 'iFlow',
kiro: 'Kiro (AWS)',
ghcp: 'GitHub Copilot (OAuth)',
claude: 'Claude (Anthropic)',
};
/**
* Get provider configuration
* Model mappings are loaded from config/base-{provider}.settings.json
*/
export function getProviderConfig(provider: CLIProxyProvider): ProviderConfig {
const displayName = PROVIDER_DISPLAY_NAMES[provider];
if (!displayName) {
throw new Error(`Unknown provider: ${provider}`);
}
// Load models from base config file
const models = getModelMappingFromConfig(provider);
return {
name: provider,
displayName,
displayName: getProviderDisplayName(provider),
models,
requiresOAuth: true, // All CLIProxy providers require OAuth
};
+2 -1
View File
@@ -18,6 +18,7 @@ import { getCcsDir } from '../utils/config-manager';
import { expandPath } from '../utils/helpers';
import type { ProfileConfig, AccountConfig, CLIProxyVariantConfig } from './unified-config-types';
import { createEmptyUnifiedConfig } from './unified-config-types';
import { CLIPROXY_PROVIDER_IDS } from '../cliproxy/provider-capabilities';
import { saveUnifiedConfig, hasUnifiedConfig, loadUnifiedConfig } from './unified-config-loader';
import { infoBox, warn } from '../utils/ui';
@@ -203,7 +204,7 @@ export async function migrate(dryRun = false): Promise<MigrationResult> {
// 6b. Migrate built-in CLIProxy OAuth profile settings (gemini, codex, agy, qwen, iflow)
// Keep settings in *.settings.json files - only record reference in config.yaml
// This matches Claude's ~/.claude/settings.json pattern for user familiarity
const builtInProviders = ['gemini', 'codex', 'agy', 'qwen', 'iflow'];
const builtInProviders = [...CLIPROXY_PROVIDER_IDS];
for (const provider of builtInProviders) {
const settingsFile = `${provider}.settings.json`;
const settingsPath = path.join(ccsDir, settingsFile);
+7 -14
View File
@@ -10,6 +10,8 @@
*/
import type { TargetType } from '../targets/target-adapter';
import type { CLIProxyProvider } from '../cliproxy/types';
import { CLIPROXY_PROVIDER_IDS } from '../cliproxy/provider-capabilities';
/**
* Unified config version.
@@ -25,18 +27,9 @@ export const UNIFIED_CONFIG_VERSION = 8;
/**
* Supported CLIProxy providers.
* Includes all OAuth-based providers supported by CLIProxyAPI.
* Derived from CLIPROXY_PROVIDER_IDS — single source of truth in provider-capabilities.ts.
*/
export const CLIPROXY_SUPPORTED_PROVIDERS = [
'gemini',
'codex',
'agy',
'qwen',
'iflow',
'kiro',
'ghcp',
'claude',
] as const;
export const CLIPROXY_SUPPORTED_PROVIDERS = CLIPROXY_PROVIDER_IDS;
/**
* Account configuration (formerly in profiles.json).
@@ -80,7 +73,7 @@ export type OAuthAccounts = Record<string, string>;
*/
export interface CLIProxyVariantConfig {
/** Base provider to use */
provider: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp' | 'claude';
provider: CLIProxyProvider;
/** Account nickname (references oauth_accounts) */
account?: string;
/** Path to settings file (e.g., "~/.ccs/gemini-custom.settings.json") */
@@ -98,14 +91,14 @@ export interface CLIProxyVariantConfig {
*/
export interface CompositeTierConfig {
/** Provider for this tier */
provider: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp' | 'claude';
provider: CLIProxyProvider;
/** Model ID to use for this tier */
model: string;
/** Account nickname (optional, references oauth_accounts) */
account?: string;
/** Fallback provider+model if primary fails */
fallback?: {
provider: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp' | 'claude';
provider: CLIProxyProvider;
model: string;
account?: string;
};
+3 -1
View File
@@ -3,6 +3,8 @@
* Source: ~/.ccs/config.json
*/
import type { CLIProxyProvider } from '../cliproxy/types';
/**
* Profile configuration mapping
* Maps profile names to settings.json paths
@@ -18,7 +20,7 @@ export interface ProfilesConfig {
*/
export interface CLIProxyVariantConfig {
/** CLIProxy provider to use */
provider: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp' | 'claude';
provider: CLIProxyProvider;
/** Path to settings.json with custom model configuration (optional) */
settings?: string;
/** Account identifier for multi-account support (optional, defaults to 'default') */
+2 -1
View File
@@ -16,6 +16,7 @@ import {
DEFAULT_CLIPROXY_SERVER_CONFIG,
CliproxyServerConfig,
} from '../../config/unified-config-types';
import { CLIPROXY_PROVIDER_IDS } from '../../cliproxy/provider-capabilities';
const router = Router();
@@ -113,7 +114,7 @@ router.put('/backend', async (req: Request, res: Response) => {
config.cliproxy = {
backend,
oauth_accounts: {},
providers: ['gemini', 'codex', 'agy', 'qwen', 'iflow', 'kiro', 'ghcp'],
providers: [...CLIPROXY_PROVIDER_IDS],
variants: {},
};
} else {
@@ -7,34 +7,23 @@
import { describe, expect, test } from 'bun:test';
import { CLIPROXY_PROFILES } from '../../../src/auth/profile-detector';
// UI providers (must manually sync - this test validates the sync)
const UI_CLIPROXY_PROVIDERS = [
'gemini',
'codex',
'agy',
'qwen',
'iflow',
'kiro',
'ghcp',
'claude',
] as const;
import { CLIPROXY_PROVIDERS } from '../../../ui/src/lib/provider-config';
describe('Provider Sync', () => {
test('backend CLIPROXY_PROFILES matches UI CLIPROXY_PROVIDERS', () => {
const backend = [...CLIPROXY_PROFILES].sort();
const ui = [...UI_CLIPROXY_PROVIDERS].sort();
const ui = [...CLIPROXY_PROVIDERS].sort();
expect(backend).toEqual(ui);
});
test('both arrays have same length', () => {
expect(CLIPROXY_PROFILES.length).toBe(UI_CLIPROXY_PROVIDERS.length);
expect(CLIPROXY_PROFILES.length).toBe(CLIPROXY_PROVIDERS.length);
});
test('UI array contains all backend providers', () => {
for (const provider of CLIPROXY_PROFILES) {
expect(UI_CLIPROXY_PROVIDERS).toContain(provider);
expect(CLIPROXY_PROVIDERS).toContain(provider);
}
});
});