mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-10 02:17:11 +00:00
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:
@@ -15,6 +15,7 @@ export {
|
|||||||
type ApiListResult,
|
type ApiListResult,
|
||||||
type CreateApiProfileResult,
|
type CreateApiProfileResult,
|
||||||
type RemoveApiProfileResult,
|
type RemoveApiProfileResult,
|
||||||
|
type UpdateApiProfileTargetResult,
|
||||||
} from './profile-types';
|
} from './profile-types';
|
||||||
|
|
||||||
// Profile read operations
|
// Profile read operations
|
||||||
@@ -27,7 +28,7 @@ export {
|
|||||||
} from './profile-reader';
|
} from './profile-reader';
|
||||||
|
|
||||||
// Profile write operations
|
// Profile write operations
|
||||||
export { createApiProfile, removeApiProfile } from './profile-writer';
|
export { createApiProfile, removeApiProfile, updateApiProfileTarget } from './profile-writer';
|
||||||
|
|
||||||
// OpenRouter catalog and picker
|
// OpenRouter catalog and picker
|
||||||
export { isOpenRouterUrl, fetchOpenRouterModels, type OpenRouterModel } from './openrouter-catalog';
|
export { isOpenRouterUrl, fetchOpenRouterModels, type OpenRouterModel } from './openrouter-catalog';
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import * as fs from 'fs';
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { getCcsDir, loadConfigSafe } from '../../utils/config-manager';
|
import { getCcsDir, loadConfigSafe } from '../../utils/config-manager';
|
||||||
import { loadOrCreateUnifiedConfig, isUnifiedMode } from '../../config/unified-config-loader';
|
import { loadOrCreateUnifiedConfig, isUnifiedMode } from '../../config/unified-config-loader';
|
||||||
|
import type { TargetType } from '../../targets/target-adapter';
|
||||||
import type { ApiProfileInfo, CliproxyVariantInfo, ApiListResult } from './profile-types';
|
import type { ApiProfileInfo, CliproxyVariantInfo, ApiListResult } from './profile-types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -68,6 +69,7 @@ export function listApiProfiles(): ApiListResult {
|
|||||||
settingsPath: profile.settings || 'config.yaml',
|
settingsPath: profile.settings || 'config.yaml',
|
||||||
isConfigured: isApiProfileConfigured(name),
|
isConfigured: isApiProfileConfigured(name),
|
||||||
configSource: 'unified',
|
configSource: 'unified',
|
||||||
|
target: profile.target || 'claude',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// CLIProxy variants
|
// CLIProxy variants
|
||||||
@@ -80,10 +82,13 @@ export function listApiProfiles(): ApiListResult {
|
|||||||
name,
|
name,
|
||||||
provider,
|
provider,
|
||||||
settings: variant?.settings || '-',
|
settings: variant?.settings || '-',
|
||||||
|
target: variant?.target || 'claude',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const config = loadConfigSafe();
|
const config = loadConfigSafe();
|
||||||
|
const legacyTargetMap = (config as { profile_targets?: Record<string, TargetType> })
|
||||||
|
.profile_targets;
|
||||||
for (const [name, settingsPath] of Object.entries(config.profiles)) {
|
for (const [name, settingsPath] of Object.entries(config.profiles)) {
|
||||||
// Skip 'default' profile - it's the user's native Claude settings
|
// Skip 'default' profile - it's the user's native Claude settings
|
||||||
if (name === 'default' && (settingsPath as string).includes('.claude/settings.json')) {
|
if (name === 'default' && (settingsPath as string).includes('.claude/settings.json')) {
|
||||||
@@ -94,16 +99,18 @@ export function listApiProfiles(): ApiListResult {
|
|||||||
settingsPath: settingsPath as string,
|
settingsPath: settingsPath as string,
|
||||||
isConfigured: isApiProfileConfigured(name),
|
isConfigured: isApiProfileConfigured(name),
|
||||||
configSource: 'legacy',
|
configSource: 'legacy',
|
||||||
|
target: legacyTargetMap?.[name] || 'claude',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// CLIProxy variants
|
// CLIProxy variants
|
||||||
if (config.cliproxy) {
|
if (config.cliproxy) {
|
||||||
for (const [name, v] of Object.entries(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({
|
variants.push({
|
||||||
name,
|
name,
|
||||||
provider: variant.provider,
|
provider: variant.provider,
|
||||||
settings: variant.settings,
|
settings: variant.settings,
|
||||||
|
target: variant.target || 'claude',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
* Shared type definitions for API profile services.
|
* Shared type definitions for API profile services.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { TargetType } from '../../targets/target-adapter';
|
||||||
|
|
||||||
/** Model mapping for API profiles */
|
/** Model mapping for API profiles */
|
||||||
export interface ModelMapping {
|
export interface ModelMapping {
|
||||||
default: string;
|
default: string;
|
||||||
@@ -18,6 +20,7 @@ export interface ApiProfileInfo {
|
|||||||
settingsPath: string;
|
settingsPath: string;
|
||||||
isConfigured: boolean;
|
isConfigured: boolean;
|
||||||
configSource: 'unified' | 'legacy';
|
configSource: 'unified' | 'legacy';
|
||||||
|
target: TargetType;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** CLIProxy variant info */
|
/** CLIProxy variant info */
|
||||||
@@ -25,6 +28,7 @@ export interface CliproxyVariantInfo {
|
|||||||
name: string;
|
name: string;
|
||||||
provider: string;
|
provider: string;
|
||||||
settings: string;
|
settings: string;
|
||||||
|
target: TargetType;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Result from list operation */
|
/** Result from list operation */
|
||||||
@@ -45,3 +49,10 @@ export interface RemoveApiProfileResult {
|
|||||||
success: boolean;
|
success: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Result from updating API profile target */
|
||||||
|
export interface UpdateApiProfileTargetResult {
|
||||||
|
success: boolean;
|
||||||
|
target?: TargetType;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,7 +12,13 @@ import {
|
|||||||
isUnifiedMode,
|
isUnifiedMode,
|
||||||
} from '../../config/unified-config-loader';
|
} from '../../config/unified-config-loader';
|
||||||
import { ensureProfileHooks } from '../../utils/websearch/profile-hook-injector';
|
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 */
|
/** Check if URL is an OpenRouter endpoint */
|
||||||
function isOpenRouterUrl(baseUrl: string): boolean {
|
function isOpenRouterUrl(baseUrl: string): boolean {
|
||||||
@@ -51,11 +57,15 @@ function createSettingsFile(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Update config.json with new API profile (legacy format) */
|
/** 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 configPath = getConfigPath();
|
||||||
const ccsDir = getCcsDir();
|
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 {
|
try {
|
||||||
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||||
} catch {
|
} catch {
|
||||||
@@ -64,6 +74,12 @@ function updateLegacyConfig(name: string): void {
|
|||||||
|
|
||||||
const relativePath = `~/.ccs/${name}.settings.json`;
|
const relativePath = `~/.ccs/${name}.settings.json`;
|
||||||
config.profiles[name] = relativePath;
|
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)) {
|
if (!fs.existsSync(ccsDir)) {
|
||||||
fs.mkdirSync(ccsDir, { recursive: true });
|
fs.mkdirSync(ccsDir, { recursive: true });
|
||||||
@@ -80,7 +96,8 @@ function createApiProfileUnified(
|
|||||||
name: string,
|
name: string,
|
||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
apiKey: string,
|
apiKey: string,
|
||||||
models: ModelMapping
|
models: ModelMapping,
|
||||||
|
target: TargetType = 'claude'
|
||||||
): void {
|
): void {
|
||||||
const ccsDir = getCcsDir();
|
const ccsDir = getCcsDir();
|
||||||
const settingsFile = `${name}.settings.json`;
|
const settingsFile = `${name}.settings.json`;
|
||||||
@@ -112,6 +129,7 @@ function createApiProfileUnified(
|
|||||||
config.profiles[name] = {
|
config.profiles[name] = {
|
||||||
type: 'api',
|
type: 'api',
|
||||||
settings: `~/.ccs/${settingsFile}`,
|
settings: `~/.ccs/${settingsFile}`,
|
||||||
|
...(target !== 'claude' && { target }),
|
||||||
};
|
};
|
||||||
saveUnifiedConfig(config);
|
saveUnifiedConfig(config);
|
||||||
}
|
}
|
||||||
@@ -121,16 +139,17 @@ export function createApiProfile(
|
|||||||
name: string,
|
name: string,
|
||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
apiKey: string,
|
apiKey: string,
|
||||||
models: ModelMapping
|
models: ModelMapping,
|
||||||
|
target: TargetType = 'claude'
|
||||||
): CreateApiProfileResult {
|
): CreateApiProfileResult {
|
||||||
try {
|
try {
|
||||||
const settingsFile = `~/.ccs/${name}.settings.json`;
|
const settingsFile = `~/.ccs/${name}.settings.json`;
|
||||||
|
|
||||||
if (isUnifiedMode()) {
|
if (isUnifiedMode()) {
|
||||||
createApiProfileUnified(name, baseUrl, apiKey, models);
|
createApiProfileUnified(name, baseUrl, apiKey, models, target);
|
||||||
} else {
|
} else {
|
||||||
createSettingsFile(name, baseUrl, apiKey, models);
|
createSettingsFile(name, baseUrl, apiKey, models);
|
||||||
updateLegacyConfig(name);
|
updateLegacyConfig(name, target);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: true, settingsFile };
|
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 */
|
/** Remove API profile from unified config */
|
||||||
function removeApiProfileUnified(name: string): void {
|
function removeApiProfileUnified(name: string): void {
|
||||||
const config = loadOrCreateUnifiedConfig();
|
const config = loadOrCreateUnifiedConfig();
|
||||||
|
|||||||
Reference in New Issue
Block a user