feat(api): persist profile target metadata

- add target fields to API profile and CLIProxy variant service types

- persist target in unified config and legacy profile_targets map

- expose target update API via updateApiProfileTarget
This commit is contained in:
Tam Nhu Tran
2026-02-25 15:51:31 +07:00
parent 3b7395b229
commit 461236e5de
4 changed files with 104 additions and 9 deletions
+2 -1
View File
@@ -15,6 +15,7 @@ export {
type ApiListResult,
type CreateApiProfileResult,
type RemoveApiProfileResult,
type UpdateApiProfileTargetResult,
} from './profile-types';
// Profile read operations
@@ -27,7 +28,7 @@ export {
} from './profile-reader';
// Profile write operations
export { createApiProfile, removeApiProfile } from './profile-writer';
export { createApiProfile, removeApiProfile, updateApiProfileTarget } from './profile-writer';
// OpenRouter catalog and picker
export { isOpenRouterUrl, fetchOpenRouterModels, type OpenRouterModel } from './openrouter-catalog';
+8 -1
View File
@@ -9,6 +9,7 @@ import * as fs from 'fs';
import * as path from 'path';
import { getCcsDir, loadConfigSafe } from '../../utils/config-manager';
import { loadOrCreateUnifiedConfig, isUnifiedMode } from '../../config/unified-config-loader';
import type { TargetType } from '../../targets/target-adapter';
import type { ApiProfileInfo, CliproxyVariantInfo, ApiListResult } from './profile-types';
/**
@@ -68,6 +69,7 @@ export function listApiProfiles(): ApiListResult {
settingsPath: profile.settings || 'config.yaml',
isConfigured: isApiProfileConfigured(name),
configSource: 'unified',
target: profile.target || 'claude',
});
}
// CLIProxy variants
@@ -80,10 +82,13 @@ export function listApiProfiles(): ApiListResult {
name,
provider,
settings: variant?.settings || '-',
target: variant?.target || 'claude',
});
}
} else {
const config = loadConfigSafe();
const legacyTargetMap = (config as { profile_targets?: Record<string, TargetType> })
.profile_targets;
for (const [name, settingsPath] of Object.entries(config.profiles)) {
// Skip 'default' profile - it's the user's native Claude settings
if (name === 'default' && (settingsPath as string).includes('.claude/settings.json')) {
@@ -94,16 +99,18 @@ export function listApiProfiles(): ApiListResult {
settingsPath: settingsPath as string,
isConfigured: isApiProfileConfigured(name),
configSource: 'legacy',
target: legacyTargetMap?.[name] || 'claude',
});
}
// CLIProxy variants
if (config.cliproxy) {
for (const [name, v] of Object.entries(config.cliproxy)) {
const variant = v as { provider: string; settings: string };
const variant = v as { provider: string; settings: string; target?: TargetType };
variants.push({
name,
provider: variant.provider,
settings: variant.settings,
target: variant.target || 'claude',
});
}
}
+11
View File
@@ -4,6 +4,8 @@
* Shared type definitions for API profile services.
*/
import type { TargetType } from '../../targets/target-adapter';
/** Model mapping for API profiles */
export interface ModelMapping {
default: string;
@@ -18,6 +20,7 @@ export interface ApiProfileInfo {
settingsPath: string;
isConfigured: boolean;
configSource: 'unified' | 'legacy';
target: TargetType;
}
/** CLIProxy variant info */
@@ -25,6 +28,7 @@ export interface CliproxyVariantInfo {
name: string;
provider: string;
settings: string;
target: TargetType;
}
/** Result from list operation */
@@ -45,3 +49,10 @@ export interface RemoveApiProfileResult {
success: boolean;
error?: string;
}
/** Result from updating API profile target */
export interface UpdateApiProfileTargetResult {
success: boolean;
target?: TargetType;
error?: string;
}
+83 -7
View File
@@ -12,7 +12,13 @@ import {
isUnifiedMode,
} from '../../config/unified-config-loader';
import { ensureProfileHooks } from '../../utils/websearch/profile-hook-injector';
import type { ModelMapping, CreateApiProfileResult, RemoveApiProfileResult } from './profile-types';
import type { TargetType } from '../../targets/target-adapter';
import type {
ModelMapping,
CreateApiProfileResult,
RemoveApiProfileResult,
UpdateApiProfileTargetResult,
} from './profile-types';
/** Check if URL is an OpenRouter endpoint */
function isOpenRouterUrl(baseUrl: string): boolean {
@@ -51,11 +57,15 @@ function createSettingsFile(
}
/** Update config.json with new API profile (legacy format) */
function updateLegacyConfig(name: string): void {
function updateLegacyConfig(name: string, target: TargetType = 'claude'): void {
const configPath = getConfigPath();
const ccsDir = getCcsDir();
let config: { profiles: Record<string, string>; cliproxy?: Record<string, unknown> };
let config: {
profiles: Record<string, string>;
cliproxy?: Record<string, unknown>;
profile_targets?: Record<string, TargetType>;
};
try {
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
} catch {
@@ -64,6 +74,12 @@ function updateLegacyConfig(name: string): void {
const relativePath = `~/.ccs/${name}.settings.json`;
config.profiles[name] = relativePath;
config.profile_targets = config.profile_targets || {};
if (target === 'claude') {
delete config.profile_targets[name];
} else {
config.profile_targets[name] = target;
}
if (!fs.existsSync(ccsDir)) {
fs.mkdirSync(ccsDir, { recursive: true });
@@ -80,7 +96,8 @@ function createApiProfileUnified(
name: string,
baseUrl: string,
apiKey: string,
models: ModelMapping
models: ModelMapping,
target: TargetType = 'claude'
): void {
const ccsDir = getCcsDir();
const settingsFile = `${name}.settings.json`;
@@ -112,6 +129,7 @@ function createApiProfileUnified(
config.profiles[name] = {
type: 'api',
settings: `~/.ccs/${settingsFile}`,
...(target !== 'claude' && { target }),
};
saveUnifiedConfig(config);
}
@@ -121,16 +139,17 @@ export function createApiProfile(
name: string,
baseUrl: string,
apiKey: string,
models: ModelMapping
models: ModelMapping,
target: TargetType = 'claude'
): CreateApiProfileResult {
try {
const settingsFile = `~/.ccs/${name}.settings.json`;
if (isUnifiedMode()) {
createApiProfileUnified(name, baseUrl, apiKey, models);
createApiProfileUnified(name, baseUrl, apiKey, models, target);
} else {
createSettingsFile(name, baseUrl, apiKey, models);
updateLegacyConfig(name);
updateLegacyConfig(name, target);
}
return { success: true, settingsFile };
@@ -143,6 +162,63 @@ export function createApiProfile(
}
}
/**
* Update API profile target (claude/droid).
* Persists to config.yaml in unified mode and config.json profile_targets in legacy mode.
*/
export function updateApiProfileTarget(
name: string,
target: TargetType
): UpdateApiProfileTargetResult {
try {
if (isUnifiedMode()) {
const config = loadOrCreateUnifiedConfig();
if (!config.profiles[name]) {
return { success: false, error: `API profile not found: ${name}` };
}
if (target === 'claude') {
delete config.profiles[name].target;
} else {
config.profiles[name].target = target;
}
saveUnifiedConfig(config);
return { success: true, target };
}
const configPath = getConfigPath();
let config: {
profiles: Record<string, string>;
cliproxy?: Record<string, unknown>;
profile_targets?: Record<string, TargetType>;
};
try {
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
} catch {
config = { profiles: {} };
}
if (!config.profiles[name]) {
return { success: false, error: `API profile not found: ${name}` };
}
config.profile_targets = config.profile_targets || {};
if (target === 'claude') {
delete config.profile_targets[name];
} else {
config.profile_targets[name] = target;
}
const tempPath = configPath + '.tmp';
fs.writeFileSync(tempPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
fs.renameSync(tempPath, configPath);
return { success: true, target };
} catch (error) {
return { success: false, error: (error as Error).message };
}
}
/** Remove API profile from unified config */
function removeApiProfileUnified(name: string): void {
const config = loadOrCreateUnifiedConfig();