feat(profiles): add cliproxy api profile bridge

- add backend and CLI flows for creating routed API profiles from CLIProxy providers

- add dashboard bridge CTAs, metadata, and guided create dialog mode

- cover bridge routes and dialog behavior with focused tests

Refs #649
This commit is contained in:
Tam Nhu Tran committed 2026-03-19 10:24:28 -04:00
1 parent 1f284132ce
commit 287691fa04
26 files changed
+1804 -465

No files matched your search

+45 -18
View File
@@ -6,11 +6,13 @@
*/
import * as fs from 'fs';
import * as path from 'path';
import { getCcsDir, loadConfigSafe } from '../../utils/config-manager';
import { loadConfigSafe } from '../../utils/config-manager';
import { loadOrCreateUnifiedConfig, isUnifiedMode } from '../../config/unified-config-loader';
import { expandPath } from '../../utils/helpers';
import type { TargetType } from '../../targets/target-adapter';
import type { Settings } from '../../types/config';
import type { ApiProfileInfo, CliproxyVariantInfo, ApiListResult } from './profile-types';
import { resolveCliproxyBridgeMetadata } from './cliproxy-profile-bridge';
const VALID_TARGETS: ReadonlySet<TargetType> = new Set<TargetType>(['claude', 'droid']);
@@ -37,23 +39,43 @@ export function apiProfileExists(name: string): boolean {
}
}
/**
* Load settings file from a config reference such as ~/.ccs/name.settings.json.
*/
function loadProfileSettings(settingsReference: string | undefined): Settings | null {
if (!settingsReference || settingsReference === 'config.yaml') {
return null;
}
try {
const settingsPath = expandPath(settingsReference);
if (!fs.existsSync(settingsPath)) return null;
return JSON.parse(fs.readFileSync(settingsPath, 'utf8')) as Settings;
} catch {
return null;
}
}
function isConfiguredFromSettings(settings: Settings | null): boolean {
const token = settings?.env?.ANTHROPIC_AUTH_TOKEN || settings?.env?.ANTHROPIC_API_KEY || '';
return token.length > 0 && !token.includes('YOUR_') && !token.includes('your-');
}
function resolveProfileSettingsReference(name: string): string | undefined {
if (isUnifiedMode()) {
const config = loadOrCreateUnifiedConfig();
return config.profiles[name]?.settings;
}
const config = loadConfigSafe();
return config.profiles[name];
}
/**
* Check if API profile has real API key (not placeholder)
*/
export function isApiProfileConfigured(apiName: string): boolean {
try {
const ccsDir = getCcsDir();
const settingsPath = path.join(ccsDir, `${apiName}.settings.json`);
// Check settings.json file for API key
if (!fs.existsSync(settingsPath)) return false;
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
const token = settings?.env?.ANTHROPIC_AUTH_TOKEN || settings?.env?.ANTHROPIC_API_KEY || '';
return token.length > 0 && !token.includes('YOUR_') && !token.includes('your-');
} catch {
return false;
}
return isConfiguredFromSettings(loadProfileSettings(resolveProfileSettingsReference(apiName)));
}
/**
@@ -73,12 +95,15 @@ export function listApiProfiles(): ApiListResult {
if (name === 'default' && profile.settings?.includes('.claude/settings.json')) {
continue;
}
const settingsPath = profile.settings || 'config.yaml';
const settings = loadProfileSettings(settingsPath);
profiles.push({
name,
settingsPath: profile.settings || 'config.yaml',
isConfigured: isApiProfileConfigured(name),
settingsPath,
isConfigured: isConfiguredFromSettings(settings),
configSource: 'unified',
target: sanitizeTarget(profile.target),
cliproxyBridge: resolveCliproxyBridgeMetadata(settings),
});
}
// CLIProxy variants
@@ -103,12 +128,14 @@ export function listApiProfiles(): ApiListResult {
if (name === 'default' && (settingsPath as string).includes('.claude/settings.json')) {
continue;
}
const settings = loadProfileSettings(settingsPath as string);
profiles.push({
name,
settingsPath: settingsPath as string,
isConfigured: isApiProfileConfigured(name),
isConfigured: isConfiguredFromSettings(settings),
configSource: 'legacy',
target: sanitizeTarget(legacyTargetMap?.[name]),
cliproxyBridge: resolveCliproxyBridgeMetadata(settings),
});
}
// CLIProxy variants