From 7e823748653e2e8411845d076e508361301b1c6e Mon Sep 17 00:00:00 2001 From: letta-code <248085862+letta-code@users.noreply.github.com> Date: Fri, 6 Feb 2026 20:52:26 +0000 Subject: [PATCH] fix: remove model field from lettabot config, add `lettabot model` command The model field in lettabot.yaml was redundant and misleading -- the model is configured on the Letta agent server-side, and lettabot shouldn't override it. Users were confused seeing a model in their startup log that didn't match the actual model being used. Changes: - Remove `model` from `LettaBotConfig.agent` (made optional for backward compat) - Remove `model` from `BotConfig` interface and `bot.ts` createAgent() calls - Remove `model` from `main.ts` config construction and startup log - Stop saving `model` to lettabot.yaml during onboarding - Stop mapping `agent.model` to `MODEL` env var in config/io.ts - Add `getAgentModel()` and `updateAgentModel()` to letta-api.ts - Add new `src/commands/model.ts` with three subcommands: - `lettabot model` - interactive model selector - `lettabot model show` - show current agent model - `lettabot model set ` - set model directly - Wire up model command in cli.ts with help text - Update docs/configuration.md, lettabot.example.yaml, SKILL.md Model selection during `lettabot onboard` is preserved for new agent creation -- the selected model is passed to createAgent() but is NOT saved to the config file. Fixes #169 Co-authored-by: Cameron --- SKILL.md | 6 +- docs/configuration.md | 9 ++- lettabot.example.yaml | 6 +- src/cli.ts | 12 ++- src/commands/model.ts | 167 +++++++++++++++++++++++++++++++++++++++++ src/config/io.ts | 5 +- src/config/types.ts | 6 +- src/core/bot.ts | 2 - src/core/types.ts | 1 - src/main.ts | 4 +- src/onboard.ts | 9 +-- src/tools/letta-api.ts | 28 +++++++ 12 files changed, 226 insertions(+), 29 deletions(-) create mode 100644 src/commands/model.ts diff --git a/SKILL.md b/SKILL.md index b9d00c6..693fd0c 100644 --- a/SKILL.md +++ b/SKILL.md @@ -35,7 +35,7 @@ lettabot server **Safe defaults used if not set:** - `LETTA_BASE_URL`: `https://api.letta.com` - `LETTA_AGENT_NAME`: `"lettabot"` -- `LETTA_MODEL`: `"claude-sonnet-4"` +- Model: selected during `lettabot onboard` or changed with `lettabot model set ` - `*_DM_POLICY`: `"pairing"` (requires approval before messaging) - `WHATSAPP_SELF_CHAT_MODE`: `true` (only "Message Yourself" chat) - `SIGNAL_SELF_CHAT_MODE`: `true` (only "Note to Self") @@ -70,7 +70,7 @@ The wizard will guide you through: |----------|-------------|---------| | `LETTA_AGENT_ID` | Use existing agent (skip agent creation) | Creates new agent | | `LETTA_AGENT_NAME` | Name for new agent | `"lettabot"` | -| `LETTA_MODEL` | Model for new agent | `"claude-sonnet-4"` | +| ~~`LETTA_MODEL`~~ | *Removed* - model is set on the agent server-side. Use `lettabot model set `. | - | ### Telegram @@ -263,7 +263,7 @@ npm install && npm run build && npm link # 4. Set environment variables export LETTA_API_KEY="letta_..." export TELEGRAM_BOT_TOKEN="123456:ABC-DEF..." -# Defaults will be used for LETTA_BASE_URL, agent name, model, and DM policy +# Defaults will be used for LETTA_BASE_URL, agent name, and DM policy # 5. Run non-interactive setup lettabot onboard --non-interactive diff --git a/docs/configuration.md b/docs/configuration.md index b9ff769..0e0be82 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -27,8 +27,9 @@ server: # Agent settings agent: name: LettaBot - model: claude-sonnet-4 # id: agent-... # Optional: use existing agent + # Note: model is configured on the Letta agent server-side. + # Use `lettabot model set ` to change it. # Channel configurations channels: @@ -108,7 +109,10 @@ docker run -v ~/.letta/.persist/pgdata:/var/lib/postgresql/data \ |--------|------|-------------| | `agent.id` | string | Use existing agent (skips creation) | | `agent.name` | string | Name for new agent | -| `agent.model` | string | Model ID (e.g., `claude-sonnet-4`) | + +> **Note:** The model is configured on the Letta agent server-side, not in the config file. +> Use `lettabot model show` to see the current model and `lettabot model set ` to change it. +> During initial setup (`lettabot onboard`), you'll be prompted to select a model for new agents. ## Channel Configuration @@ -213,7 +217,6 @@ Environment variables override config file values: | `LETTA_BASE_URL` | `server.baseUrl` | | `LETTA_AGENT_ID` | `agent.id` | | `LETTA_AGENT_NAME` | `agent.name` | -| `LETTA_MODEL` | `agent.model` | | `TELEGRAM_BOT_TOKEN` | `channels.telegram.token` | | `TELEGRAM_DM_POLICY` | `channels.telegram.dmPolicy` | | `SLACK_BOT_TOKEN` | `channels.slack.botToken` | diff --git a/lettabot.example.yaml b/lettabot.example.yaml index d35c463..5b9bde1 100644 --- a/lettabot.example.yaml +++ b/lettabot.example.yaml @@ -15,10 +15,8 @@ server: agent: name: LettaBot - # Model to use: - # - Free plan: zai/glm-4.7, minimax/MiniMax-M1-80k - # - BYOK: lc-anthropic/claude-sonnet-4-5-20250929, lc-openai/gpt-5.2 - model: zai/glm-4.7 + # Note: model is configured on the Letta agent server-side. + # Select a model during `lettabot onboard` or change it with `lettabot model set `. # BYOK Providers (optional, cloud mode only) # These will be synced to Letta Cloud on startup diff --git a/src/cli.ts b/src/cli.ts index c258f01..bd0a767 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -51,7 +51,6 @@ async function configure() { ['Server Mode', config.server.mode], ['API Key', config.server.apiKey ? '✓ Set' : '✗ Not set'], ['Agent Name', config.agent.name], - ['Model', config.agent.model], ['Telegram', config.channels.telegram?.enabled ? '✓ Enabled' : '✗ Disabled'], ['Slack', config.channels.slack?.enabled ? '✓ Enabled' : '✗ Disabled'], ['Discord', config.channels.discord?.enabled ? '✓ Enabled' : '✗ Disabled'], @@ -190,6 +189,9 @@ Commands: onboard Setup wizard (integrations, skills, configuration) server Start the bot server configure View and edit configuration + model Interactive model selector + model show Show current agent model + model set Set model by handle (e.g., anthropic/claude-sonnet-4-5-20250929) channels Manage channels (interactive menu) channels list Show channel status channels add Add a channel (telegram, slack, discord, whatsapp, signal) @@ -257,6 +259,12 @@ async function main() { break; } + case 'model': { + const { modelCommand } = await import('./commands/model.js'); + await modelCommand(subCommand, args[2]); + break; + } + case 'channels': case 'channel': { const { channelManagementCommand } = await import('./cli/channel-management.js'); @@ -457,7 +465,7 @@ async function main() { case undefined: console.log('Usage: lettabot \n'); - console.log('Commands: onboard, server, configure, channels, skills, reset-conversation, destroy, help\n'); + console.log('Commands: onboard, server, configure, model, channels, skills, reset-conversation, destroy, help\n'); console.log('Run "lettabot help" for more information.'); break; diff --git a/src/commands/model.ts b/src/commands/model.ts new file mode 100644 index 0000000..02be185 --- /dev/null +++ b/src/commands/model.ts @@ -0,0 +1,167 @@ +/** + * lettabot model - Manage the agent's model + * + * Subcommands: + * lettabot model - Interactive model selector + * lettabot model show - Show current agent model + * lettabot model set - Set model by handle + */ + +import { existsSync, readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { getDataDir } from '../utils/paths.js'; +import { getAgentModel, updateAgentModel, listModels } from '../tools/letta-api.js'; +import { buildModelOptions, handleModelSelection, getBillingTier } from '../utils/model-selection.js'; +import { isLettaCloudUrl } from '../utils/server.js'; + +/** + * Get agent ID from store file + */ +function getAgentId(): string | null { + const storePath = resolve(getDataDir(), 'lettabot-agent.json'); + if (!existsSync(storePath)) return null; + try { + const store = JSON.parse(readFileSync(storePath, 'utf-8')); + return store.agentId || null; + } catch { + return null; + } +} + +/** + * Show the current agent's model + */ +export async function modelShow(): Promise { + const agentId = getAgentId(); + if (!agentId) { + console.error('No agent found. Run `lettabot server` first to create an agent.'); + process.exit(1); + } + + const model = await getAgentModel(agentId); + if (model) { + console.log(`Agent model: ${model}`); + } else { + console.error('Could not retrieve agent model. Check your connection and API key.'); + process.exit(1); + } +} + +/** + * Set the agent's model by handle + */ +export async function modelSet(handle: string): Promise { + const agentId = getAgentId(); + if (!agentId) { + console.error('No agent found. Run `lettabot server` first to create an agent.'); + process.exit(1); + } + + console.log(`Setting model to: ${handle}`); + const success = await updateAgentModel(agentId, handle); + if (success) { + console.log(`Model updated to: ${handle}`); + } else { + console.error('Failed to update model. Check the handle is valid and try again.'); + process.exit(1); + } +} + +/** + * Interactive model selector + */ +export async function modelInteractive(): Promise { + const p = await import('@clack/prompts'); + + const agentId = getAgentId(); + if (!agentId) { + console.error('No agent found. Run `lettabot server` first to create an agent.'); + process.exit(1); + } + + p.intro('Model Management'); + + // Show current model + const currentModel = await getAgentModel(agentId); + if (currentModel) { + p.log.info(`Current model: ${currentModel}`); + } + + // Determine if self-hosted + const baseUrl = process.env.LETTA_BASE_URL; + const isSelfHosted = !!baseUrl && !isLettaCloudUrl(baseUrl); + + // Get billing tier for cloud users + let billingTier: string | null = null; + if (!isSelfHosted) { + const spinner = p.spinner(); + spinner.start('Checking account...'); + const apiKey = process.env.LETTA_API_KEY; + billingTier = await getBillingTier(apiKey, isSelfHosted); + spinner.stop(billingTier === 'free' ? 'Free plan' : `Plan: ${billingTier || 'Pro'}`); + } + + // Build model options + const spinner = p.spinner(); + spinner.start('Fetching available models...'); + const apiKey = process.env.LETTA_API_KEY; + const modelOptions = await buildModelOptions({ billingTier, isSelfHosted, apiKey }); + spinner.stop(`${modelOptions.length} models available`); + + // Show model selector + let selectedModel: string | null = null; + while (!selectedModel) { + const modelChoice = await p.select({ + message: 'Select model', + options: modelOptions, + maxItems: 12, + }); + if (p.isCancel(modelChoice)) { + p.cancel('Cancelled'); + return; + } + + selectedModel = await handleModelSelection(modelChoice, p.text); + // If null (e.g., header selected), loop again + } + + // Update the model + const updateSpinner = p.spinner(); + updateSpinner.start(`Updating model to ${selectedModel}...`); + const success = await updateAgentModel(agentId, selectedModel); + if (success) { + updateSpinner.stop(`Model updated to: ${selectedModel}`); + } else { + updateSpinner.stop('Failed to update model'); + p.log.error('Check the model handle is valid and try again.'); + } + + p.outro('Done'); +} + +/** + * Main model command handler + */ +export async function modelCommand(subCommand?: string, arg?: string): Promise { + switch (subCommand) { + case 'show': + await modelShow(); + break; + case 'set': + if (!arg) { + console.error('Usage: lettabot model set '); + console.error('Example: lettabot model set anthropic/claude-sonnet-4-5-20250929'); + process.exit(1); + } + await modelSet(arg); + break; + case undefined: + case '': + await modelInteractive(); + break; + default: + console.error(`Unknown subcommand: ${subCommand}`); + console.error('Usage: lettabot model [show|set ]'); + process.exit(1); + } +} diff --git a/src/config/io.ts b/src/config/io.ts index 0143f54..96c56c1 100644 --- a/src/config/io.ts +++ b/src/config/io.ts @@ -116,9 +116,8 @@ export function configToEnv(config: LettaBotConfig): Record { if (config.agent.name) { env.AGENT_NAME = config.agent.name; } - if (config.agent.model) { - env.MODEL = config.agent.model; - } + // Note: agent.model is intentionally NOT mapped to env. + // The model is configured on the Letta agent server-side. // Channels if (config.channels.telegram?.token) { diff --git a/src/config/types.ts b/src/config/types.ts index 8d1ca1e..8104840 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -21,7 +21,9 @@ export interface LettaBotConfig { agent: { id?: string; name: string; - model: string; + // model is configured on the Letta agent server-side, not in config + // Kept as optional for backward compat (ignored if present in existing configs) + model?: string; }; // BYOK providers (cloud mode only) @@ -131,7 +133,7 @@ export const DEFAULT_CONFIG: LettaBotConfig = { }, agent: { name: 'LettaBot', - model: 'zai/glm-4.7', // Free model default + // model is configured on the Letta agent server-side (via onboarding or `lettabot model set`) }, channels: {}, }; diff --git a/src/core/bot.ts b/src/core/bot.ts index 92d9903..facf6c5 100644 --- a/src/core/bot.ts +++ b/src/core/bot.ts @@ -332,7 +332,6 @@ export class LettaBot { // Create new agent with default conversation console.log('[Bot] Creating new agent'); const newAgentId = await createAgent({ - model: this.config.model, systemPrompt: SYSTEM_PROMPT, memory: loadMemoryBlocks(this.config.agentName), }); @@ -756,7 +755,6 @@ export class LettaBot { } else { // Create new agent with default conversation const newAgentId = await createAgent({ - model: this.config.model, systemPrompt: SYSTEM_PROMPT, memory: loadMemoryBlocks(this.config.agentName), }); diff --git a/src/core/types.ts b/src/core/types.ts index c0733e8..d25cd97 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -118,7 +118,6 @@ export interface SkillsConfig { export interface BotConfig { // Letta workingDir: string; - model?: string; // e.g., 'anthropic/claude-sonnet-4-5-20250929' agentName?: string; // Name for the agent (set via API after creation) allowedTools: string[]; diff --git a/src/main.ts b/src/main.ts index d34f9cf..70dbd8d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -20,7 +20,7 @@ import { getDataDir, getWorkingDir, hasRailwayVolume } from './utils/paths.js'; const yamlConfig = loadConfig(); const configSource = existsSync(resolveConfigPath()) ? resolveConfigPath() : 'defaults + environment variables'; console.log(`[Config] Loaded from ${configSource}`); -console.log(`[Config] Mode: ${yamlConfig.server.mode}, Agent: ${yamlConfig.agent.name}, Model: ${yamlConfig.agent.model}`); +console.log(`[Config] Mode: ${yamlConfig.server.mode}, Agent: ${yamlConfig.agent.name}`); applyConfigToEnv(yamlConfig); // Sync BYOK providers on startup (async, don't block) @@ -233,7 +233,6 @@ async function pruneAttachmentsDir(baseDir: string, maxAgeDays: number): Promise // Configuration from environment const config = { workingDir: getWorkingDir(), - model: process.env.MODEL, // e.g., 'claude-sonnet-4-20250514' allowedTools: (process.env.ALLOWED_TOOLS || 'Bash,Read,Edit,Write,Glob,Grep,Task,web_search,conversation_search').split(','), attachmentsMaxBytes: resolveAttachmentsMaxBytes(), attachmentsMaxAgeDays: resolveAttachmentsMaxAgeDays(), @@ -326,7 +325,6 @@ async function main() { // Create bot with skills config (skills installed to agent-scoped location after agent creation) const bot = new LettaBot({ workingDir: config.workingDir, - model: config.model, agentName: process.env.AGENT_NAME || 'LettaBot', allowedTools: config.allowedTools, maxToolCalls: process.env.MAX_TOOL_CALLS ? Number(process.env.MAX_TOOL_CALLS) : undefined, diff --git a/src/onboard.ts b/src/onboard.ts index 8bafcd8..3757ac9 100644 --- a/src/onboard.ts +++ b/src/onboard.ts @@ -21,7 +21,6 @@ function readConfigFromEnv(existingConfig: any): any { apiKey: process.env.LETTA_API_KEY || existingConfig.server?.apiKey, agentId: process.env.LETTA_AGENT_ID || existingConfig.agent?.id, agentName: process.env.LETTA_AGENT_NAME || existingConfig.agent?.name || 'lettabot', - model: process.env.LETTA_MODEL || existingConfig.agent?.model || 'claude-sonnet-4', telegram: { enabled: !!process.env.TELEGRAM_BOT_TOKEN, @@ -74,7 +73,7 @@ async function saveConfigFromEnv(config: any, configPath: string): Promise agent: { id: config.agentId, name: config.agentName, - model: config.model, + // model is configured on the Letta agent server-side, not saved to config }, channels: { telegram: config.telegram.enabled ? { @@ -1276,7 +1275,6 @@ export async function onboard(options?: { nonInteractive?: boolean }): Promise ({ id: p.id, name: p.name, apiKey: p.apiKey })), }; @@ -1307,7 +1305,6 @@ export async function onboard(options?: { nonInteractive?: boolean }): Promise { } } +/** + * Get an agent's current model handle + */ +export async function getAgentModel(agentId: string): Promise { + try { + const client = getClient(); + const agent = await client.agents.retrieve(agentId); + return agent.model ?? null; + } catch (e) { + console.error('[Letta API] Failed to get agent model:', e); + return null; + } +} + +/** + * Update an agent's model + */ +export async function updateAgentModel(agentId: string, model: string): Promise { + try { + const client = getClient(); + await client.agents.update(agentId, { model }); + return true; + } catch (e) { + console.error('[Letta API] Failed to update agent model:', e); + return false; + } +} + /** * Update an agent's name */