Files
ccs/src/cliproxy/provider-capabilities.ts
T

433 lines
14 KiB
TypeScript

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<CLIProxyProvider, ProviderCapabilities> = {
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<CLIProxyProvider>(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<QuotaSupportedProvider>(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<T>(
valueFor: (provider: CLIProxyProvider) => T
): Record<CLIProxyProvider, T> {
return CLIPROXY_PROVIDER_IDS.reduce(
(acc, provider) => {
acc[provider] = valueFor(provider);
return acc;
},
{} as Record<CLIProxyProvider, T>
);
}
const PROVIDER_ID_SET = new Set(CLIPROXY_PROVIDER_IDS);
export function buildProviderAliasMap(
capabilities: Record<CLIProxyProvider, ProviderCapabilities> = PROVIDER_CAPABILITIES
): ReadonlyMap<string, CLIProxyProvider> {
const aliasMap = new Map<string, CLIProxyProvider>();
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<string, CLIProxyProvider> = buildProviderAliasMap();
const PROVIDER_CLI_SHORTCUT_MAP: ReadonlyMap<string, CLIProxyProvider> = (() => {
const shortcutMap = new Map<string, CLIProxyProvider>();
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);
}