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:
6
SKILL.md
6
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 <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
|
||||
|
||||
@@ -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` |
|
||||
|
||||
@@ -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
|
||||
|
||||
12
src/cli.ts
12
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 <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
167
src/commands/model.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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: {},
|
||||
};
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
@@ -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[];
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user