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 <handle>` - 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 <cpfiffer@users.noreply.github.com>
This commit is contained in:
letta-code
2026-02-06 20:52:26 +00:00
parent f413df8df7
commit 7e82374865
12 changed files with 226 additions and 29 deletions

View File

@@ -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 <handle>`
- `*_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 <handle>`. | - |
### 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

View File

@@ -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 <handle>` 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 <handle>` 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` |

View File

@@ -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 <handle>`.
# BYOK Providers (optional, cloud mode only)
# These will be synced to Letta Cloud on startup

View File

@@ -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 <handle> Set model by handle (e.g., anthropic/claude-sonnet-4-5-20250929)
channels Manage channels (interactive menu)
channels list Show channel status
channels add <ch> 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 <command>\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;

167
src/commands/model.ts Normal file
View File

@@ -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 <handle> - 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<void> {
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<void> {
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<void> {
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<void> {
switch (subCommand) {
case 'show':
await modelShow();
break;
case 'set':
if (!arg) {
console.error('Usage: lettabot model set <handle>');
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 <handle>]');
process.exit(1);
}
}

View File

@@ -116,9 +116,8 @@ export function configToEnv(config: LettaBotConfig): Record<string, string> {
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) {

View File

@@ -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: {},
};

View File

@@ -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),
});

View File

@@ -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[];

View File

@@ -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,

View File

@@ -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<void>
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<v
agentChoice: hasExistingConfig ? 'env' : 'skip',
agentName: existingConfig.agent.name,
agentId: existingConfig.agent.id,
model: existingConfig.agent.model,
providers: existingConfig.providers?.map(p => ({ id: p.id, name: p.name, apiKey: p.apiKey })),
};
@@ -1307,7 +1305,6 @@ export async function onboard(options?: { nonInteractive?: boolean }): Promise<v
// Apply config to env
if (config.agentName) env.AGENT_NAME = config.agentName;
if (config.model) env.MODEL = config.model;
if (config.telegram.enabled && config.telegram.token) {
env.TELEGRAM_BOT_TOKEN = config.telegram.token;
@@ -1413,7 +1410,7 @@ export async function onboard(options?: { nonInteractive?: boolean }): Promise<v
// Show summary
const summary = [
`Agent: ${config.agentId ? `${config.agentName} (${config.agentId.slice(0, 20)}...)` : config.agentName || '(will create on first message)'}`,
`Model: ${config.model || 'default'}`,
...(config.model ? [`Model: ${config.model} (for initial agent creation only)`] : []),
'',
'Channels:',
config.telegram.enabled ? ` ✓ Telegram (${formatAccess(config.telegram.dmPolicy, config.telegram.allowedUsers)})` : ' ✗ Telegram',
@@ -1442,7 +1439,7 @@ export async function onboard(options?: { nonInteractive?: boolean }): Promise<v
},
agent: {
name: config.agentName || 'LettaBot',
model: config.model || 'zai/glm-4.7',
// model is configured on the Letta agent server-side, not saved to config
...(config.agentId ? { id: config.agentId } : {}),
},
channels: {

View File

@@ -101,6 +101,34 @@ export async function agentExists(agentId: string): Promise<boolean> {
}
}
/**
* Get an agent's current model handle
*/
export async function getAgentModel(agentId: string): Promise<string | null> {
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<boolean> {
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
*/