mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 14:19:56 +00:00
Merge pull request #384 from kaitranntt/dev
feat(cliproxy): Claude OAuth provider + skip-local-auth improvements
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kaitranntt/ccs",
|
||||
"version": "7.28.2",
|
||||
"version": "7.28.2-dev.5",
|
||||
"description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6",
|
||||
"keywords": [
|
||||
"cli",
|
||||
|
||||
@@ -28,6 +28,7 @@ export const CLIPROXY_PROFILES = [
|
||||
'iflow',
|
||||
'kiro',
|
||||
'ghcp',
|
||||
'claude',
|
||||
] as const;
|
||||
export type CLIProxyProfileName = (typeof CLIPROXY_PROFILES)[number];
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { CLIProxyProvider } from './types';
|
||||
import { CLIPROXY_PROFILES } from '../auth/profile-detector';
|
||||
import { getCliproxyDir, getAuthDir } from './config-generator';
|
||||
import { PROVIDER_TYPE_VALUES } from './auth/auth-types';
|
||||
|
||||
@@ -946,7 +947,7 @@ export async function soloAccount(
|
||||
* Get summary of all accounts across providers
|
||||
*/
|
||||
export function getAllAccountsSummary(): Record<CLIProxyProvider, AccountInfo[]> {
|
||||
const providers: CLIProxyProvider[] = ['gemini', 'codex', 'agy', 'qwen', 'iflow', 'kiro', 'ghcp'];
|
||||
const providers: CLIProxyProvider[] = [...CLIPROXY_PROFILES];
|
||||
const summary: Record<CLIProxyProvider, AccountInfo[]> = {} as Record<
|
||||
CLIProxyProvider,
|
||||
AccountInfo[]
|
||||
|
||||
@@ -27,6 +27,7 @@ export const OAUTH_CALLBACK_PORTS: Partial<Record<CLIProxyProvider, number>> = {
|
||||
codex: 1455,
|
||||
agy: 51121,
|
||||
iflow: 11451,
|
||||
claude: 54545,
|
||||
// qwen: Device Code Flow - no callback port
|
||||
// ghcp: Device Code Flow - no callback port
|
||||
};
|
||||
@@ -121,6 +122,13 @@ export const OAUTH_CONFIGS: Record<CLIProxyProvider, ProviderOAuthConfig> = {
|
||||
scopes: ['copilot'],
|
||||
authFlag: '--github-copilot-login',
|
||||
},
|
||||
claude: {
|
||||
provider: 'claude',
|
||||
displayName: 'Claude (Anthropic)',
|
||||
authUrl: 'https://console.anthropic.com/oauth/authorize',
|
||||
scopes: ['user:inference', 'user:profile'],
|
||||
authFlag: '--claude-login',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -136,6 +144,7 @@ export const PROVIDER_AUTH_PREFIXES: Record<CLIProxyProvider, string[]> = {
|
||||
iflow: ['iflow-'],
|
||||
kiro: ['kiro-', 'aws-', 'codewhisperer-'],
|
||||
ghcp: ['github-copilot-', 'copilot-', 'gh-'],
|
||||
claude: ['claude-', 'anthropic-'],
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -150,6 +159,7 @@ export const PROVIDER_TYPE_VALUES: Record<CLIProxyProvider, string[]> = {
|
||||
iflow: ['iflow'],
|
||||
kiro: ['kiro', 'codewhisperer'],
|
||||
ghcp: ['github-copilot', 'copilot'],
|
||||
claude: ['claude', 'anthropic'],
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { CLIProxyProvider } from '../types';
|
||||
import { CLIPROXY_PROFILES } from '../../auth/profile-detector';
|
||||
import { getProviderAccounts, getAccountTokenPath } from '../account-manager';
|
||||
|
||||
/** Preemptive refresh time: refresh tokens 45 minutes before expiry */
|
||||
@@ -113,7 +114,7 @@ export function getTokenExpiryInfo(
|
||||
* @returns Array of token expiry info, excluding invalid tokens
|
||||
*/
|
||||
export function getAllTokenExpiryInfo(): TokenExpiryInfo[] {
|
||||
const providers: CLIProxyProvider[] = ['gemini', 'codex', 'agy', 'qwen', 'iflow', 'kiro', 'ghcp'];
|
||||
const providers: CLIProxyProvider[] = [...CLIPROXY_PROFILES];
|
||||
const results: TokenExpiryInfo[] = [];
|
||||
|
||||
for (const provider of providers) {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { CLIProxyProvider } from '../types';
|
||||
import { CLIPROXY_PROFILES } from '../../auth/profile-detector';
|
||||
import { getProviderAuthDir } from '../config-generator';
|
||||
import { getProviderAccounts, getDefaultAccount } from '../account-manager';
|
||||
import {
|
||||
@@ -145,7 +146,7 @@ export function getAuthStatus(provider: CLIProxyProvider): AuthStatus {
|
||||
* Get auth status for all providers
|
||||
*/
|
||||
export function getAllAuthStatus(): AuthStatus[] {
|
||||
const providers: CLIProxyProvider[] = ['gemini', 'codex', 'agy', 'qwen', 'iflow', 'kiro', 'ghcp'];
|
||||
const providers: CLIProxyProvider[] = [...CLIPROXY_PROFILES];
|
||||
return providers.map(getAuthStatus);
|
||||
}
|
||||
|
||||
|
||||
@@ -498,7 +498,16 @@ export async function execClaudeWithCLIProxy(
|
||||
}
|
||||
|
||||
// 3. Ensure OAuth completed (if provider requires it)
|
||||
if (providerConfig.requiresOAuth) {
|
||||
// Skip local OAuth check when using remote proxy with auth token
|
||||
// The remote proxy has its own OAuth sessions and handles authentication
|
||||
// Note: Trim authToken to reject whitespace-only values
|
||||
const remoteAuthToken = proxyConfig.authToken?.trim();
|
||||
const skipLocalAuth = useRemoteProxy && !!remoteAuthToken;
|
||||
if (skipLocalAuth) {
|
||||
log(`Using remote proxy authentication (skipping local OAuth)`);
|
||||
}
|
||||
|
||||
if (providerConfig.requiresOAuth && !skipLocalAuth) {
|
||||
log(`Checking authentication for ${provider}`);
|
||||
|
||||
if (forceAuth || !isAuthenticated(provider)) {
|
||||
@@ -549,7 +558,8 @@ export async function execClaudeWithCLIProxy(
|
||||
|
||||
// 3b. Preflight quota check - auto-switch to account with quota before launch
|
||||
// Uses quota-manager for caching, tier priority, and cooldown support
|
||||
if (provider === 'agy') {
|
||||
// Skip for remote proxy - quota is managed on the remote server
|
||||
if (provider === 'agy' && !skipLocalAuth) {
|
||||
const preflight = await preflightCheck(provider);
|
||||
|
||||
if (!preflight.proceed) {
|
||||
@@ -571,11 +581,13 @@ export async function execClaudeWithCLIProxy(
|
||||
// 4. First-run model configuration (interactive)
|
||||
// For supported providers, prompt user to select model on first run
|
||||
// Pass customSettingsPath for CLIProxy variants
|
||||
if (supportsModelConfig(provider)) {
|
||||
// Skip for remote proxy - model is configured on the remote server
|
||||
if (supportsModelConfig(provider) && !skipLocalAuth) {
|
||||
await configureProviderModel(provider, false, cfg.customSettingsPath); // false = only if not configured
|
||||
}
|
||||
|
||||
// 5. Check for known broken models and warn user
|
||||
// Show warning for both local and remote modes - user should be aware of model issues
|
||||
const currentModel = getCurrentModel(provider, cfg.customSettingsPath);
|
||||
if (currentModel && isModelBroken(provider, currentModel)) {
|
||||
const modelEntry = findModel(provider, currentModel);
|
||||
@@ -586,7 +598,11 @@ export async function execClaudeWithCLIProxy(
|
||||
if (issueUrl) {
|
||||
console.error(` Tracking: ${issueUrl}`);
|
||||
}
|
||||
console.error(` Run "ccs ${provider} --config" to change model.`);
|
||||
if (skipLocalAuth) {
|
||||
console.error(' Note: Model may be overridden by remote proxy configuration.');
|
||||
} else {
|
||||
console.error(` Run "ccs ${provider} --config" to change model.`);
|
||||
}
|
||||
console.error('');
|
||||
}
|
||||
|
||||
|
||||
@@ -299,6 +299,7 @@ const PROVIDER_DISPLAY_NAMES: Record<CLIProxyProvider, string> = {
|
||||
iflow: 'iFlow',
|
||||
kiro: 'Kiro (AWS)',
|
||||
ghcp: 'GitHub Copilot (OAuth)',
|
||||
claude: 'Claude (Anthropic)',
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -166,6 +166,55 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
|
||||
},
|
||||
],
|
||||
},
|
||||
claude: {
|
||||
provider: 'claude',
|
||||
displayName: 'Claude (Anthropic)',
|
||||
defaultModel: 'claude-sonnet-4-5-20250514',
|
||||
models: [
|
||||
{
|
||||
id: 'claude-opus-4-5-20250220',
|
||||
name: 'Claude Opus 4.5',
|
||||
description: 'Most capable Claude model',
|
||||
thinking: {
|
||||
type: 'budget',
|
||||
min: 1024,
|
||||
max: 128000,
|
||||
zeroAllowed: false,
|
||||
dynamicAllowed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'claude-sonnet-4-5-20250514',
|
||||
name: 'Claude Sonnet 4.5',
|
||||
description: 'Balanced performance and speed',
|
||||
thinking: {
|
||||
type: 'budget',
|
||||
min: 1024,
|
||||
max: 128000,
|
||||
zeroAllowed: false,
|
||||
dynamicAllowed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'claude-sonnet-4-20250514',
|
||||
name: 'Claude Sonnet 4',
|
||||
description: 'Previous generation Sonnet',
|
||||
thinking: {
|
||||
type: 'budget',
|
||||
min: 1024,
|
||||
max: 128000,
|
||||
zeroAllowed: false,
|
||||
dynamicAllowed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'claude-haiku-4-5-20250514',
|
||||
name: 'Claude Haiku 4.5',
|
||||
description: 'Fast and efficient',
|
||||
thinking: { type: 'none' },
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -132,7 +132,7 @@ export function saveVariantUnified(
|
||||
if (!config.cliproxy) {
|
||||
config.cliproxy = {
|
||||
oauth_accounts: {},
|
||||
providers: ['gemini', 'codex', 'agy', 'qwen', 'iflow', 'kiro', 'ghcp'],
|
||||
providers: ['gemini', 'codex', 'agy', 'qwen', 'iflow', 'kiro', 'ghcp', 'claude'],
|
||||
variants: {},
|
||||
};
|
||||
}
|
||||
|
||||
+10
-1
@@ -118,8 +118,17 @@ export interface DownloadResult {
|
||||
* - iflow: iFlow via OAuth
|
||||
* - kiro: Kiro (AWS CodeWhisperer) via OAuth
|
||||
* - ghcp: GitHub Copilot via Device Code (OAuth through CLIProxyAPIPlus)
|
||||
* - claude: Claude (Anthropic) via OAuth
|
||||
*/
|
||||
export type CLIProxyProvider = 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp';
|
||||
export type CLIProxyProvider =
|
||||
| 'gemini'
|
||||
| 'codex'
|
||||
| 'agy'
|
||||
| 'qwen'
|
||||
| 'iflow'
|
||||
| 'kiro'
|
||||
| 'ghcp'
|
||||
| 'claude';
|
||||
|
||||
/**
|
||||
* CLIProxy backend selection
|
||||
|
||||
@@ -61,7 +61,7 @@ export type OAuthAccounts = Record<string, string>;
|
||||
*/
|
||||
export interface CLIProxyVariantConfig {
|
||||
/** Base provider to use */
|
||||
provider: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp';
|
||||
provider: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp' | 'claude';
|
||||
/** Account nickname (references oauth_accounts) */
|
||||
account?: string;
|
||||
/** Path to settings file (e.g., "~/.ccs/gemini-custom.settings.json") */
|
||||
|
||||
@@ -8,7 +8,11 @@
|
||||
* - Gemini: 8085
|
||||
* - Codex: 1455
|
||||
* - Agy: 51121
|
||||
* - iFlow: 11451
|
||||
* - Kiro: 9876
|
||||
* - Claude: 54545
|
||||
* - Qwen: Device Code Flow (no port needed)
|
||||
* - GHCP: Device Code Flow (no port needed)
|
||||
*/
|
||||
|
||||
import {
|
||||
@@ -21,6 +25,7 @@ import {
|
||||
BindingTestResult,
|
||||
} from '../utils/port-utils';
|
||||
import { CLIProxyProvider } from '../cliproxy/types';
|
||||
import { CLIPROXY_PROFILES } from '../auth/profile-detector';
|
||||
|
||||
/**
|
||||
* OAuth callback ports for each provider
|
||||
@@ -34,6 +39,7 @@ export const OAUTH_CALLBACK_PORTS: Record<CLIProxyProvider, number | null> = {
|
||||
iflow: 11451, // Authorization Code Flow
|
||||
kiro: 9876, // Authorization Code Flow
|
||||
ghcp: null, // Device Code Flow - no callback port
|
||||
claude: 54545, // Authorization Code Flow (Anthropic OAuth)
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -52,6 +58,7 @@ export const OAUTH_FLOW_TYPES: Record<CLIProxyProvider, OAuthFlowType> = {
|
||||
iflow: 'authorization_code',
|
||||
kiro: 'authorization_code',
|
||||
ghcp: 'device_code',
|
||||
claude: 'authorization_code',
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -138,7 +145,7 @@ export async function checkOAuthPort(provider: CLIProxyProvider): Promise<OAuthP
|
||||
* Check OAuth ports for all providers
|
||||
*/
|
||||
export async function checkAllOAuthPorts(): Promise<OAuthPortDiagnostic[]> {
|
||||
const providers: CLIProxyProvider[] = ['gemini', 'codex', 'agy', 'qwen', 'iflow', 'kiro', 'ghcp'];
|
||||
const providers: CLIProxyProvider[] = [...CLIPROXY_PROFILES];
|
||||
const results: OAuthPortDiagnostic[] = [];
|
||||
|
||||
for (const provider of providers) {
|
||||
@@ -153,7 +160,8 @@ export async function checkAllOAuthPorts(): Promise<OAuthPortDiagnostic[]> {
|
||||
* Check OAuth ports for providers that use Authorization Code flow only
|
||||
*/
|
||||
export async function checkAuthCodePorts(): Promise<OAuthPortDiagnostic[]> {
|
||||
const providers: CLIProxyProvider[] = ['gemini', 'codex', 'agy', 'kiro'];
|
||||
// Filter providers that use authorization_code flow (DRY: derive from OAUTH_FLOW_TYPES)
|
||||
const providers = CLIPROXY_PROFILES.filter((p) => OAUTH_FLOW_TYPES[p] === 'authorization_code');
|
||||
const results: OAuthPortDiagnostic[] = [];
|
||||
|
||||
for (const provider of providers) {
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ export interface ProfilesConfig {
|
||||
*/
|
||||
export interface CLIProxyVariantConfig {
|
||||
/** CLIProxy provider to use */
|
||||
provider: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp';
|
||||
provider: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp' | 'claude';
|
||||
/** Path to settings.json with custom model configuration (optional) */
|
||||
settings?: string;
|
||||
/** Account identifier for multi-account support (optional, defaults to 'default') */
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
@@ -78,6 +71,8 @@ router.get('/', async (_req: Request, res: Response): Promise<void> => {
|
||||
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
|
||||
|
||||
@@ -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<voi
|
||||
router.get('/quota/:provider/:accountId', async (req: Request, res: Response): Promise<void> => {
|
||||
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',
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Provider Sync Test
|
||||
*
|
||||
* Validates that backend CLIPROXY_PROFILES and UI CLIPROXY_PROVIDERS stay in sync.
|
||||
* This test catches mismatches when adding new providers.
|
||||
*/
|
||||
|
||||
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;
|
||||
|
||||
describe('Provider Sync', () => {
|
||||
test('backend CLIPROXY_PROFILES matches UI CLIPROXY_PROVIDERS', () => {
|
||||
const backend = [...CLIPROXY_PROFILES].sort();
|
||||
const ui = [...UI_CLIPROXY_PROVIDERS].sort();
|
||||
|
||||
expect(backend).toEqual(ui);
|
||||
});
|
||||
|
||||
test('both arrays have same length', () => {
|
||||
expect(CLIPROXY_PROFILES.length).toBe(UI_CLIPROXY_PROVIDERS.length);
|
||||
});
|
||||
|
||||
test('UI array contains all backend providers', () => {
|
||||
for (const provider of CLIPROXY_PROFILES) {
|
||||
expect(UI_CLIPROXY_PROVIDERS).toContain(provider);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,282 @@
|
||||
/**
|
||||
* Unit tests for skip-local-auth functionality when using remote proxy with auth token
|
||||
*
|
||||
* When --proxy-host and --proxy-auth-token are provided together, the system should
|
||||
* skip local OAuth checks because the remote proxy handles authentication.
|
||||
*
|
||||
* Implementation uses: const remoteAuthToken = proxyConfig.authToken?.trim();
|
||||
* const skipLocalAuth = useRemoteProxy && !!remoteAuthToken;
|
||||
*/
|
||||
import { describe, it, expect } from 'bun:test';
|
||||
|
||||
/**
|
||||
* Helper to compute skipLocalAuth exactly as the implementation does
|
||||
* Mirrors logic from cliproxy-executor.ts lines 503-505
|
||||
*/
|
||||
function computeSkipLocalAuth(
|
||||
useRemoteProxy: boolean,
|
||||
proxyConfig: { authToken?: string | null }
|
||||
): boolean {
|
||||
const remoteAuthToken = proxyConfig.authToken?.trim();
|
||||
return useRemoteProxy && !!remoteAuthToken;
|
||||
}
|
||||
|
||||
describe('skip-local-auth logic', () => {
|
||||
describe('skipLocalAuth flag determination', () => {
|
||||
it('should skip local auth when both useRemoteProxy and authToken are truthy', () => {
|
||||
const useRemoteProxy = true;
|
||||
const proxyConfig = { authToken: 'test-token-123' };
|
||||
|
||||
const skipLocalAuth = computeSkipLocalAuth(useRemoteProxy, proxyConfig);
|
||||
|
||||
expect(skipLocalAuth).toBe(true);
|
||||
});
|
||||
|
||||
it('should NOT skip local auth when useRemoteProxy is false', () => {
|
||||
const useRemoteProxy = false;
|
||||
const proxyConfig = { authToken: 'test-token-123' };
|
||||
|
||||
const skipLocalAuth = computeSkipLocalAuth(useRemoteProxy, proxyConfig);
|
||||
|
||||
expect(skipLocalAuth).toBe(false);
|
||||
});
|
||||
|
||||
it('should NOT skip local auth when authToken is undefined', () => {
|
||||
const useRemoteProxy = true;
|
||||
const proxyConfig = { authToken: undefined };
|
||||
|
||||
const skipLocalAuth = computeSkipLocalAuth(useRemoteProxy, proxyConfig);
|
||||
|
||||
expect(skipLocalAuth).toBe(false);
|
||||
});
|
||||
|
||||
it('should NOT skip local auth when authToken is empty string', () => {
|
||||
const useRemoteProxy = true;
|
||||
const proxyConfig = { authToken: '' };
|
||||
|
||||
const skipLocalAuth = computeSkipLocalAuth(useRemoteProxy, proxyConfig);
|
||||
|
||||
expect(skipLocalAuth).toBe(false);
|
||||
});
|
||||
|
||||
it('should NOT skip local auth when both are falsy', () => {
|
||||
const useRemoteProxy = false;
|
||||
const proxyConfig = { authToken: undefined };
|
||||
|
||||
const skipLocalAuth = computeSkipLocalAuth(useRemoteProxy, proxyConfig);
|
||||
|
||||
expect(skipLocalAuth).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('authToken edge cases', () => {
|
||||
it('should NOT skip local auth when authToken is whitespace-only', () => {
|
||||
const useRemoteProxy = true;
|
||||
const proxyConfig = { authToken: ' ' };
|
||||
|
||||
const skipLocalAuth = computeSkipLocalAuth(useRemoteProxy, proxyConfig);
|
||||
|
||||
expect(skipLocalAuth).toBe(false);
|
||||
});
|
||||
|
||||
it('should NOT skip local auth when authToken is tabs and newlines', () => {
|
||||
const useRemoteProxy = true;
|
||||
const proxyConfig = { authToken: '\t\n\r' };
|
||||
|
||||
const skipLocalAuth = computeSkipLocalAuth(useRemoteProxy, proxyConfig);
|
||||
|
||||
expect(skipLocalAuth).toBe(false);
|
||||
});
|
||||
|
||||
it('should NOT skip local auth when authToken is null', () => {
|
||||
const useRemoteProxy = true;
|
||||
const proxyConfig = { authToken: null };
|
||||
|
||||
const skipLocalAuth = computeSkipLocalAuth(useRemoteProxy, proxyConfig);
|
||||
|
||||
expect(skipLocalAuth).toBe(false);
|
||||
});
|
||||
|
||||
it('should skip local auth when authToken has leading/trailing whitespace but valid content', () => {
|
||||
const useRemoteProxy = true;
|
||||
const proxyConfig = { authToken: ' valid-token-123 ' };
|
||||
|
||||
const skipLocalAuth = computeSkipLocalAuth(useRemoteProxy, proxyConfig);
|
||||
|
||||
expect(skipLocalAuth).toBe(true);
|
||||
});
|
||||
|
||||
it('should skip local auth when authToken contains special characters', () => {
|
||||
const useRemoteProxy = true;
|
||||
const proxyConfig = { authToken: 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.test' };
|
||||
|
||||
const skipLocalAuth = computeSkipLocalAuth(useRemoteProxy, proxyConfig);
|
||||
|
||||
expect(skipLocalAuth).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('OAuth check bypass scenarios', () => {
|
||||
it('should document that OAuth is skipped for remote proxy with auth', () => {
|
||||
// This test documents the expected behavior:
|
||||
// When using remote proxy with auth token, the remote server
|
||||
// already has its own OAuth sessions, so local OAuth is unnecessary
|
||||
const scenario = {
|
||||
useRemoteProxy: true,
|
||||
authToken: 'bearer-token',
|
||||
providerRequiresOAuth: true,
|
||||
};
|
||||
|
||||
const skipLocalAuth = scenario.useRemoteProxy && scenario.authToken;
|
||||
const shouldTriggerLocalOAuth = scenario.providerRequiresOAuth && !skipLocalAuth;
|
||||
|
||||
expect(skipLocalAuth).toBeTruthy();
|
||||
expect(shouldTriggerLocalOAuth).toBe(false);
|
||||
});
|
||||
|
||||
it('should document that OAuth runs when no remote proxy', () => {
|
||||
const scenario = {
|
||||
useRemoteProxy: false,
|
||||
authToken: undefined,
|
||||
providerRequiresOAuth: true,
|
||||
};
|
||||
|
||||
const skipLocalAuth = scenario.useRemoteProxy && scenario.authToken;
|
||||
const shouldTriggerLocalOAuth = scenario.providerRequiresOAuth && !skipLocalAuth;
|
||||
|
||||
expect(skipLocalAuth).toBeFalsy();
|
||||
expect(shouldTriggerLocalOAuth).toBe(true);
|
||||
});
|
||||
|
||||
it('should document that OAuth runs when remote proxy has no auth token', () => {
|
||||
// Edge case: remote proxy configured but no auth token
|
||||
// This should fall back to local OAuth
|
||||
const scenario = {
|
||||
useRemoteProxy: true,
|
||||
authToken: undefined,
|
||||
providerRequiresOAuth: true,
|
||||
};
|
||||
|
||||
const skipLocalAuth = scenario.useRemoteProxy && scenario.authToken;
|
||||
const shouldTriggerLocalOAuth = scenario.providerRequiresOAuth && !skipLocalAuth;
|
||||
|
||||
expect(skipLocalAuth).toBeFalsy();
|
||||
expect(shouldTriggerLocalOAuth).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('preflight quota check bypass', () => {
|
||||
it('should skip preflight for agy provider when using remote proxy with auth', () => {
|
||||
const provider = 'agy';
|
||||
const skipLocalAuth = true;
|
||||
|
||||
const shouldRunPreflight = provider === 'agy' && !skipLocalAuth;
|
||||
|
||||
expect(shouldRunPreflight).toBe(false);
|
||||
});
|
||||
|
||||
it('should run preflight for agy provider when using local mode', () => {
|
||||
const provider = 'agy';
|
||||
const skipLocalAuth = false;
|
||||
|
||||
const shouldRunPreflight = provider === 'agy' && !skipLocalAuth;
|
||||
|
||||
expect(shouldRunPreflight).toBe(true);
|
||||
});
|
||||
|
||||
it('should not run preflight for non-agy providers regardless of mode', () => {
|
||||
const providers = ['gemini', 'codex', 'ghcp', 'kiro'];
|
||||
|
||||
for (const provider of providers) {
|
||||
const shouldRunPreflight = provider === 'agy' && !false;
|
||||
expect(shouldRunPreflight).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('model configuration bypass', () => {
|
||||
it('should skip model config when using remote proxy with auth', () => {
|
||||
const supportsModelConfig = true;
|
||||
const skipLocalAuth = true;
|
||||
|
||||
const shouldConfigureModel = supportsModelConfig && !skipLocalAuth;
|
||||
|
||||
expect(shouldConfigureModel).toBe(false);
|
||||
});
|
||||
|
||||
it('should run model config when using local mode', () => {
|
||||
const supportsModelConfig = true;
|
||||
const skipLocalAuth = false;
|
||||
|
||||
const shouldConfigureModel = supportsModelConfig && !skipLocalAuth;
|
||||
|
||||
expect(shouldConfigureModel).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('broken model warning behavior', () => {
|
||||
it('should show broken model warning in BOTH remote and local modes', () => {
|
||||
// Updated behavior: warnings always shown (with different messaging for remote)
|
||||
// Remote users need to know about broken models too
|
||||
const currentModel = 'some-broken-model';
|
||||
const isModelBroken = true;
|
||||
|
||||
// Warning should show regardless of skipLocalAuth
|
||||
const shouldWarnRemote = currentModel && isModelBroken; // skipLocalAuth=true
|
||||
const shouldWarnLocal = currentModel && isModelBroken; // skipLocalAuth=false
|
||||
|
||||
expect(shouldWarnRemote).toBe(true);
|
||||
expect(shouldWarnLocal).toBe(true);
|
||||
});
|
||||
|
||||
it('should show different message for remote vs local mode', () => {
|
||||
const skipLocalAuth = true;
|
||||
const currentModel = 'some-broken-model';
|
||||
const isModelBroken = true;
|
||||
|
||||
// When remote: "Note: Model may be overridden by remote proxy configuration."
|
||||
// When local: "Run ccs <provider> --config to change model."
|
||||
const remoteMessage = skipLocalAuth
|
||||
? 'Note: Model may be overridden by remote proxy configuration.'
|
||||
: 'Run "ccs provider --config" to change model.';
|
||||
|
||||
expect(remoteMessage).toContain('remote proxy');
|
||||
});
|
||||
});
|
||||
|
||||
describe('CI/CD workflow scenarios', () => {
|
||||
it('should enable headless CI workflow with remote proxy', () => {
|
||||
// Simulate GitHub Actions workflow configuration
|
||||
const workflowConfig = {
|
||||
headless: true,
|
||||
proxyHost: 'proxy.example.com',
|
||||
proxyPort: 443,
|
||||
proxyProtocol: 'https',
|
||||
proxyAuthToken: 'github-secret-token',
|
||||
remoteOnly: true,
|
||||
};
|
||||
|
||||
// Determine if this configuration should skip local OAuth
|
||||
const useRemoteProxy = !!workflowConfig.proxyHost;
|
||||
const skipLocalAuth = useRemoteProxy && !!workflowConfig.proxyAuthToken;
|
||||
|
||||
expect(useRemoteProxy).toBe(true);
|
||||
expect(skipLocalAuth).toBe(true);
|
||||
});
|
||||
|
||||
it('should require local OAuth when no proxy configured', () => {
|
||||
// Simulate local development without proxy
|
||||
const localConfig = {
|
||||
headless: false,
|
||||
proxyHost: undefined,
|
||||
proxyAuthToken: undefined,
|
||||
};
|
||||
|
||||
const useRemoteProxy = !!localConfig.proxyHost;
|
||||
const skipLocalAuth = useRemoteProxy && !!localConfig.proxyAuthToken;
|
||||
|
||||
expect(useRemoteProxy).toBe(false);
|
||||
expect(skipLocalAuth).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Claude</title><path d="M4.709 15.955l4.72-2.647.08-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312-.006.006z" fill="#D97757" fill-rule="nonzero"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -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'] 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,15 +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)' },
|
||||
];
|
||||
const providerOptions = CLIPROXY_PROVIDERS.map((id) => ({
|
||||
value: id,
|
||||
label: getProviderDisplayName(id),
|
||||
}));
|
||||
|
||||
export function CliproxyDialog({ open, onClose }: CliproxyDialogProps) {
|
||||
const createMutation = useCreateVariant();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -187,9 +187,11 @@ export function AccountItem({
|
||||
>
|
||||
<User className="w-4 h-4" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={cn('font-medium text-sm', privacyMode && PRIVACY_BLUR_CLASS)}>
|
||||
<span
|
||||
className={cn('font-medium text-sm truncate', privacyMode && PRIVACY_BLUR_CLASS)}
|
||||
>
|
||||
{account.email || account.id}
|
||||
</span>
|
||||
{account.isDefault && (
|
||||
|
||||
@@ -20,6 +20,7 @@ const PROVIDER_IMAGES: Record<string, string> = {
|
||||
iflow: '/assets/providers/iflow.png',
|
||||
kiro: '/assets/providers/kiro.png',
|
||||
ghcp: '/assets/providers/copilot.svg',
|
||||
claude: '/assets/providers/claude.svg',
|
||||
};
|
||||
|
||||
/** Provider color configuration (for fallback only - no background for image logos) */
|
||||
|
||||
@@ -1,19 +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: 'gemini', name: 'Google Gemini', description: 'Gemini Pro/Flash models' },
|
||||
{ id: 'codex', name: 'OpenAI Codex', description: 'GPT-4 and codex models' },
|
||||
{ id: 'agy', name: 'Antigravity', description: 'Antigravity AI 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<CLIProxyProvider, { name: string; description: string }> = {
|
||||
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 {
|
||||
|
||||
@@ -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,7 +137,7 @@ export function QuickSetupWizard({ open, onClose }: QuickSetupWizardProps) {
|
||||
try {
|
||||
await createMutation.mutateAsync({
|
||||
name: variantName,
|
||||
provider: selectedProvider as 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow',
|
||||
provider: selectedProvider as CLIProxyProvider,
|
||||
model: modelName || undefined,
|
||||
account: selectedAccount?.id,
|
||||
});
|
||||
|
||||
@@ -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'];
|
||||
|
||||
export function useCliproxyAuthFlow() {
|
||||
const [state, setState] = useState<AuthFlowState>({
|
||||
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,
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
* Phase 03: REST API Routes & CRUD
|
||||
*/
|
||||
|
||||
import type { CLIProxyProvider } from './provider-config';
|
||||
|
||||
const BASE_URL = '/api';
|
||||
|
||||
async function request<T>(url: string, options?: RequestInit): Promise<T> {
|
||||
@@ -47,7 +49,7 @@ export interface UpdateProfile {
|
||||
|
||||
export interface Variant {
|
||||
name: string;
|
||||
provider: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp';
|
||||
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';
|
||||
provider: CLIProxyProvider;
|
||||
model?: string;
|
||||
account?: string;
|
||||
}
|
||||
|
||||
export interface UpdateVariant {
|
||||
provider?: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp';
|
||||
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';
|
||||
provider: CLIProxyProvider;
|
||||
isDefault: boolean;
|
||||
tokenFile: string;
|
||||
createdAt: string;
|
||||
|
||||
@@ -311,4 +311,49 @@ export const MODEL_CATALOGS: Record<string, ProviderCatalog> = {
|
||||
},
|
||||
],
|
||||
},
|
||||
claude: {
|
||||
provider: 'claude',
|
||||
displayName: 'Claude (Anthropic)',
|
||||
defaultModel: 'claude-sonnet-4-5-20250514',
|
||||
models: [
|
||||
{
|
||||
id: 'claude-opus-4-5-20250220',
|
||||
name: 'Claude Opus 4.5',
|
||||
description: 'Most capable Claude model',
|
||||
presetMapping: {
|
||||
default: 'claude-opus-4-5-20250220',
|
||||
opus: 'claude-opus-4-5-20250220',
|
||||
sonnet: 'claude-sonnet-4-5-20250514',
|
||||
haiku: 'claude-haiku-4-5-20250514',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'claude-sonnet-4-5-20250514',
|
||||
name: 'Claude Sonnet 4.5',
|
||||
description: 'Balanced performance and speed',
|
||||
presetMapping: {
|
||||
default: 'claude-sonnet-4-5-20250514',
|
||||
opus: 'claude-opus-4-5-20250220',
|
||||
sonnet: 'claude-sonnet-4-5-20250514',
|
||||
haiku: 'claude-haiku-4-5-20250514',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'claude-sonnet-4-20250514',
|
||||
name: 'Claude Sonnet 4',
|
||||
description: 'Previous generation Sonnet',
|
||||
presetMapping: {
|
||||
default: 'claude-sonnet-4-20250514',
|
||||
opus: 'claude-opus-4-5-20250220',
|
||||
sonnet: 'claude-sonnet-4-20250514',
|
||||
haiku: 'claude-haiku-4-5-20250514',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'claude-haiku-4-5-20250514',
|
||||
name: 'Claude Haiku 4.5',
|
||||
description: 'Fast and efficient',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,16 +1,44 @@
|
||||
/**
|
||||
* 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<string, string> = {
|
||||
gemini: '/assets/providers/gemini-color.svg',
|
||||
agy: '/assets/providers/agy.png',
|
||||
codex: '/assets/providers/openai.svg',
|
||||
qwen: '/assets/providers/qwen-color.svg',
|
||||
iflow: '/assets/providers/iflow.png',
|
||||
kiro: '/assets/providers/kiro.png',
|
||||
ghcp: '/assets/providers/copilot.svg',
|
||||
claude: '/assets/providers/claude.svg',
|
||||
};
|
||||
|
||||
// Provider brand colors
|
||||
@@ -23,6 +51,7 @@ export const PROVIDER_COLORS: Record<string, string> = {
|
||||
qwen: '#6236FF',
|
||||
kiro: '#4d908e', // Dark Cyan (AWS-inspired)
|
||||
ghcp: '#43aa8b', // Seaweed (GitHub-inspired)
|
||||
claude: '#D97757', // Anthropic brand color (matches SVG)
|
||||
};
|
||||
|
||||
// Provider display names
|
||||
@@ -35,6 +64,7 @@ const PROVIDER_NAMES: Record<string, string> = {
|
||||
qwen: 'Qwen',
|
||||
kiro: 'Kiro (AWS)',
|
||||
ghcp: 'GitHub Copilot (OAuth)',
|
||||
claude: 'Claude (Anthropic)',
|
||||
};
|
||||
|
||||
// Map provider to display name
|
||||
|
||||
Reference in New Issue
Block a user