mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 18:18:43 +00:00
feat(api): improve create UX with URL validation and model mapping
- Add URL warning for common mistakes (e.g., /chat/completions endpoint) - Add optional model mapping prompt for Opus/Sonnet/Haiku backends - Show edit hint after profile creation for modifying settings - Support custom model configurations in unified config mode Closes #72
This commit is contained in:
+143
-35
@@ -93,7 +93,7 @@ function validateApiName(name: string): string | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate URL format
|
* Validate URL format and warn about common mistakes
|
||||||
*/
|
*/
|
||||||
function validateUrl(url: string): string | null {
|
function validateUrl(url: string): string | null {
|
||||||
if (!url) {
|
if (!url) {
|
||||||
@@ -107,6 +107,25 @@ function validateUrl(url: string): string | null {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if URL looks like it includes endpoint path (common mistake)
|
||||||
|
* Returns warning message if problematic, null if OK
|
||||||
|
*/
|
||||||
|
function getUrlWarning(url: string): string | null {
|
||||||
|
const problematicPaths = ['/chat/completions', '/v1/messages', '/messages', '/completions'];
|
||||||
|
const lowerUrl = url.toLowerCase();
|
||||||
|
|
||||||
|
for (const path of problematicPaths) {
|
||||||
|
if (lowerUrl.endsWith(path)) {
|
||||||
|
return (
|
||||||
|
`URL ends with "${path}" - Claude appends this automatically.\n` +
|
||||||
|
` You likely want: ${url.replace(new RegExp(path + '$', 'i'), '')}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if unified config mode is active
|
* Check if unified config mode is active
|
||||||
*/
|
*/
|
||||||
@@ -130,11 +149,24 @@ function apiExists(name: string): boolean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Model mapping for API profiles */
|
||||||
|
interface ModelMapping {
|
||||||
|
default: string;
|
||||||
|
opus: string;
|
||||||
|
sonnet: string;
|
||||||
|
haiku: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create settings.json file for API profile
|
* Create settings.json file for API profile
|
||||||
* Includes all 4 model fields for proper Claude CLI integration
|
* Includes all 4 model fields for proper Claude CLI integration
|
||||||
*/
|
*/
|
||||||
function createSettingsFile(name: string, baseUrl: string, apiKey: string, model: string): string {
|
function createSettingsFile(
|
||||||
|
name: string,
|
||||||
|
baseUrl: string,
|
||||||
|
apiKey: string,
|
||||||
|
models: ModelMapping
|
||||||
|
): string {
|
||||||
const ccsDir = getCcsDir();
|
const ccsDir = getCcsDir();
|
||||||
const settingsPath = path.join(ccsDir, `${name}.settings.json`);
|
const settingsPath = path.join(ccsDir, `${name}.settings.json`);
|
||||||
|
|
||||||
@@ -142,10 +174,10 @@ function createSettingsFile(name: string, baseUrl: string, apiKey: string, model
|
|||||||
env: {
|
env: {
|
||||||
ANTHROPIC_BASE_URL: baseUrl,
|
ANTHROPIC_BASE_URL: baseUrl,
|
||||||
ANTHROPIC_AUTH_TOKEN: apiKey,
|
ANTHROPIC_AUTH_TOKEN: apiKey,
|
||||||
ANTHROPIC_MODEL: model,
|
ANTHROPIC_MODEL: models.default,
|
||||||
ANTHROPIC_DEFAULT_OPUS_MODEL: model,
|
ANTHROPIC_DEFAULT_OPUS_MODEL: models.opus,
|
||||||
ANTHROPIC_DEFAULT_SONNET_MODEL: model,
|
ANTHROPIC_DEFAULT_SONNET_MODEL: models.sonnet,
|
||||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: model,
|
ANTHROPIC_DEFAULT_HAIKU_MODEL: models.haiku,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -192,7 +224,7 @@ function createApiProfileUnified(
|
|||||||
name: string,
|
name: string,
|
||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
apiKey: string,
|
apiKey: string,
|
||||||
model: string
|
models: ModelMapping
|
||||||
): void {
|
): void {
|
||||||
const ccsDir = path.join(os.homedir(), '.ccs');
|
const ccsDir = path.join(os.homedir(), '.ccs');
|
||||||
const settingsFile = `${name}.settings.json`;
|
const settingsFile = `${name}.settings.json`;
|
||||||
@@ -203,10 +235,10 @@ function createApiProfileUnified(
|
|||||||
env: {
|
env: {
|
||||||
ANTHROPIC_BASE_URL: baseUrl,
|
ANTHROPIC_BASE_URL: baseUrl,
|
||||||
ANTHROPIC_AUTH_TOKEN: apiKey,
|
ANTHROPIC_AUTH_TOKEN: apiKey,
|
||||||
ANTHROPIC_MODEL: model,
|
ANTHROPIC_MODEL: models.default,
|
||||||
ANTHROPIC_DEFAULT_OPUS_MODEL: model,
|
ANTHROPIC_DEFAULT_OPUS_MODEL: models.opus,
|
||||||
ANTHROPIC_DEFAULT_SONNET_MODEL: model,
|
ANTHROPIC_DEFAULT_SONNET_MODEL: models.sonnet,
|
||||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: model,
|
ANTHROPIC_DEFAULT_HAIKU_MODEL: models.haiku,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -293,9 +325,12 @@ async function handleCreate(args: string[]): Promise<void> {
|
|||||||
// Step 2: Base URL
|
// Step 2: Base URL
|
||||||
let baseUrl = parsedArgs.baseUrl;
|
let baseUrl = parsedArgs.baseUrl;
|
||||||
if (!baseUrl) {
|
if (!baseUrl) {
|
||||||
baseUrl = await InteractivePrompt.input('API Base URL (e.g., https://api.example.com)', {
|
baseUrl = await InteractivePrompt.input(
|
||||||
validate: validateUrl,
|
'API Base URL (e.g., https://api.example.com/v1 - without /chat/completions)',
|
||||||
});
|
{
|
||||||
|
validate: validateUrl,
|
||||||
|
}
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
const error = validateUrl(baseUrl);
|
const error = validateUrl(baseUrl);
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -304,6 +339,23 @@ async function handleCreate(args: string[]): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for common URL mistakes and warn
|
||||||
|
const urlWarning = getUrlWarning(baseUrl);
|
||||||
|
if (urlWarning) {
|
||||||
|
console.log('');
|
||||||
|
console.log(warn(urlWarning));
|
||||||
|
const continueAnyway = await InteractivePrompt.confirm('Continue with this URL anyway?', {
|
||||||
|
default: false,
|
||||||
|
});
|
||||||
|
if (!continueAnyway) {
|
||||||
|
// Let user re-enter URL
|
||||||
|
baseUrl = await InteractivePrompt.input('API Base URL', {
|
||||||
|
validate: validateUrl,
|
||||||
|
default: baseUrl.replace(/\/(chat\/completions|v1\/messages|messages|completions)$/i, ''),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Step 3: API Key
|
// Step 3: API Key
|
||||||
let apiKey = parsedArgs.apiKey;
|
let apiKey = parsedArgs.apiKey;
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
@@ -324,44 +376,100 @@ async function handleCreate(args: string[]): Promise<void> {
|
|||||||
}
|
}
|
||||||
model = model || defaultModel;
|
model = model || defaultModel;
|
||||||
|
|
||||||
|
// Step 5: Optional model mapping for Opus/Sonnet/Haiku
|
||||||
|
// Ask user if they want different models for each type
|
||||||
|
let opusModel = model;
|
||||||
|
let sonnetModel = model;
|
||||||
|
let haikuModel = model;
|
||||||
|
|
||||||
|
if (!parsedArgs.yes) {
|
||||||
|
console.log('');
|
||||||
|
console.log(dim('Some API proxies route different model types to different backends.'));
|
||||||
|
const wantCustomMapping = await InteractivePrompt.confirm(
|
||||||
|
'Configure different models for Opus/Sonnet/Haiku?',
|
||||||
|
{ default: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (wantCustomMapping) {
|
||||||
|
console.log('');
|
||||||
|
console.log(dim('Leave blank to use the default model for each.'));
|
||||||
|
opusModel = (await InteractivePrompt.input('Opus model', { default: model })) || model;
|
||||||
|
sonnetModel = (await InteractivePrompt.input('Sonnet model', { default: model })) || model;
|
||||||
|
haikuModel = (await InteractivePrompt.input('Haiku model', { default: model })) || model;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build model mapping
|
||||||
|
const models: ModelMapping = {
|
||||||
|
default: model,
|
||||||
|
opus: opusModel,
|
||||||
|
sonnet: sonnetModel,
|
||||||
|
haiku: haikuModel,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if custom model mapping is configured
|
||||||
|
const hasCustomMapping = opusModel !== model || sonnetModel !== model || haikuModel !== model;
|
||||||
|
|
||||||
// Create files
|
// Create files
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log(info('Creating API profile...'));
|
console.log(info('Creating API profile...'));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const settingsFile = `~/.ccs/${name}.settings.json`;
|
||||||
|
|
||||||
if (isUnifiedMode()) {
|
if (isUnifiedMode()) {
|
||||||
// Use unified config format
|
// Use unified config format
|
||||||
createApiProfileUnified(name, baseUrl, apiKey, model);
|
createApiProfileUnified(name, baseUrl, apiKey, models);
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log(
|
|
||||||
infoBox(
|
// Build info message
|
||||||
`API: ${name}\n` +
|
let infoMsg =
|
||||||
`Config: ~/.ccs/config.yaml\n` +
|
`API: ${name}\n` +
|
||||||
`Secrets: ~/.ccs/secrets.yaml\n` +
|
`Config: ~/.ccs/config.yaml\n` +
|
||||||
`Base URL: ${baseUrl}\n` +
|
`Settings: ${settingsFile}\n` +
|
||||||
`Model: ${model}`,
|
`Base URL: ${baseUrl}\n` +
|
||||||
'API Profile Created (Unified Config)'
|
`Model: ${model}`;
|
||||||
)
|
|
||||||
);
|
if (hasCustomMapping) {
|
||||||
|
infoMsg +=
|
||||||
|
`\n\nModel Mapping:\n` +
|
||||||
|
` Opus: ${opusModel}\n` +
|
||||||
|
` Sonnet: ${sonnetModel}\n` +
|
||||||
|
` Haiku: ${haikuModel}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(infoBox(infoMsg, 'API Profile Created'));
|
||||||
} else {
|
} else {
|
||||||
// Use legacy JSON format
|
// Use legacy JSON format
|
||||||
const settingsPath = createSettingsFile(name, baseUrl, apiKey, model);
|
const settingsPath = createSettingsFile(name, baseUrl, apiKey, models);
|
||||||
updateConfig(name, settingsPath);
|
updateConfig(name, settingsPath);
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log(
|
|
||||||
infoBox(
|
let infoMsg =
|
||||||
`API: ${name}\n` +
|
`API: ${name}\n` +
|
||||||
`Settings: ~/.ccs/${name}.settings.json\n` +
|
`Settings: ${settingsFile}\n` +
|
||||||
`Base URL: ${baseUrl}\n` +
|
`Base URL: ${baseUrl}\n` +
|
||||||
`Model: ${model}`,
|
`Model: ${model}`;
|
||||||
'API Profile Created'
|
|
||||||
)
|
if (hasCustomMapping) {
|
||||||
);
|
infoMsg +=
|
||||||
|
`\n\nModel Mapping:\n` +
|
||||||
|
` Opus: ${opusModel}\n` +
|
||||||
|
` Sonnet: ${sonnetModel}\n` +
|
||||||
|
` Haiku: ${haikuModel}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(infoBox(infoMsg, 'API Profile Created'));
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log(header('Usage'));
|
console.log(header('Usage'));
|
||||||
console.log(` ${color(`ccs ${name} "your prompt"`, 'command')}`);
|
console.log(` ${color(`ccs ${name} "your prompt"`, 'command')}`);
|
||||||
console.log('');
|
console.log('');
|
||||||
|
console.log(header('Edit Settings'));
|
||||||
|
console.log(` ${dim('To modify env vars later:')}`);
|
||||||
|
console.log(` ${color(`nano ${settingsFile.replace('~', '$HOME')}`, 'command')}`);
|
||||||
|
console.log('');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(fail(`Failed to create API profile: ${(error as Error).message}`));
|
console.log(fail(`Failed to create API profile: ${(error as Error).message}`));
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
Reference in New Issue
Block a user