feat: add --model flag (#32)

This commit is contained in:
Devansh Jain
2025-10-30 16:07:21 -07:00
committed by GitHub
parent f71d2c9b66
commit 09e4f0b13a
14 changed files with 98 additions and 27 deletions

View File

@@ -83,9 +83,13 @@ Join our [Discord](https://discord.gg/letta) to share feedback on persistence pa
letta # Auto-resume project agent (or create new if first time) letta # Auto-resume project agent (or create new if first time)
letta --new # Force create new agent letta --new # Force create new agent
letta --agent <id> # Use specific agent ID letta --agent <id> # Use specific agent ID
letta --model <model> # Specify model (e.g., claude-sonnet-4.5, gpt-4o)
letta -m <model> # Short form of --model
letta --continue # Resume global last agent (deprecated, use project-based) letta --continue # Resume global last agent (deprecated, use project-based)
``` ```
> **Note:** The `--model` flag is inconsistent when resuming sessions. We recommend using the `/model` command instead to change models in interactive mode.
### Headless Mode ### Headless Mode
```bash ```bash
letta -p "Run bun lint and correct errors" # Auto-resumes project agent letta -p "Run bun lint and correct errors" # Auto-resumes project agent
@@ -93,6 +97,7 @@ letta -p "Pick up where you left off" # Same - auto-resumes by
letta -p "Start fresh" --new # Force new agent letta -p "Start fresh" --new # Force new agent
letta -p "Run all the test" --allowedTools "Bash" # Control tool permissions letta -p "Run all the test" --allowedTools "Bash" # Control tool permissions
letta -p "Just read the code" --disallowedTools "Bash" # Control tool permissions letta -p "Just read the code" --disallowedTools "Bash" # Control tool permissions
letta -p "Explain this code" -m gpt-4o # Use specific model
# Pipe input from stdin # Pipe input from stdin
echo "Explain this code" | letta -p echo "Explain this code" | letta -p

View File

@@ -7,6 +7,7 @@ import type {
Block, Block,
CreateBlock, CreateBlock,
} from "@letta-ai/letta-client/resources/agents/agents"; } from "@letta-ai/letta-client/resources/agents/agents";
import { formatAvailableModels, resolveModel } from "../model";
import { import {
loadProjectSettings, loadProjectSettings,
updateProjectSettings, updateProjectSettings,
@@ -19,9 +20,25 @@ import { SYSTEM_PROMPT } from "./promptAssets";
export async function createAgent( export async function createAgent(
name = "letta-cli-agent", name = "letta-cli-agent",
model = "anthropic/claude-sonnet-4-5-20250929", model?: string,
embeddingModel = "openai/text-embedding-3-small", embeddingModel = "openai/text-embedding-3-small",
) { ) {
// Resolve model identifier to handle
let modelHandle: string;
if (model) {
const resolved = resolveModel(model);
if (!resolved) {
console.error(`Error: Unknown model "${model}"`);
console.error("Available models:");
console.error(formatAvailableModels());
process.exit(1);
}
modelHandle = resolved;
} else {
// Use default model
modelHandle = "anthropic/claude-sonnet-4-5-20250929";
}
const client = await getClient(); const client = await getClient();
// Get loaded tool names (tools are already registered with Letta) // Get loaded tool names (tools are already registered with Letta)
@@ -141,7 +158,7 @@ export async function createAgent(
system: SYSTEM_PROMPT, system: SYSTEM_PROMPT,
name, name,
embedding: embeddingModel, embedding: embeddingModel,
model, model: modelHandle,
context_window_limit: 200_000, context_window_limit: 200_000,
tools: toolNames, tools: toolNames,
block_ids: blockIds, block_ids: blockIds,

View File

@@ -989,8 +989,7 @@ export default function App({
try { try {
// Find the selected model from models.json first (for loading message) // Find the selected model from models.json first (for loading message)
const modelsModule = await import("../models.json"); const { models } = await import("../model");
const models = modelsModule.default;
const selectedModel = models.find((m) => m.id === modelId); const selectedModel = models.find((m) => m.id === modelId);
if (!selectedModel) { if (!selectedModel) {

View File

@@ -1,7 +1,7 @@
// Import useInput from vendored Ink for bracketed paste support // Import useInput from vendored Ink for bracketed paste support
import { Box, Text, useInput } from "ink"; import { Box, Text, useInput } from "ink";
import { useState } from "react"; import { useState } from "react";
import models from "../../models.json"; import { models } from "../../model";
import { colors } from "./colors"; import { colors } from "./colors";
interface ModelSelectorProps { interface ModelSelectorProps {

View File

@@ -15,7 +15,7 @@ import { drainStream } from "./cli/helpers/stream";
import { loadSettings, updateSettings } from "./settings"; import { loadSettings, updateSettings } from "./settings";
import { checkToolPermission, executeTool } from "./tools/manager"; import { checkToolPermission, executeTool } from "./tools/manager";
export async function handleHeadlessCommand(argv: string[]) { export async function handleHeadlessCommand(argv: string[], model?: string) {
const settings = await loadSettings(); const settings = await loadSettings();
// Parse CLI args // Parse CLI args
@@ -70,7 +70,7 @@ export async function handleHeadlessCommand(argv: string[]) {
// Priority 2: Check if --new flag was passed (skip all resume logic) // Priority 2: Check if --new flag was passed (skip all resume logic)
if (!agent && forceNew) { if (!agent && forceNew) {
agent = await createAgent(); agent = await createAgent(undefined, model);
} }
// Priority 3: Try to resume from project settings (.letta/settings.local.json) // Priority 3: Try to resume from project settings (.letta/settings.local.json)
@@ -101,7 +101,7 @@ export async function handleHeadlessCommand(argv: string[]) {
// Priority 5: Create a new agent // Priority 5: Create a new agent
if (!agent) { if (!agent) {
agent = await createAgent(); agent = await createAgent(undefined, model);
} }
// Save agent ID to both project and global settings // Save agent ID to both project and global settings

View File

@@ -28,6 +28,7 @@ OPTIONS
--new Force create new agent (skip auto-resume) --new Force create new agent (skip auto-resume)
-c, --continue Resume previous session (uses global lastAgent, deprecated) -c, --continue Resume previous session (uses global lastAgent, deprecated)
-a, --agent <id> Use a specific agent ID -a, --agent <id> Use a specific agent ID
-m, --model <id> Model ID or handle (e.g., "opus" or "anthropic/claude-opus-4-1-20250805")
-p, --prompt Headless prompt mode -p, --prompt Headless prompt mode
--output-format <fmt> Output format for headless mode (text, json, stream-json) --output-format <fmt> Output format for headless mode (text, json, stream-json)
Default: text Default: text
@@ -65,6 +66,7 @@ async function main() {
continue: { type: "boolean", short: "c" }, continue: { type: "boolean", short: "c" },
new: { type: "boolean" }, new: { type: "boolean" },
agent: { type: "string", short: "a" }, agent: { type: "string", short: "a" },
model: { type: "string", short: "m" },
prompt: { type: "boolean", short: "p" }, prompt: { type: "boolean", short: "p" },
run: { type: "boolean" }, run: { type: "boolean" },
tools: { type: "string" }, tools: { type: "string" },
@@ -109,6 +111,7 @@ async function main() {
const shouldContinue = (values.continue as boolean | undefined) ?? false; const shouldContinue = (values.continue as boolean | undefined) ?? false;
const forceNew = (values.new as boolean | undefined) ?? false; const forceNew = (values.new as boolean | undefined) ?? false;
const specifiedAgentId = (values.agent as string | undefined) ?? null; const specifiedAgentId = (values.agent as string | undefined) ?? null;
const specifiedModel = (values.model as string | undefined) ?? undefined;
const isHeadless = values.prompt || values.run || !process.stdin.isTTY; const isHeadless = values.prompt || values.run || !process.stdin.isTTY;
// Validate API key early before any UI rendering // Validate API key early before any UI rendering
@@ -174,7 +177,7 @@ async function main() {
await upsertToolsToServer(client); await upsertToolsToServer(client);
const { handleHeadlessCommand } = await import("./headless"); const { handleHeadlessCommand } = await import("./headless");
await handleHeadlessCommand(process.argv); await handleHeadlessCommand(process.argv, specifiedModel);
return; return;
} }
@@ -189,10 +192,12 @@ async function main() {
continueSession, continueSession,
forceNew, forceNew,
agentIdArg, agentIdArg,
model,
}: { }: {
continueSession: boolean; continueSession: boolean;
forceNew: boolean; forceNew: boolean;
agentIdArg: string | null; agentIdArg: string | null;
model?: string;
}) { }) {
const [loadingState, setLoadingState] = useState< const [loadingState, setLoadingState] = useState<
"assembling" | "upserting" | "initializing" | "checking" | "ready" "assembling" | "upserting" | "initializing" | "checking" | "ready"
@@ -233,7 +238,7 @@ async function main() {
// Priority 2: Check if --new flag was passed (skip all resume logic) // Priority 2: Check if --new flag was passed (skip all resume logic)
if (!agent && forceNew) { if (!agent && forceNew) {
// Create new agent, don't check any lastAgent fields // Create new agent, don't check any lastAgent fields
agent = await createAgent(); agent = await createAgent(undefined, model);
} }
// Priority 3: Try to resume from project settings (.letta/settings.local.json) // Priority 3: Try to resume from project settings (.letta/settings.local.json)
@@ -265,7 +270,7 @@ async function main() {
// Priority 5: Create a new agent // Priority 5: Create a new agent
if (!agent) { if (!agent) {
agent = await createAgent(); agent = await createAgent(undefined, model);
} }
// Save agent ID to both project and global settings // Save agent ID to both project and global settings
@@ -294,7 +299,7 @@ async function main() {
} }
init(); init();
}, [continueSession, forceNew, agentIdArg]); }, [continueSession, forceNew, agentIdArg, model]);
if (!agentId) { if (!agentId) {
return React.createElement(App, { return React.createElement(App, {
@@ -323,6 +328,7 @@ async function main() {
continueSession: shouldContinue, continueSession: shouldContinue,
forceNew: forceNew, forceNew: forceNew,
agentIdArg: specifiedAgentId, agentIdArg: specifiedAgentId,
model: specifiedModel,
}), }),
{ {
exitOnCtrlC: false, // We handle CTRL-C manually with double-press guard exitOnCtrlC: false, // We handle CTRL-C manually with double-press guard

36
src/model.ts Normal file
View File

@@ -0,0 +1,36 @@
/**
* Model resolution and handling utilities
*/
import modelsData from "./models.json";
export const models = modelsData;
/**
* Resolve a model by ID or handle
* @param modelIdentifier - Can be either a model ID (e.g., "opus") or a full handle (e.g., "anthropic/claude-opus-4-1-20250805")
* @returns The model handle if found, null otherwise
*/
export function resolveModel(modelIdentifier: string): string | null {
const byId = models.find((m) => m.id === modelIdentifier);
if (byId) return byId.handle;
const byHandle = models.find((m) => m.handle === modelIdentifier);
if (byHandle) return byHandle.handle;
return null;
}
/**
* Get the default model handle
*/
export function getDefaultModel(): string {
const defaultModel = models.find((m) => m.isDefault);
return defaultModel?.handle || models[0].handle;
}
/**
* Format available models for error messages
*/
export function formatAvailableModels(): string {
return models.map((m) => ` ${m.id.padEnd(20)} ${m.handle}`).join("\n");
}

View File

@@ -80,7 +80,7 @@ describe("Bash tool", () => {
}); });
test("throws error when command is missing", async () => { test("throws error when command is missing", async () => {
await expect(bash({} as any)).rejects.toThrow( await expect(bash({} as Parameters<typeof bash>[0])).rejects.toThrow(
/missing required parameter.*command/, /missing required parameter.*command/,
); );
}); });

View File

@@ -71,7 +71,7 @@ describe("Edit tool", () => {
edit({ edit({
old_string: "foo", old_string: "foo",
new_string: "bar", new_string: "bar",
} as any), } as Parameters<typeof edit>[0]),
).rejects.toThrow(/missing required parameter.*file_path/); ).rejects.toThrow(/missing required parameter.*file_path/);
}); });
@@ -83,7 +83,7 @@ describe("Edit tool", () => {
edit({ edit({
file_path: file, file_path: file,
new_string: "bar", new_string: "bar",
} as any), } as Parameters<typeof edit>[0]),
).rejects.toThrow(/missing required parameter.*old_string/); ).rejects.toThrow(/missing required parameter.*old_string/);
}); });
@@ -95,7 +95,7 @@ describe("Edit tool", () => {
edit({ edit({
file_path: file, file_path: file,
old_string: "foo", old_string: "foo",
} as any), } as Parameters<typeof edit>[0]),
).rejects.toThrow(/missing required parameter.*new_string/); ).rejects.toThrow(/missing required parameter.*new_string/);
}); });
@@ -108,7 +108,7 @@ describe("Edit tool", () => {
file_path: file, file_path: file,
old_string: "World", old_string: "World",
new_str: "Bun", new_str: "Bun",
} as any), } as Parameters<typeof edit>[0]),
).rejects.toThrow(/missing required parameter.*new_string/); ).rejects.toThrow(/missing required parameter.*new_string/);
}); });
}); });

View File

@@ -58,7 +58,7 @@ describe("Grep tool", () => {
}); });
test("throws error when pattern is missing", async () => { test("throws error when pattern is missing", async () => {
await expect(grep({} as any)).rejects.toThrow( await expect(grep({} as Parameters<typeof grep>[0])).rejects.toThrow(
/missing required parameter.*pattern/, /missing required parameter.*pattern/,
); );
}); });

View File

@@ -47,7 +47,7 @@ describe("LS tool", () => {
}); });
test("throws error when path is missing", async () => { test("throws error when path is missing", async () => {
await expect(ls({} as any)).rejects.toThrow( await expect(ls({} as Parameters<typeof ls>[0])).rejects.toThrow(
/missing required parameter.*path/, /missing required parameter.*path/,
); );
}); });

View File

@@ -45,7 +45,7 @@ describe("MultiEdit tool", () => {
await expect( await expect(
multi_edit({ multi_edit({
edits: [{ old_string: "foo", new_string: "bar" }], edits: [{ old_string: "foo", new_string: "bar" }],
} as any), } as Parameters<typeof multi_edit>[0]),
).rejects.toThrow(/missing required parameter.*file_path/); ).rejects.toThrow(/missing required parameter.*file_path/);
}); });
@@ -56,7 +56,7 @@ describe("MultiEdit tool", () => {
await expect( await expect(
multi_edit({ multi_edit({
file_path: file, file_path: file,
} as any), } as Parameters<typeof multi_edit>[0]),
).rejects.toThrow(/missing required parameter.*edits/); ).rejects.toThrow(/missing required parameter.*edits/);
}); });
@@ -67,7 +67,9 @@ describe("MultiEdit tool", () => {
await expect( await expect(
multi_edit({ multi_edit({
file_path: file, file_path: file,
edits: [{ new_string: "bar" } as any], edits: [
{ new_string: "bar" } as Parameters<typeof multi_edit>[0]["edits"][0],
],
}), }),
).rejects.toThrow(/missing required parameter.*old_string/); ).rejects.toThrow(/missing required parameter.*old_string/);
}); });
@@ -79,7 +81,9 @@ describe("MultiEdit tool", () => {
await expect( await expect(
multi_edit({ multi_edit({
file_path: file, file_path: file,
edits: [{ old_string: "foo" } as any], edits: [
{ old_string: "foo" } as Parameters<typeof multi_edit>[0]["edits"][0],
],
}), }),
).rejects.toThrow(/missing required parameter.*new_string/); ).rejects.toThrow(/missing required parameter.*new_string/);
}); });
@@ -91,7 +95,11 @@ describe("MultiEdit tool", () => {
await expect( await expect(
multi_edit({ multi_edit({
file_path: file, file_path: file,
edits: [{ old_string: "foo", new_str: "baz" } as any], edits: [
{ old_string: "foo", new_str: "baz" } as Parameters<
typeof multi_edit
>[0]["edits"][0],
],
}), }),
).rejects.toThrow(/missing required parameter.*new_string/); ).rejects.toThrow(/missing required parameter.*new_string/);
}); });

View File

@@ -104,7 +104,7 @@ export default box;
}); });
test("throws error when file_path is missing", async () => { test("throws error when file_path is missing", async () => {
await expect(read({} as any)).rejects.toThrow( await expect(read({} as Parameters<typeof read>[0])).rejects.toThrow(
/missing required parameter.*file_path/, /missing required parameter.*file_path/,
); );
}); });

View File

@@ -53,7 +53,7 @@ describe("Write tool", () => {
await expect( await expect(
write({ write({
content: "Hello", content: "Hello",
} as any), } as Parameters<typeof write>[0]),
).rejects.toThrow(/missing required parameter.*file_path/); ).rejects.toThrow(/missing required parameter.*file_path/);
}); });
@@ -64,7 +64,7 @@ describe("Write tool", () => {
await expect( await expect(
write({ write({
file_path: filePath, file_path: filePath,
} as any), } as Parameters<typeof write>[0]),
).rejects.toThrow(/missing required parameter.*content/); ).rejects.toThrow(/missing required parameter.*content/);
}); });
}); });