import type { CLIProxyProvider } from './types'; import { ConfigError } from '../errors/error-types'; export type OAuthFlowType = 'authorization_code' | 'device_code'; export type TokenRefreshOwnership = 'ccs' | 'cliproxy' | 'unsupported'; export type AuthStartSupport = 'cliproxy-cli' | 'unsupported'; export interface ProviderCapabilities { displayName: string; description: string; oauthFlow: OAuthFlowType; callbackPort: number | null; /** Provider name expected by CLIProxyAPI callback endpoint payload. */ callbackProviderName: string; /** Provider name prefix used by CLIProxyAPI auth URL endpoint. */ authUrlProviderName: string; /** Who owns token refresh logic for this provider. */ refreshOwnership: TokenRefreshOwnership; /** Whether CCS can start account linking through the bundled CLIProxy binary. */ authStartSupport: AuthStartSupport; /** User-facing reason when account linking cannot be started. */ authStartUnsupportedReason?: string; /** Filename prefixes used to identify auth tokens for this provider. */ authFilePrefixes: readonly string[]; /** Token JSON "type" values accepted for this provider. */ tokenTypeValues: readonly string[]; /** Alternative names accepted as first-class CCS CLI shortcuts. */ cliAliases?: readonly string[]; /** * Alternative provider names used by CLIProxyAPI or stats endpoints. * These aliases normalize external names to canonical CCS provider IDs. */ aliases: readonly string[]; } export const PROVIDER_CAPABILITIES: Record = { gemini: { displayName: 'Google Gemini', description: 'Gemini Pro/Flash models', oauthFlow: 'authorization_code', callbackPort: 8085, callbackProviderName: 'gemini', authUrlProviderName: 'gemini-cli', refreshOwnership: 'cliproxy', authStartSupport: 'cliproxy-cli', authFilePrefixes: ['gemini-', 'google-'], tokenTypeValues: ['gemini'], aliases: ['gemini-cli'], }, codex: { displayName: 'OpenAI Codex', description: 'GPT-4 and codex models', oauthFlow: 'authorization_code', callbackPort: 1455, callbackProviderName: 'codex', authUrlProviderName: 'codex', refreshOwnership: 'cliproxy', authStartSupport: 'cliproxy-cli', authFilePrefixes: ['codex-', 'openai-'], tokenTypeValues: ['codex'], aliases: [], }, xai: { displayName: 'xAI (Grok)', description: 'Grok coding and reasoning models', oauthFlow: 'device_code', callbackPort: null, callbackProviderName: 'xai', authUrlProviderName: 'xai', refreshOwnership: 'cliproxy', authStartSupport: 'cliproxy-cli', authFilePrefixes: ['xai-'], tokenTypeValues: ['xai'], cliAliases: ['grok'], aliases: ['grok', 'x-ai', 'x.ai'], }, agy: { displayName: 'Antigravity', description: 'Antigravity AI models', oauthFlow: 'authorization_code', callbackPort: 51121, callbackProviderName: 'antigravity', authUrlProviderName: 'antigravity', refreshOwnership: 'cliproxy', authStartSupport: 'cliproxy-cli', authFilePrefixes: ['antigravity-', 'agy-'], tokenTypeValues: ['antigravity'], aliases: ['antigravity'], }, qwen: { displayName: 'Alibaba Qwen', description: 'Qwen Code models', oauthFlow: 'device_code', callbackPort: null, callbackProviderName: 'qwen', authUrlProviderName: 'qwen', refreshOwnership: 'unsupported', authStartSupport: 'unsupported', authStartUnsupportedReason: 'Alibaba Qwen account linking is not supported by the bundled CLIProxy runtime. Use an API-key Qwen profile; CLIProxyAPI does not expose Qwen OAuth yet.', authFilePrefixes: ['qwen-'], tokenTypeValues: ['qwen'], aliases: [], }, iflow: { displayName: 'iFlow', description: 'iFlow AI models', oauthFlow: 'authorization_code', callbackPort: 11451, callbackProviderName: 'iflow', authUrlProviderName: 'iflow', refreshOwnership: 'cliproxy', authStartSupport: 'cliproxy-cli', authFilePrefixes: ['iflow-'], tokenTypeValues: ['iflow'], aliases: [], }, kiro: { displayName: 'Kiro (AWS)', description: 'AWS CodeWhisperer models', oauthFlow: 'device_code', callbackPort: null, callbackProviderName: 'kiro', authUrlProviderName: 'kiro', refreshOwnership: 'cliproxy', authStartSupport: 'cliproxy-cli', authFilePrefixes: ['kiro-', 'aws-', 'codewhisperer-'], tokenTypeValues: ['kiro', 'codewhisperer'], aliases: ['codewhisperer'], }, ghcp: { displayName: 'GitHub Copilot (OAuth)', description: 'Deprecated GitHub Copilot compatibility via OAuth', oauthFlow: 'device_code', callbackPort: null, callbackProviderName: 'copilot', authUrlProviderName: 'github', refreshOwnership: 'cliproxy', authStartSupport: 'cliproxy-cli', authFilePrefixes: ['github-copilot-', 'copilot-', 'gh-'], tokenTypeValues: ['github-copilot', 'copilot'], aliases: ['github-copilot', 'copilot'], }, claude: { displayName: 'Claude (Anthropic)', description: 'Claude Opus/Sonnet models', oauthFlow: 'authorization_code', callbackPort: 54545, callbackProviderName: 'anthropic', authUrlProviderName: 'anthropic', refreshOwnership: 'unsupported', authStartSupport: 'cliproxy-cli', authFilePrefixes: ['claude-', 'anthropic-'], tokenTypeValues: ['claude', 'anthropic'], aliases: ['anthropic'], }, kimi: { displayName: 'Kimi (Moonshot)', description: 'Moonshot AI K2/K2.5 models', oauthFlow: 'device_code', callbackPort: null, callbackProviderName: 'kimi', authUrlProviderName: 'kimi', refreshOwnership: 'cliproxy', authStartSupport: 'cliproxy-cli', authFilePrefixes: ['kimi-'], tokenTypeValues: ['kimi'], aliases: ['moonshot'], }, cursor: { displayName: 'Cursor', description: 'Cursor browser-authenticated provider', oauthFlow: 'device_code', callbackPort: null, callbackProviderName: 'cursor', authUrlProviderName: 'cursor', refreshOwnership: 'cliproxy', authStartSupport: 'cliproxy-cli', authFilePrefixes: ['cursor.', 'cursor-'], tokenTypeValues: ['cursor'], aliases: [], }, gitlab: { displayName: 'GitLab Duo', description: 'GitLab Duo with OAuth or PAT auth', oauthFlow: 'authorization_code', callbackPort: 17171, callbackProviderName: 'gitlab', authUrlProviderName: 'gitlab', refreshOwnership: 'cliproxy', authStartSupport: 'cliproxy-cli', authFilePrefixes: ['gitlab-'], tokenTypeValues: ['gitlab'], aliases: ['gitlab-duo'], }, codebuddy: { displayName: 'CodeBuddy (Tencent)', description: 'Tencent CodeBuddy AI assistant', oauthFlow: 'device_code', callbackPort: null, callbackProviderName: 'codebuddy', authUrlProviderName: 'codebuddy', refreshOwnership: 'cliproxy', authStartSupport: 'cliproxy-cli', authFilePrefixes: ['codebuddy-'], tokenTypeValues: ['codebuddy'], aliases: ['tencent'], }, kilo: { displayName: 'Kilo AI', description: 'Kilo AI coding assistant', oauthFlow: 'device_code', callbackPort: null, callbackProviderName: 'kilo', authUrlProviderName: 'kilo', refreshOwnership: 'unsupported', authStartSupport: 'cliproxy-cli', authFilePrefixes: ['kilo-'], tokenTypeValues: ['kilo'], aliases: [], }, qoder: { displayName: 'Qoder', description: 'Qoder AI coding assistant', oauthFlow: 'device_code', callbackPort: null, callbackProviderName: 'qoder', authUrlProviderName: 'qoder', refreshOwnership: 'unsupported', authStartSupport: 'cliproxy-cli', authFilePrefixes: ['qoder-'], tokenTypeValues: ['qoder'], aliases: [], }, }; export const CLIPROXY_PROVIDER_IDS = Object.freeze( Object.keys(PROVIDER_CAPABILITIES) as CLIProxyProvider[] ); export const BROWSER_URL_AUTH_PROVIDER_IDS = Object.freeze([ 'cursor', ] as const satisfies readonly CLIProxyProvider[]); const BROWSER_URL_AUTH_PROVIDER_SET = new Set(BROWSER_URL_AUTH_PROVIDER_IDS); /** Providers currently supported by quota status fetchers. */ export const QUOTA_SUPPORTED_PROVIDER_IDS = Object.freeze([ 'agy', 'codex', 'claude', 'gemini', 'ghcp', ] as const); export type QuotaSupportedProvider = (typeof QUOTA_SUPPORTED_PROVIDER_IDS)[number]; const QUOTA_SUPPORTED_PROVIDER_SET = new Set(QUOTA_SUPPORTED_PROVIDER_IDS); export const QUOTA_PROVIDER_OPTION_VALUES = Object.freeze( [ ...QUOTA_SUPPORTED_PROVIDER_IDS, ...QUOTA_SUPPORTED_PROVIDER_IDS.flatMap((provider) => PROVIDER_CAPABILITIES[provider].aliases), 'all', ].filter((value, index, values) => values.indexOf(value) === index) ); export const QUOTA_PROVIDER_HELP_TEXT = QUOTA_PROVIDER_OPTION_VALUES.join(', '); export function buildProviderMap( valueFor: (provider: CLIProxyProvider) => T ): Record { return CLIPROXY_PROVIDER_IDS.reduce( (acc, provider) => { acc[provider] = valueFor(provider); return acc; }, {} as Record ); } const PROVIDER_ID_SET = new Set(CLIPROXY_PROVIDER_IDS); export function buildProviderAliasMap( capabilities: Record = PROVIDER_CAPABILITIES ): ReadonlyMap { const aliasMap = new Map(); const providers = Object.keys(capabilities) as CLIProxyProvider[]; const registerAlias = (alias: string, provider: CLIProxyProvider): void => { const normalized = alias.trim().toLowerCase(); if (!normalized) { return; } const existingProvider = aliasMap.get(normalized); if (existingProvider && existingProvider !== provider) { throw new ConfigError( `Provider alias collision for "${normalized}": ${existingProvider} and ${provider}` ); } aliasMap.set(normalized, provider); }; for (const provider of providers) { registerAlias(provider, provider); for (const alias of capabilities[provider].aliases) { registerAlias(alias, provider); } } return aliasMap; } const PROVIDER_ALIAS_MAP: ReadonlyMap = buildProviderAliasMap(); const PROVIDER_CLI_SHORTCUT_MAP: ReadonlyMap = (() => { const shortcutMap = new Map(); for (const provider of CLIPROXY_PROVIDER_IDS) { shortcutMap.set(provider, provider); for (const alias of PROVIDER_CAPABILITIES[provider].cliAliases ?? []) { const normalized = alias.trim().toLowerCase(); if (!normalized) continue; const existingProvider = shortcutMap.get(normalized); if (existingProvider && existingProvider !== provider) { throw new ConfigError( `Provider CLI alias collision for "${normalized}": ${existingProvider} and ${provider}` ); } shortcutMap.set(normalized, provider); } } return shortcutMap; })(); export function isCLIProxyProvider(provider: string): provider is CLIProxyProvider { return PROVIDER_ID_SET.has(provider as CLIProxyProvider); } /** Resolve canonical provider IDs and explicitly supported CLI shortcut aliases. */ export function resolveCLIProxyProviderShortcut(providerName: string): CLIProxyProvider | null { const normalized = providerName.trim().toLowerCase(); if (!normalized) return null; return PROVIDER_CLI_SHORTCUT_MAP.get(normalized) ?? null; } export function getProviderCLIAliases(provider: CLIProxyProvider): readonly string[] { return PROVIDER_CAPABILITIES[provider].cliAliases ?? []; } export function getProviderCapabilities(provider: CLIProxyProvider): ProviderCapabilities { return PROVIDER_CAPABILITIES[provider]; } export function getProviderDisplayName(provider: CLIProxyProvider): string { return PROVIDER_CAPABILITIES[provider].displayName; } export function getProviderDescription(provider: CLIProxyProvider): string { return PROVIDER_CAPABILITIES[provider].description; } export function getProvidersByOAuthFlow(flowType: OAuthFlowType): CLIProxyProvider[] { return CLIPROXY_PROVIDER_IDS.filter( (provider) => PROVIDER_CAPABILITIES[provider].oauthFlow === flowType ); } export function isBrowserUrlAuthProvider(provider: CLIProxyProvider): boolean { return BROWSER_URL_AUTH_PROVIDER_SET.has(provider); } export function getDeviceCodeVerificationProviders(): CLIProxyProvider[] { return getProvidersByOAuthFlow('device_code').filter( (provider) => !isBrowserUrlAuthProvider(provider) ); } export function getOAuthFlowType(provider: CLIProxyProvider): OAuthFlowType { return PROVIDER_CAPABILITIES[provider].oauthFlow; } export function getOAuthCallbackPort(provider: CLIProxyProvider): number | null { return PROVIDER_CAPABILITIES[provider].callbackPort; } export function getCLIProxyCallbackProviderName(provider: CLIProxyProvider): string { return PROVIDER_CAPABILITIES[provider].callbackProviderName; } export function getCLIProxyAuthUrlProviderName(provider: CLIProxyProvider): string { return PROVIDER_CAPABILITIES[provider].authUrlProviderName; } export function getTokenRefreshOwnership(provider: CLIProxyProvider): TokenRefreshOwnership { return PROVIDER_CAPABILITIES[provider].refreshOwnership; } export function isRefreshDelegatedToCLIProxy(provider: CLIProxyProvider): boolean { return PROVIDER_CAPABILITIES[provider].refreshOwnership === 'cliproxy'; } export function getUnsupportedAuthStartReason(provider: CLIProxyProvider): string | null { const capabilities = PROVIDER_CAPABILITIES[provider]; if (capabilities.authStartSupport !== 'unsupported') { return null; } return ( capabilities.authStartUnsupportedReason ?? `${capabilities.displayName} account linking is not supported by the bundled CLIProxy runtime.` ); } export function getProviderAuthFilePrefixes(provider: CLIProxyProvider): readonly string[] { return PROVIDER_CAPABILITIES[provider].authFilePrefixes; } export function getProviderTokenTypeValues(provider: CLIProxyProvider): readonly string[] { return PROVIDER_CAPABILITIES[provider].tokenTypeValues; } export function mapExternalProviderName(providerName: string): CLIProxyProvider | null { const normalized = providerName.trim().toLowerCase(); if (!normalized) { return null; } return PROVIDER_ALIAS_MAP.get(normalized) ?? null; } export function isQuotaSupportedProvider( provider: CLIProxyProvider ): provider is QuotaSupportedProvider { return QUOTA_SUPPORTED_PROVIDER_SET.has(provider as QuotaSupportedProvider); }