mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 00:22:34 +00:00
fix(profile): handle km compatibility for legacy kimi api users
This commit is contained in:
@@ -99,7 +99,8 @@ The dashboard provides visual management for all account types:
|
||||
| **Ollama** | Local | `ccs ollama` | Local open-source models, privacy |
|
||||
| **Ollama Cloud** | API Key | `ccs ollama-cloud` | Cloud-hosted open-source models |
|
||||
| **GLM** | API Key | `ccs glm` | Cost-optimized execution |
|
||||
| **Kimi** | API Key | `ccs kimi` | Long-context, thinking mode |
|
||||
| **KM (Kimi API)** | API Key | `ccs km` | Long-context, thinking mode |
|
||||
| **Kimi (OAuth)** | OAuth | `ccs kimi` | Device-code OAuth via CLIProxy |
|
||||
| **Azure Foundry** | API Key | `ccs foundry` | Claude via Microsoft Azure |
|
||||
| **Minimax** | API Key | `ccs mm` | M2 series, 1M context |
|
||||
| **DeepSeek** | API Key | `ccs deepseek` | V3.2 and R1 reasoning |
|
||||
@@ -139,6 +140,7 @@ ccs ghcp # GitHub Copilot (OAuth device flow)
|
||||
ccs agy # Antigravity (OAuth)
|
||||
ccs ollama # Local Ollama (no API key needed)
|
||||
ccs glm # GLM (API key)
|
||||
ccs km # Kimi API profile (API key)
|
||||
```
|
||||
|
||||
### Droid Alias (`argv[0]` pattern)
|
||||
|
||||
@@ -26,6 +26,9 @@ export interface ProviderPreset {
|
||||
}
|
||||
|
||||
export const OPENROUTER_BASE_URL = 'https://openrouter.ai/api';
|
||||
const LEGACY_PRESET_ALIASES: Readonly<Record<string, string>> = Object.freeze({
|
||||
kimi: 'km',
|
||||
});
|
||||
|
||||
/**
|
||||
* Provider presets available via CLI and UI
|
||||
@@ -169,7 +172,9 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [
|
||||
|
||||
/** Get preset by ID */
|
||||
export function getPresetById(id: string): ProviderPreset | undefined {
|
||||
return PROVIDER_PRESETS.find((p) => p.id === id.toLowerCase());
|
||||
const normalized = id.toLowerCase();
|
||||
const canonical = LEGACY_PRESET_ALIASES[normalized] || normalized;
|
||||
return PROVIDER_PRESETS.find((p) => p.id === canonical);
|
||||
}
|
||||
|
||||
/** Get all preset IDs */
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Profile Detector
|
||||
*
|
||||
* Determines profile type (settings-based vs account-based) for routing.
|
||||
* Priority: settings-based profiles (glm/kimi) checked FIRST for backward compatibility.
|
||||
* Priority: settings-based profiles (glm/km) checked FIRST for backward compatibility.
|
||||
*
|
||||
* Supports dual-mode configuration:
|
||||
* - Unified YAML format (config.yaml) when CCS_UNIFIED_CONFIG=1 or config.yaml exists
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
} from '../config/unified-config-types';
|
||||
import { loadUnifiedConfig, isUnifiedMode } from '../config/unified-config-loader';
|
||||
import { getCcsDir } from '../utils/config-manager';
|
||||
import { getProfileLookupCandidates, isLegacyProfileAlias } from '../utils/profile-compat';
|
||||
import type { CLIProxyProvider } from '../cliproxy/types';
|
||||
import { CLIPROXY_PROVIDER_IDS, isCLIProxyProvider } from '../cliproxy/provider-capabilities';
|
||||
import type { TargetType } from '../targets/target-adapter';
|
||||
@@ -166,16 +167,26 @@ class ProfileDetector {
|
||||
};
|
||||
}
|
||||
|
||||
// Check API profiles
|
||||
if (config.profiles?.[profileName]) {
|
||||
const profile = config.profiles[profileName];
|
||||
// Load env from settings file
|
||||
const settingsEnv = loadSettingsFromFile(profile.settings);
|
||||
// Check API profiles (supports compatibility aliases, e.g. km -> kimi)
|
||||
for (const candidate of getProfileLookupCandidates(profileName)) {
|
||||
if (!config.profiles?.[candidate]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const profile = config.profiles[candidate];
|
||||
const settingsPath = profile.settings;
|
||||
const settingsEnv = loadSettingsFromFile(settingsPath);
|
||||
const viaLegacyAlias = isLegacyProfileAlias(profileName, candidate);
|
||||
|
||||
return {
|
||||
type: 'settings',
|
||||
name: profileName,
|
||||
target: profile.target,
|
||||
settingsPath,
|
||||
env: settingsEnv,
|
||||
message: viaLegacyAlias
|
||||
? `Using legacy API profile "${candidate}" for "${profileName}".`
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -308,13 +319,23 @@ class ProfileDetector {
|
||||
};
|
||||
}
|
||||
|
||||
// Priority 3: Check settings-based profiles (glm) - LEGACY FALLBACK
|
||||
if (config.profiles && config.profiles[profileName]) {
|
||||
return {
|
||||
type: 'settings',
|
||||
name: profileName,
|
||||
settingsPath: config.profiles[profileName],
|
||||
};
|
||||
// Priority 3: Check settings-based profiles (glm, km) - LEGACY FALLBACK
|
||||
if (config.profiles) {
|
||||
for (const candidate of getProfileLookupCandidates(profileName)) {
|
||||
if (!config.profiles[candidate]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const viaLegacyAlias = isLegacyProfileAlias(profileName, candidate);
|
||||
return {
|
||||
type: 'settings',
|
||||
name: profileName,
|
||||
settingsPath: config.profiles[candidate],
|
||||
message: viaLegacyAlias
|
||||
? `Using legacy API profile "${candidate}" for "${profileName}".`
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 4: Check account-based profiles (work, personal) - LEGACY FALLBACK
|
||||
|
||||
+6
-3
@@ -9,6 +9,7 @@ import {
|
||||
setGlobalConfigDir,
|
||||
detectCloudSyncPath,
|
||||
} from './utils/config-manager';
|
||||
import { expandPath } from './utils/helpers';
|
||||
import { validateGlmKey, validateMiniMaxKey } from './utils/api-key-validator';
|
||||
import { ErrorManager } from './utils/error-manager';
|
||||
import { execClaudeWithCLIProxy, CLIProxyProvider } from './cliproxy';
|
||||
@@ -689,7 +690,7 @@ async function main(): Promise<void> {
|
||||
if (profileInfo.type === 'settings' && profileInfo.name === 'glmt') {
|
||||
console.error(fail(`${targetAdapter.displayName} does not support GLMT proxy profiles`));
|
||||
console.error(
|
||||
info('Use --target claude for glmt, or switch to a direct API profile (glm/kimi)')
|
||||
info('Use --target claude for glmt, or switch to a direct API profile (glm/km)')
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -856,7 +857,7 @@ async function main(): Promise<void> {
|
||||
fail(`${targetAdapter?.displayName || 'Target'} does not support GLMT proxy profiles`)
|
||||
);
|
||||
console.error(
|
||||
info('Use --target claude for glmt, or switch to a direct API profile (glm/kimi)')
|
||||
info('Use --target claude for glmt, or switch to a direct API profile (glm/km)')
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -865,7 +866,9 @@ async function main(): Promise<void> {
|
||||
} else {
|
||||
// EXISTING FLOW: Settings-based profile (glm)
|
||||
// Use --settings flag (backward compatible)
|
||||
const expandedSettingsPath = getSettingsPath(profileInfo.name);
|
||||
const expandedSettingsPath = profileInfo.settingsPath
|
||||
? expandPath(profileInfo.settingsPath)
|
||||
: getSettingsPath(profileInfo.name);
|
||||
const webSearchEnv = getWebSearchHookEnv();
|
||||
const imageAnalysisEnv = getImageAnalysisHookEnv(profileInfo.name);
|
||||
// Get global env vars (DISABLE_TELEMETRY, etc.) for third-party profiles
|
||||
|
||||
@@ -447,7 +447,7 @@ async function showHelp(): Promise<void> {
|
||||
console.log('');
|
||||
console.log(subheader('Options'));
|
||||
console.log(
|
||||
` ${color('--preset <id>', 'command')} Use provider preset (openrouter, ollama, ollama-cloud, glm, glmt, kimi, foundry, mm, deepseek, qwen)`
|
||||
` ${color('--preset <id>', 'command')} Use provider preset (openrouter, ollama, ollama-cloud, glm, glmt, km, foundry, mm, deepseek, qwen)`
|
||||
);
|
||||
console.log(` ${color('--base-url <url>', 'command')} API base URL (create)`);
|
||||
console.log(` ${color('--api-key <key>', 'command')} API key (create)`);
|
||||
@@ -467,7 +467,8 @@ async function showHelp(): Promise<void> {
|
||||
);
|
||||
console.log(` ${color('glm', 'command')} GLM - Claude via Z.AI`);
|
||||
console.log(` ${color('glmt', 'command')} GLMT - GLM with Thinking mode`);
|
||||
console.log(` ${color('kimi', 'command')} Kimi - Moonshot AI reasoning model`);
|
||||
console.log(` ${color('km', 'command')} Kimi - Moonshot AI reasoning model`);
|
||||
console.log(` ${dim(' Legacy alias: --preset kimi (auto-mapped to km)')}`);
|
||||
console.log(` ${color('foundry', 'command')} Azure Foundry - Claude via Microsoft Azure`);
|
||||
console.log(` ${color('mm', 'command')} Minimax - M2 series with 1M context`);
|
||||
console.log(` ${color('deepseek', 'command')} DeepSeek - V3.2 and R1 reasoning (128K)`);
|
||||
|
||||
@@ -14,6 +14,7 @@ import { isUnifiedMode, loadUnifiedConfig } from '../config/unified-config-loade
|
||||
import { expandPath } from '../utils/helpers';
|
||||
import { getCcsDir } from '../utils/config-manager';
|
||||
import { ProfileRegistry } from '../auth/profile-registry';
|
||||
import { getProfileLookupCandidates } from '../utils/profile-compat';
|
||||
|
||||
type ShellType = 'bash' | 'fish' | 'powershell';
|
||||
type OutputFormat = 'openai' | 'anthropic' | 'raw';
|
||||
@@ -101,15 +102,22 @@ function isCLIProxyProfile(name: string): boolean {
|
||||
return (CLIPROXY_PROFILES as readonly string[]).includes(name);
|
||||
}
|
||||
|
||||
/** Resolve env vars for settings-based profiles (glm, kimi, custom API profiles) */
|
||||
/** Resolve env vars for settings-based profiles (glm, km, custom API profiles) */
|
||||
function resolveSettingsProfile(profileName: string): Record<string, string> | null {
|
||||
if (!isUnifiedMode()) return null;
|
||||
|
||||
const config = loadUnifiedConfig();
|
||||
if (!config) return null;
|
||||
|
||||
// Check unified config profiles section
|
||||
const profileConfig = config.profiles?.[profileName];
|
||||
// Check unified config profiles section (supports compatibility aliases, e.g. km -> kimi)
|
||||
let profileConfig: { type?: string; settings?: string } | undefined;
|
||||
for (const candidate of getProfileLookupCandidates(profileName)) {
|
||||
const candidateConfig = config.profiles?.[candidate];
|
||||
if (candidateConfig) {
|
||||
profileConfig = candidateConfig;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!profileConfig) return null;
|
||||
|
||||
if (profileConfig.type !== 'api') {
|
||||
@@ -225,7 +233,7 @@ export async function handleEnvCommand(args: string[]): Promise<void> {
|
||||
if (v !== undefined) envVars[k] = v;
|
||||
}
|
||||
} else {
|
||||
// Settings-based profile (glm, kimi, custom API)
|
||||
// Settings-based profile (glm, km, custom API)
|
||||
const resolved = resolveSettingsProfile(profile);
|
||||
if (!resolved) {
|
||||
// Check if it's an account-based profile
|
||||
|
||||
@@ -390,7 +390,7 @@ async function showHelp(): Promise<void> {
|
||||
);
|
||||
console.log('');
|
||||
console.log(subheader('Supported Profile Types'));
|
||||
console.log(` ${color('API profiles', 'command')} glm, glmt, kimi, custom API profiles`);
|
||||
console.log(` ${color('API profiles', 'command')} glm, glmt, km, custom API profiles`);
|
||||
console.log(` ${color('CLIProxy', 'command')} gemini, codex, agy, qwen, kiro, ghcp`);
|
||||
console.log(` ${color('Copilot', 'command')} copilot (requires copilot-api daemon)`);
|
||||
console.log(` ${dim('Account-based')} Not supported (uses CLAUDE_CONFIG_DIR)`);
|
||||
|
||||
@@ -371,7 +371,7 @@ async function runSetupWizard(force: boolean = false): Promise<void> {
|
||||
console.log(' Use the following commands to create profiles:');
|
||||
console.log('');
|
||||
console.log(' ccs api create glm --preset glm');
|
||||
console.log(' ccs api create kimi --preset kimi');
|
||||
console.log(' ccs api create km --preset km');
|
||||
console.log(' ccs api create custom --prompt');
|
||||
console.log('');
|
||||
console.log(' After creating, edit the settings file to add your API key.');
|
||||
|
||||
@@ -9,6 +9,7 @@ import * as fs from 'fs';
|
||||
import { initUI, header, subheader, color, warn } from '../utils/ui';
|
||||
import { getActiveConfigPath, getCcsDir } from '../utils/config-manager';
|
||||
import { getVersion } from '../utils/version';
|
||||
import { getProfileLookupCandidates } from '../utils/profile-compat';
|
||||
|
||||
/**
|
||||
* Handle version command
|
||||
@@ -38,18 +39,23 @@ export async function handleVersionCommand(): Promise<void> {
|
||||
const readyProfiles: string[] = [];
|
||||
|
||||
// Check for profiles with valid API keys
|
||||
for (const profile of ['glm', 'kimi']) {
|
||||
const settingsPath = path.join(ccsDir, `${profile}.settings.json`);
|
||||
if (fs.existsSync(settingsPath)) {
|
||||
try {
|
||||
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
||||
const apiKey = settings.env?.ANTHROPIC_AUTH_TOKEN;
|
||||
if (apiKey && !apiKey.match(/YOUR_.*_API_KEY_HERE/) && !apiKey.match(/sk-test.*/)) {
|
||||
readyProfiles.push(profile);
|
||||
}
|
||||
} catch (_error) {
|
||||
// Invalid JSON, skip
|
||||
for (const profile of ['glm', 'km']) {
|
||||
const settingsPath = getProfileLookupCandidates(profile)
|
||||
.map((candidate) => path.join(ccsDir, `${candidate}.settings.json`))
|
||||
.find((candidatePath) => fs.existsSync(candidatePath));
|
||||
|
||||
if (!settingsPath) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
||||
const apiKey = settings.env?.ANTHROPIC_AUTH_TOKEN;
|
||||
if (apiKey && !apiKey.match(/YOUR_.*_API_KEY_HERE/) && !apiKey.match(/sk-test.*/)) {
|
||||
readyProfiles.push(profile);
|
||||
}
|
||||
} catch (_error) {
|
||||
// Invalid JSON, skip
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,9 @@ import { saveUnifiedConfig, hasUnifiedConfig, loadUnifiedConfig } from './unifie
|
||||
import { infoBox, warn } from '../utils/ui';
|
||||
|
||||
const BACKUP_DIR_PREFIX = 'backup-v1-';
|
||||
const LEGACY_API_PROFILE_RENAMES: Readonly<Record<string, string>> = Object.freeze({
|
||||
kimi: 'km',
|
||||
});
|
||||
|
||||
/**
|
||||
* Migration result with details about what was migrated.
|
||||
@@ -182,6 +185,7 @@ export async function migrate(dryRun = false): Promise<MigrationResult> {
|
||||
// config.yaml only stores reference to the settings file
|
||||
if (oldConfig?.profiles) {
|
||||
for (const [name, settingsPath] of Object.entries(oldConfig.profiles)) {
|
||||
const targetName = LEGACY_API_PROFILE_RENAMES[name] || name;
|
||||
const pathStr = settingsPath as string;
|
||||
const expandedPath = expandPath(pathStr);
|
||||
|
||||
@@ -191,13 +195,30 @@ export async function migrate(dryRun = false): Promise<MigrationResult> {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (unifiedConfig.profiles[targetName]) {
|
||||
const existing = unifiedConfig.profiles[targetName].settings;
|
||||
if (existing !== pathStr) {
|
||||
warnings.push(
|
||||
`Skipped ${name}: target profile "${targetName}" already exists with different settings (${existing})`
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Store reference to settings file (keep using ~ for portability)
|
||||
const profile: ProfileConfig = {
|
||||
type: 'api',
|
||||
settings: pathStr,
|
||||
};
|
||||
unifiedConfig.profiles[name] = profile;
|
||||
migratedFiles.push(`config.json.profiles.${name} → config.yaml (settings: ${pathStr})`);
|
||||
unifiedConfig.profiles[targetName] = profile;
|
||||
migratedFiles.push(
|
||||
`config.json.profiles.${name} → config.yaml.profiles.${targetName} (settings: ${pathStr})`
|
||||
);
|
||||
if (targetName !== name) {
|
||||
warnings.push(
|
||||
`Renamed legacy API profile "${name}" to "${targetName}" (ccs kimi API profile is now ccs km)`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -456,15 +477,16 @@ async function migrateProfilesToUnified(
|
||||
|
||||
// Migrate API profiles from config.json
|
||||
for (const [name, settingsPath] of Object.entries(oldConfig.profiles)) {
|
||||
const targetName = LEGACY_API_PROFILE_RENAMES[name] || name;
|
||||
const pathStr = settingsPath as string;
|
||||
|
||||
// H7: Detect collision - profile exists in both configs
|
||||
if (unifiedConfig.profiles[name]) {
|
||||
if (unifiedConfig.profiles[targetName]) {
|
||||
// Check if settings differ (potential data loss)
|
||||
const existingSettings = unifiedConfig.profiles[name].settings;
|
||||
const existingSettings = unifiedConfig.profiles[targetName].settings;
|
||||
if (existingSettings && existingSettings !== pathStr) {
|
||||
warnings.push(
|
||||
`Profile "${name}" exists in both configs with different settings - keeping existing (${existingSettings}), skipping legacy (${pathStr})`
|
||||
`Profile "${targetName}" exists in both configs with different settings - keeping existing (${existingSettings}), skipping legacy ${name} (${pathStr})`
|
||||
);
|
||||
}
|
||||
continue;
|
||||
@@ -479,11 +501,16 @@ async function migrateProfilesToUnified(
|
||||
}
|
||||
|
||||
// Store reference to settings file
|
||||
unifiedConfig.profiles[name] = {
|
||||
unifiedConfig.profiles[targetName] = {
|
||||
type: 'api',
|
||||
settings: pathStr,
|
||||
};
|
||||
migratedFiles.push(name);
|
||||
migratedFiles.push(targetName);
|
||||
if (targetName !== name) {
|
||||
warnings.push(
|
||||
`Renamed legacy API profile "${name}" to "${targetName}" (ccs kimi API profile is now ccs km)`
|
||||
);
|
||||
}
|
||||
modified = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -320,7 +320,7 @@ export class DelegationHandler {
|
||||
if (!profile) {
|
||||
console.error(fail('No profile specified'));
|
||||
console.error(' Usage: ccs <profile> -p "task"');
|
||||
console.error(' Examples: ccs glm -p "task", ccs kimi -p "task"');
|
||||
console.error(' Examples: ccs glm -p "task", ccs km -p "task"');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import { type ExecutionOptions, type ExecutionResult, type StreamMessage } from
|
||||
import { StreamBuffer, formatToolVerbose } from './executor/stream-parser';
|
||||
import { buildExecutionResult } from './executor/result-aggregator';
|
||||
import { getCcsDir, getModelDisplayName } from '../utils/config-manager';
|
||||
import { getProfileLookupCandidates } from '../utils/profile-compat';
|
||||
|
||||
// Re-export types for consumers
|
||||
export type { ExecutionOptions, ExecutionResult, StreamMessage } from './executor/types';
|
||||
@@ -26,7 +27,7 @@ export type { ExecutionOptions, ExecutionResult, StreamMessage } from './executo
|
||||
export class HeadlessExecutor {
|
||||
/**
|
||||
* Execute task via headless Claude CLI
|
||||
* @param profile - Profile name (glm, kimi, custom)
|
||||
* @param profile - Profile name (glm, km, custom)
|
||||
* @param enhancedPrompt - Enhanced prompt with context
|
||||
* @param options - Execution options
|
||||
* @returns execution result
|
||||
@@ -63,13 +64,18 @@ export class HeadlessExecutor {
|
||||
);
|
||||
}
|
||||
|
||||
// Get settings path for profile
|
||||
const settingsPath = path.join(getCcsDir(), `${profile}.settings.json`);
|
||||
// Get settings path for profile (supports compatibility aliases like km -> kimi)
|
||||
const ccsDir = getCcsDir();
|
||||
const settingsCandidates = getProfileLookupCandidates(profile).map((candidate) =>
|
||||
path.join(ccsDir, `${candidate}.settings.json`)
|
||||
);
|
||||
const settingsPath = settingsCandidates.find((candidatePath) => fs.existsSync(candidatePath));
|
||||
const primarySettingsPath = path.join(ccsDir, `${profile}.settings.json`);
|
||||
|
||||
// Validate settings file exists
|
||||
if (!fs.existsSync(settingsPath)) {
|
||||
if (!settingsPath) {
|
||||
throw new Error(
|
||||
`Settings file not found: ${settingsPath}\nProfile "${profile}" may not be configured.`
|
||||
`Settings file not found: ${primarySettingsPath}\nProfile "${profile}" may not be configured.`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import * as path from 'path';
|
||||
import { Settings } from '../types';
|
||||
import { ValidationResult } from '../types/utils';
|
||||
import { getCcsDir } from './config-manager';
|
||||
import { getProfileLookupCandidates } from './profile-compat';
|
||||
|
||||
/**
|
||||
* Extended validation result for delegation profiles
|
||||
@@ -24,19 +25,24 @@ interface DelegationValidationResult extends ValidationResult {
|
||||
export class DelegationValidator {
|
||||
/**
|
||||
* Validate a delegation profile
|
||||
* @param profileName - Name of profile to validate (e.g., 'glm', 'kimi')
|
||||
* @param profileName - Name of profile to validate (e.g., 'glm', 'km')
|
||||
* @returns Validation result { valid: boolean, error?: string, settingsPath?: string }
|
||||
*/
|
||||
static validate(profileName: string): DelegationValidationResult {
|
||||
const settingsPath = path.join(getCcsDir(), `${profileName}.settings.json`);
|
||||
const ccsDir = getCcsDir();
|
||||
const candidateSettingsPath = getProfileLookupCandidates(profileName)
|
||||
.map((candidate) => path.join(ccsDir, `${candidate}.settings.json`))
|
||||
.find((candidatePath) => fs.existsSync(candidatePath));
|
||||
const primarySettingsPath = path.join(ccsDir, `${profileName}.settings.json`);
|
||||
const settingsPath = candidateSettingsPath || primarySettingsPath;
|
||||
|
||||
// Check if profile directory exists
|
||||
if (!fs.existsSync(settingsPath)) {
|
||||
if (!candidateSettingsPath) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Profile not found: ${profileName}`,
|
||||
suggestion:
|
||||
`Profile settings missing at: ${settingsPath}\n\n` +
|
||||
`Profile settings missing at: ${primarySettingsPath}\n\n` +
|
||||
`To set up ${profileName} profile:\n` +
|
||||
` 1. Copy base settings: cp config/base-${profileName}.settings.json ~/.ccs/${profileName}.settings.json\n` +
|
||||
` 2. Edit settings: Edit ~/.ccs/${profileName}.settings.json\n` +
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Profile compatibility helpers for renamed commands/profiles.
|
||||
*
|
||||
* Current compatibility mappings:
|
||||
* - `km` is the canonical Kimi API profile command
|
||||
* - `kimi` remains as legacy API profile name in existing user configs
|
||||
*/
|
||||
|
||||
const PROFILE_COMPAT_ALIASES: Readonly<Record<string, readonly string[]>> = Object.freeze({
|
||||
km: ['kimi'],
|
||||
});
|
||||
|
||||
/**
|
||||
* Build lookup candidates for a profile.
|
||||
* Order: exact input -> lowercase form (if different) -> legacy aliases.
|
||||
*/
|
||||
export function getProfileLookupCandidates(profileName: string): string[] {
|
||||
const raw = profileName.trim();
|
||||
const normalized = raw.toLowerCase();
|
||||
const aliases = PROFILE_COMPAT_ALIASES[normalized] || [];
|
||||
const ordered = [raw, normalized, ...aliases];
|
||||
|
||||
return [...new Set(ordered.filter(Boolean))];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a resolved profile name came from a legacy alias.
|
||||
*/
|
||||
export function isLegacyProfileAlias(requestedName: string, resolvedName: string): boolean {
|
||||
const requestedNormalized = requestedName.trim().toLowerCase();
|
||||
const resolvedNormalized = resolvedName.trim().toLowerCase();
|
||||
|
||||
if (requestedNormalized === resolvedNormalized) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const aliases = PROFILE_COMPAT_ALIASES[requestedNormalized] || [];
|
||||
return aliases.includes(resolvedNormalized);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import { getPresetById, isValidPresetId } from '../../../src/api/services/provider-presets';
|
||||
|
||||
describe('provider-presets', () => {
|
||||
it('resolves canonical km preset id', () => {
|
||||
const preset = getPresetById('km');
|
||||
expect(preset?.id).toBe('km');
|
||||
});
|
||||
|
||||
it('resolves legacy kimi preset alias to km', () => {
|
||||
const preset = getPresetById('kimi');
|
||||
expect(preset?.id).toBe('km');
|
||||
});
|
||||
|
||||
it('treats legacy kimi alias as a valid preset id', () => {
|
||||
expect(isValidPresetId('kimi')).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -120,6 +120,43 @@ describe('ProfileDetector', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should resolve km to legacy kimi API profile from unified config', () => {
|
||||
const settingsPath = path.join(tempDir, 'kimi.settings.json');
|
||||
fs.writeFileSync(
|
||||
settingsPath,
|
||||
JSON.stringify({ env: { ANTHROPIC_MODEL: 'kimi-k2-thinking-turbo' } })
|
||||
);
|
||||
|
||||
const mockUnifiedConfig = {
|
||||
version: 2,
|
||||
profiles: {
|
||||
kimi: { settings: settingsPath, type: 'api' },
|
||||
},
|
||||
};
|
||||
|
||||
const isUnifiedModeSpy = spyOn(unifiedConfigLoader, 'isUnifiedMode').mockReturnValue(true);
|
||||
const loadUnifiedConfigSpy = spyOn(unifiedConfigLoader, 'loadUnifiedConfig').mockReturnValue(
|
||||
mockUnifiedConfig as any
|
||||
);
|
||||
|
||||
try {
|
||||
const result = detector.detectProfileType('km');
|
||||
expect(result.type).toBe('settings');
|
||||
expect(result.name).toBe('km');
|
||||
expect(result.settingsPath).toBe(settingsPath);
|
||||
expect(result.env).toEqual({ ANTHROPIC_MODEL: 'kimi-k2-thinking-turbo' });
|
||||
} finally {
|
||||
isUnifiedModeSpy.mockRestore();
|
||||
loadUnifiedConfigSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it('should keep ccs kimi mapped to CLIProxy provider', () => {
|
||||
const result = detector.detectProfileType('kimi');
|
||||
expect(result.type).toBe('cliproxy');
|
||||
expect(result.provider).toBe('kimi');
|
||||
});
|
||||
|
||||
it('should detect account-based profile from unified config', () => {
|
||||
const mockUnifiedConfig = {
|
||||
version: 2,
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import { getProfileLookupCandidates, isLegacyProfileAlias } from '../../../src/utils/profile-compat';
|
||||
|
||||
describe('profile-compat', () => {
|
||||
describe('getProfileLookupCandidates', () => {
|
||||
it('returns km candidates with legacy kimi fallback', () => {
|
||||
expect(getProfileLookupCandidates('km')).toEqual(['km', 'kimi']);
|
||||
});
|
||||
|
||||
it('keeps non-aliased profiles unchanged', () => {
|
||||
expect(getProfileLookupCandidates('glm')).toEqual(['glm']);
|
||||
});
|
||||
|
||||
it('normalizes uppercase input and still resolves aliases', () => {
|
||||
expect(getProfileLookupCandidates('KM')).toEqual(['KM', 'km', 'kimi']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isLegacyProfileAlias', () => {
|
||||
it('returns true for km -> kimi', () => {
|
||||
expect(isLegacyProfileAlias('km', 'kimi')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for canonical names', () => {
|
||||
expect(isLegacyProfileAlias('km', 'km')).toBe(false);
|
||||
expect(isLegacyProfileAlias('glm', 'glm')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for unrelated names', () => {
|
||||
expect(isLegacyProfileAlias('glm', 'kimi')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user