diff --git a/src/commands/model-commands.ts b/src/commands/model-commands.ts index 6eef673..e8cf202 100644 --- a/src/commands/model-commands.ts +++ b/src/commands/model-commands.ts @@ -14,6 +14,7 @@ import { loadModelMap, validateModelMap, initModelMapFile, + addModelToMap, getExampleModelMap, getGlobalConfigDir, type ModelMapConfig, @@ -266,28 +267,32 @@ export const modelMapCommand: Command = { name: 'modelmap', aliases: ['mm', 'models-map'], description: 'Show and manage model map configuration (codi-models.yaml)', - usage: '/modelmap [init [--global]|show|example]', + usage: '/modelmap [add|init|show|example] [--global]', taskType: 'fast', execute: async (args: string, context: CommandContext): Promise => { - const trimmed = args.trim().toLowerCase(); + const trimmed = args.trim(); + const trimmedLower = trimmed.toLowerCase(); // Handle help flag locally without API call - if (trimmed === '-h' || trimmed === '--help') { - console.log('\nUsage: /modelmap [init [--global]|show|example]'); + if (trimmedLower === '-h' || trimmedLower === '--help') { + console.log('\nUsage: /modelmap [action] [options]'); console.log('\nShow and manage model map configuration.'); console.log('\nConfig locations:'); console.log(` Global: ${getGlobalConfigDir()}/models.yaml`); console.log(' Project: ./codi-models.yaml'); console.log('\nActions:'); - console.log(' show Show current model map configuration (default)'); - console.log(' init Create a new codi-models.yaml in current directory'); - console.log(' init --global Create a new models.yaml in ~/.codi/'); - console.log(' example Show example configuration'); + console.log(' show Show current model map configuration (default)'); + console.log(' add [description] Add a model alias'); + console.log(' init Create a new codi-models.yaml in current directory'); + console.log(' example Show example configuration'); + console.log('\nFlags:'); + console.log(' --global Apply to global config (~/.codi/models.yaml)'); console.log('\nExamples:'); - console.log(' /modelmap Show current configuration'); - console.log(' /modelmap init Create project codi-models.yaml'); - console.log(' /modelmap init --global Create global ~/.codi/models.yaml'); - console.log(' /modelmap example Show example YAML'); + console.log(' /modelmap Show current configuration'); + console.log(' /modelmap add coder ollama-cloud qwen3-coder:480b-cloud'); + console.log(' /modelmap add --global fast anthropic claude-3-5-haiku-latest'); + console.log(' /modelmap init Create project codi-models.yaml'); + console.log(' /modelmap init --global Create global ~/.codi/models.yaml'); console.log('\nGlobal models can be used in any project with /switch '); console.log(); return null; @@ -295,10 +300,37 @@ export const modelMapCommand: Command = { // Parse action and flags const parts = trimmed.split(/\s+/).filter(p => p); - const action = parts[0] || 'show'; const isGlobal = parts.includes('--global'); + // Remove --global from parts for easier parsing + const filteredParts = parts.filter(p => p.toLowerCase() !== '--global'); + const action = (filteredParts[0] || 'show').toLowerCase(); switch (action) { + case 'add': { + // Parse: add [description...] + const name = filteredParts[1]; + const provider = filteredParts[2]; + const model = filteredParts[3]; + const description = filteredParts.slice(4).join(' ') || undefined; + + if (!name || !provider || !model) { + return '__MODELMAP_ERROR__|Usage: /modelmap add [description]'; + } + + // Validate provider + const validProviders = ['anthropic', 'openai', 'ollama', 'ollama-cloud', 'runpod']; + if (!validProviders.includes(provider.toLowerCase())) { + return `__MODELMAP_ERROR__|Invalid provider "${provider}". Valid: ${validProviders.join(', ')}`; + } + + const result = addModelToMap(name, provider.toLowerCase(), model, description, isGlobal); + if (result.success) { + const scope = isGlobal ? 'global' : 'project'; + return `__MODELMAP_ADD__|${name}|${provider}|${model}|${result.path}|${scope}`; + } + return `__MODELMAP_ERROR__|${result.error}`; + } + case 'init': { const result = initModelMapFile(process.cwd(), isGlobal); if (result.success) { diff --git a/src/index.ts b/src/index.ts index 0fcd192..e16c256 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1597,6 +1597,17 @@ function handleModelMapOutput(output: string): void { return; } + if (firstLine.startsWith('__MODELMAP_ADD__|')) { + const parts = firstLine.slice('__MODELMAP_ADD__|'.length).split('|'); + const [name, provider, model, filePath, scope] = parts; + console.log(chalk.green(`\nAdded model "${name}" to ${scope} config`)); + console.log(chalk.dim(` Provider: ${provider}`)); + console.log(chalk.dim(` Model: ${model}`)); + console.log(chalk.dim(` File: ${filePath}`)); + console.log(chalk.cyan(`\nUse with: /switch ${name}`)); + return; + } + if (firstLine.startsWith('__MODELMAP_INIT__|')) { const parts = firstLine.slice('__MODELMAP_INIT__|'.length).split('|'); const filePath = parts[0]; diff --git a/src/model-map/index.ts b/src/model-map/index.ts index 78dc726..a54ad15 100644 --- a/src/model-map/index.ts +++ b/src/model-map/index.ts @@ -21,6 +21,7 @@ export { watchModelMap, getExampleModelMap, initModelMap as initModelMapFile, + addModelToMap, getGlobalConfigDir, ModelMapValidationError, type ValidationResult, diff --git a/src/model-map/loader.ts b/src/model-map/loader.ts index ad15751..f190036 100644 --- a/src/model-map/loader.ts +++ b/src/model-map/loader.ts @@ -723,3 +723,91 @@ export function initModelMap( export function getGlobalConfigDir(): string { return GLOBAL_CONFIG_DIR; } + +/** + * Add a model to an existing model map file. + * Creates the file if it doesn't exist. + * + * @param name Model alias name + * @param provider Provider type (anthropic, openai, ollama, etc.) + * @param model Model ID + * @param description Optional description + * @param global If true, adds to global config (~/.codi/models.yaml) + * @param cwd Working directory for project config + */ +export function addModelToMap( + name: string, + provider: string, + model: string, + description?: string, + global: boolean = false, + cwd: string = process.cwd() +): { success: boolean; path: string; error?: string } { + const targetDir = global ? GLOBAL_CONFIG_DIR : cwd; + const fileName = global ? GLOBAL_MODEL_MAP_FILE : MODEL_MAP_FILE; + const configPath = path.join(targetDir, fileName); + + // Ensure directory exists for global config + if (global && !fs.existsSync(targetDir)) { + try { + fs.mkdirSync(targetDir, { recursive: true }); + } catch (error) { + return { + success: false, + path: configPath, + error: `Failed to create directory: ${error instanceof Error ? error.message : String(error)}`, + }; + } + } + + // Load existing config or create new one + let config: ModelMapConfig; + if (fs.existsSync(configPath)) { + try { + const content = fs.readFileSync(configPath, 'utf-8'); + config = yaml.load(content) as ModelMapConfig; + if (!config.models) config.models = {}; + } catch (error) { + return { + success: false, + path: configPath, + error: `Failed to parse existing config: ${error instanceof Error ? error.message : String(error)}`, + }; + } + } else { + // Create minimal config + config = { + version: '1', + models: {}, + }; + } + + // Check if model already exists + if (config.models[name]) { + return { + success: false, + path: configPath, + error: `Model "${name}" already exists. Use a different name or edit the file directly.`, + }; + } + + // Add the model + config.models[name] = { + provider, + model, + ...(description && { description }), + }; + + // Write back + try { + const yamlContent = yaml.dump(config, { lineWidth: 100, noRefs: true }); + fs.writeFileSync(configPath, yamlContent); + return { success: true, path: configPath }; + } catch (error) { + return { + success: false, + path: configPath, + error: `Failed to write config: ${error instanceof Error ? error.message : String(error)}`, + }; + } +}