diff --git a/.gitignore b/.gitignore index 9b675031..a85ef785 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,4 @@ local/* MCP directories /.serena/ +/.agents/skills/bmad* diff --git a/src/cli/commands/models.ts b/src/cli/commands/models.ts new file mode 100644 index 00000000..b1a7f826 --- /dev/null +++ b/src/cli/commands/models.ts @@ -0,0 +1,89 @@ +import { Command } from 'commander'; +import chalk from 'chalk'; +import { ConfigLoader } from '../../utils/config.js'; +import { ProviderRegistry } from '../../providers/core/registry.js'; +import { logger } from '../../utils/logger.js'; +import type { ModelInfo } from '../../providers/core/types.js'; + +const UNSUPPORTED_PROVIDERS = new Set(['openai', 'bearer-auth', 'openai-compatible']); + +function formatTable(models: ModelInfo[]): void { + const ID_WIDTH = 40; + const NAME_WIDTH = 35; + const DESC_WIDTH = 60; + + const header = + chalk.bold(padEnd('ID', ID_WIDTH)) + + chalk.bold(padEnd('NAME', NAME_WIDTH)) + + chalk.bold('DESCRIPTION'); + + console.log(header); + console.log(chalk.dim('─'.repeat(ID_WIDTH + NAME_WIDTH + DESC_WIDTH))); + + for (const model of models) { + const id = padEnd(model.id, ID_WIDTH); + const name = padEnd(model.name || model.id, NAME_WIDTH); + const desc = truncate(model.description ?? '', DESC_WIDTH); + console.log(chalk.cyan(id) + chalk.white(name) + chalk.dim(desc)); + } +} + +function padEnd(str: string, width: number): string { + return str.length >= width ? str.slice(0, width - 1) + ' ' : str.padEnd(width); +} + +function truncate(str: string, maxLen: number): string { + return str.length > maxLen ? str.slice(0, maxLen - 3) + '...' : str; +} + +export function createModelsCommand(): Command { + const command = new Command('models'); + command.description('Manage and list AI models'); + + const listCommand = new Command('list'); + listCommand + .description('List all models available for the current provider configuration') + .action(async () => { + try { + const config = await ConfigLoader.load(process.cwd()); + const provider = config.provider; + + if (!provider) { + console.error(chalk.red('No provider configured. Run ' + chalk.cyan('codemie setup') + ' to get started.')); + process.exit(1); + } + + if (UNSUPPORTED_PROVIDERS.has(provider)) { + console.error(chalk.red(`Model listing is not supported for provider '${provider}'.`)); + process.exit(1); + } + + const proxy = ProviderRegistry.getModelProxy(provider); + + if (!proxy) { + console.error(chalk.red(`Model listing is not supported for provider '${provider}'.`)); + process.exit(1); + } + + logger.debug(`Fetching models for provider: ${provider}`); + + const models = await proxy.fetchModels(config); + + if (models.length === 0) { + console.log(chalk.yellow(`No models found for provider '${provider}'.`)); + return; + } + + console.log(chalk.bold(`\nProvider: ${chalk.cyan(provider)}\n`)); + formatTable(models); + console.log(chalk.dim(`\n${models.length} model(s) available.\n`)); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error); + console.error(chalk.red(`Failed to fetch models: ${message}`)); + process.exit(1); + } + }); + + command.addCommand(listCommand); + return command; +} diff --git a/src/cli/index.ts b/src/cli/index.ts index b9f3565f..1a00b655 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -29,6 +29,7 @@ import { createSkillCommand } from './commands/skill.js'; import { createPluginCommand } from './commands/plugin.js'; import { createOpencodeMetricsCommand } from './commands/opencode-metrics.js'; import { createTestMetricsCommand } from './commands/test-metrics.js'; +import { createModelsCommand } from './commands/models.js'; import { createAssistantsCommand } from './commands/assistants/index.js'; import { FirstTimeExperience } from './first-time.js'; import { getDirname } from '../utils/paths.js'; @@ -72,6 +73,7 @@ program.addCommand(createSkillCommand()); program.addCommand(createPluginCommand()); program.addCommand(createOpencodeMetricsCommand()); program.addCommand(createTestMetricsCommand()); +program.addCommand(createModelsCommand()); // Check for --task option before parsing commands const taskIndex = process.argv.indexOf('--task'); diff --git a/src/providers/plugins/bedrock/bedrock.models.ts b/src/providers/plugins/bedrock/bedrock.models.ts index 646ab963..183702c5 100644 --- a/src/providers/plugins/bedrock/bedrock.models.ts +++ b/src/providers/plugins/bedrock/bedrock.models.ts @@ -36,28 +36,32 @@ export class BedrockModelProxy implements ProviderModelFetcher { /** * Fetch available models from Bedrock */ - async fetchModels(_config: CodeMieConfigOptions): Promise { + async fetchModels(config: CodeMieConfigOptions): Promise { try { const { BedrockClient, ListInferenceProfilesCommand } = await import('@aws-sdk/client-bedrock'); - const clientConfig: any = { - region: this.region - }; + // Prefer runtime config values over constructor defaults + const region = config.awsRegion || this.region; + const awsProfile = config.awsProfile || this.profile; + const accessKeyId = config.apiKey || this.accessKeyId; + const secretAccessKey = config.awsSecretAccessKey || this.secretAccessKey; - if (this.profile) { + const clientConfig: any = { region }; + + if (awsProfile) { // Use AWS profile - fromIni returns a credential provider function // that the SDK will call when needed clientConfig.credentials = fromIni({ - profile: this.profile + profile: awsProfile }); - } else if (this.accessKeyId && this.secretAccessKey) { + } else if (accessKeyId && secretAccessKey) { // Use direct credentials clientConfig.credentials = { - accessKeyId: this.accessKeyId, - secretAccessKey: this.secretAccessKey + accessKeyId, + secretAccessKey }; } else { // Try to use default credentials chain (environment variables, default profile, etc.) diff --git a/src/providers/plugins/litellm/index.ts b/src/providers/plugins/litellm/index.ts index 301e7be3..8b2b256f 100644 --- a/src/providers/plugins/litellm/index.ts +++ b/src/providers/plugins/litellm/index.ts @@ -6,9 +6,13 @@ import { ProviderRegistry } from '../../core/registry.js'; import { LiteLLMSetupSteps } from './litellm.setup-steps.js'; +import { LiteLLMModelProxy } from './litellm.models.js'; export { LiteLLMTemplate } from './litellm.template.js'; export { LiteLLMSetupSteps } from './litellm.setup-steps.js'; // Register setup steps ProviderRegistry.registerSetupSteps('litellm', LiteLLMSetupSteps); + +// Register model proxy (fetchModels uses runtime config, so empty defaults are fine) +ProviderRegistry.registerModelProxy('litellm', new LiteLLMModelProxy('')); diff --git a/src/providers/plugins/litellm/litellm.models.ts b/src/providers/plugins/litellm/litellm.models.ts index 71bebfb4..ef926157 100644 --- a/src/providers/plugins/litellm/litellm.models.ts +++ b/src/providers/plugins/litellm/litellm.models.ts @@ -54,9 +54,13 @@ export class LiteLLMModelProxy extends BaseModelProxy { } /** - * Fetch models for setup wizard + * Fetch models using runtime config values (baseUrl and apiKey). + * Falls back to constructor values when config fields are absent. */ - async fetchModels(_config: CodeMieConfigOptions): Promise { - return this.listModels(); + async fetchModels(config: CodeMieConfigOptions): Promise { + const effectiveBaseUrl = config.baseUrl || this.baseUrl; + const effectiveApiKey = config.apiKey !== undefined ? config.apiKey : this.apiKey; + const proxy = new LiteLLMModelProxy(effectiveBaseUrl, effectiveApiKey); + return proxy.listModels(); } }