feat: add --model flag (#32)
This commit is contained in:
@@ -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 --new # Force create new agent
|
||||
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)
|
||||
```
|
||||
|
||||
> **Note:** The `--model` flag is inconsistent when resuming sessions. We recommend using the `/model` command instead to change models in interactive mode.
|
||||
|
||||
### Headless Mode
|
||||
```bash
|
||||
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 "Run all the test" --allowedTools "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
|
||||
echo "Explain this code" | letta -p
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
Block,
|
||||
CreateBlock,
|
||||
} from "@letta-ai/letta-client/resources/agents/agents";
|
||||
import { formatAvailableModels, resolveModel } from "../model";
|
||||
import {
|
||||
loadProjectSettings,
|
||||
updateProjectSettings,
|
||||
@@ -19,9 +20,25 @@ import { SYSTEM_PROMPT } from "./promptAssets";
|
||||
|
||||
export async function createAgent(
|
||||
name = "letta-cli-agent",
|
||||
model = "anthropic/claude-sonnet-4-5-20250929",
|
||||
model?: string,
|
||||
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();
|
||||
|
||||
// Get loaded tool names (tools are already registered with Letta)
|
||||
@@ -141,7 +158,7 @@ export async function createAgent(
|
||||
system: SYSTEM_PROMPT,
|
||||
name,
|
||||
embedding: embeddingModel,
|
||||
model,
|
||||
model: modelHandle,
|
||||
context_window_limit: 200_000,
|
||||
tools: toolNames,
|
||||
block_ids: blockIds,
|
||||
|
||||
@@ -989,8 +989,7 @@ export default function App({
|
||||
|
||||
try {
|
||||
// Find the selected model from models.json first (for loading message)
|
||||
const modelsModule = await import("../models.json");
|
||||
const models = modelsModule.default;
|
||||
const { models } = await import("../model");
|
||||
const selectedModel = models.find((m) => m.id === modelId);
|
||||
|
||||
if (!selectedModel) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Import useInput from vendored Ink for bracketed paste support
|
||||
import { Box, Text, useInput } from "ink";
|
||||
import { useState } from "react";
|
||||
import models from "../../models.json";
|
||||
import { models } from "../../model";
|
||||
import { colors } from "./colors";
|
||||
|
||||
interface ModelSelectorProps {
|
||||
|
||||
@@ -15,7 +15,7 @@ import { drainStream } from "./cli/helpers/stream";
|
||||
import { loadSettings, updateSettings } from "./settings";
|
||||
import { checkToolPermission, executeTool } from "./tools/manager";
|
||||
|
||||
export async function handleHeadlessCommand(argv: string[]) {
|
||||
export async function handleHeadlessCommand(argv: string[], model?: string) {
|
||||
const settings = await loadSettings();
|
||||
|
||||
// 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)
|
||||
if (!agent && forceNew) {
|
||||
agent = await createAgent();
|
||||
agent = await createAgent(undefined, model);
|
||||
}
|
||||
|
||||
// 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
|
||||
if (!agent) {
|
||||
agent = await createAgent();
|
||||
agent = await createAgent(undefined, model);
|
||||
}
|
||||
|
||||
// Save agent ID to both project and global settings
|
||||
|
||||
14
src/index.ts
14
src/index.ts
@@ -28,6 +28,7 @@ OPTIONS
|
||||
--new Force create new agent (skip auto-resume)
|
||||
-c, --continue Resume previous session (uses global lastAgent, deprecated)
|
||||
-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
|
||||
--output-format <fmt> Output format for headless mode (text, json, stream-json)
|
||||
Default: text
|
||||
@@ -65,6 +66,7 @@ async function main() {
|
||||
continue: { type: "boolean", short: "c" },
|
||||
new: { type: "boolean" },
|
||||
agent: { type: "string", short: "a" },
|
||||
model: { type: "string", short: "m" },
|
||||
prompt: { type: "boolean", short: "p" },
|
||||
run: { type: "boolean" },
|
||||
tools: { type: "string" },
|
||||
@@ -109,6 +111,7 @@ async function main() {
|
||||
const shouldContinue = (values.continue as boolean | undefined) ?? false;
|
||||
const forceNew = (values.new as boolean | undefined) ?? false;
|
||||
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;
|
||||
|
||||
// Validate API key early before any UI rendering
|
||||
@@ -174,7 +177,7 @@ async function main() {
|
||||
await upsertToolsToServer(client);
|
||||
|
||||
const { handleHeadlessCommand } = await import("./headless");
|
||||
await handleHeadlessCommand(process.argv);
|
||||
await handleHeadlessCommand(process.argv, specifiedModel);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -189,10 +192,12 @@ async function main() {
|
||||
continueSession,
|
||||
forceNew,
|
||||
agentIdArg,
|
||||
model,
|
||||
}: {
|
||||
continueSession: boolean;
|
||||
forceNew: boolean;
|
||||
agentIdArg: string | null;
|
||||
model?: string;
|
||||
}) {
|
||||
const [loadingState, setLoadingState] = useState<
|
||||
"assembling" | "upserting" | "initializing" | "checking" | "ready"
|
||||
@@ -233,7 +238,7 @@ async function main() {
|
||||
// Priority 2: Check if --new flag was passed (skip all resume logic)
|
||||
if (!agent && forceNew) {
|
||||
// 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)
|
||||
@@ -265,7 +270,7 @@ async function main() {
|
||||
|
||||
// Priority 5: Create a new agent
|
||||
if (!agent) {
|
||||
agent = await createAgent();
|
||||
agent = await createAgent(undefined, model);
|
||||
}
|
||||
|
||||
// Save agent ID to both project and global settings
|
||||
@@ -294,7 +299,7 @@ async function main() {
|
||||
}
|
||||
|
||||
init();
|
||||
}, [continueSession, forceNew, agentIdArg]);
|
||||
}, [continueSession, forceNew, agentIdArg, model]);
|
||||
|
||||
if (!agentId) {
|
||||
return React.createElement(App, {
|
||||
@@ -323,6 +328,7 @@ async function main() {
|
||||
continueSession: shouldContinue,
|
||||
forceNew: forceNew,
|
||||
agentIdArg: specifiedAgentId,
|
||||
model: specifiedModel,
|
||||
}),
|
||||
{
|
||||
exitOnCtrlC: false, // We handle CTRL-C manually with double-press guard
|
||||
|
||||
36
src/model.ts
Normal file
36
src/model.ts
Normal 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");
|
||||
}
|
||||
@@ -80,7 +80,7 @@ describe("Bash tool", () => {
|
||||
});
|
||||
|
||||
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/,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -71,7 +71,7 @@ describe("Edit tool", () => {
|
||||
edit({
|
||||
old_string: "foo",
|
||||
new_string: "bar",
|
||||
} as any),
|
||||
} as Parameters<typeof edit>[0]),
|
||||
).rejects.toThrow(/missing required parameter.*file_path/);
|
||||
});
|
||||
|
||||
@@ -83,7 +83,7 @@ describe("Edit tool", () => {
|
||||
edit({
|
||||
file_path: file,
|
||||
new_string: "bar",
|
||||
} as any),
|
||||
} as Parameters<typeof edit>[0]),
|
||||
).rejects.toThrow(/missing required parameter.*old_string/);
|
||||
});
|
||||
|
||||
@@ -95,7 +95,7 @@ describe("Edit tool", () => {
|
||||
edit({
|
||||
file_path: file,
|
||||
old_string: "foo",
|
||||
} as any),
|
||||
} as Parameters<typeof edit>[0]),
|
||||
).rejects.toThrow(/missing required parameter.*new_string/);
|
||||
});
|
||||
|
||||
@@ -108,7 +108,7 @@ describe("Edit tool", () => {
|
||||
file_path: file,
|
||||
old_string: "World",
|
||||
new_str: "Bun",
|
||||
} as any),
|
||||
} as Parameters<typeof edit>[0]),
|
||||
).rejects.toThrow(/missing required parameter.*new_string/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -58,7 +58,7 @@ describe("Grep tool", () => {
|
||||
});
|
||||
|
||||
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/,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -47,7 +47,7 @@ describe("LS tool", () => {
|
||||
});
|
||||
|
||||
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/,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -45,7 +45,7 @@ describe("MultiEdit tool", () => {
|
||||
await expect(
|
||||
multi_edit({
|
||||
edits: [{ old_string: "foo", new_string: "bar" }],
|
||||
} as any),
|
||||
} as Parameters<typeof multi_edit>[0]),
|
||||
).rejects.toThrow(/missing required parameter.*file_path/);
|
||||
});
|
||||
|
||||
@@ -56,7 +56,7 @@ describe("MultiEdit tool", () => {
|
||||
await expect(
|
||||
multi_edit({
|
||||
file_path: file,
|
||||
} as any),
|
||||
} as Parameters<typeof multi_edit>[0]),
|
||||
).rejects.toThrow(/missing required parameter.*edits/);
|
||||
});
|
||||
|
||||
@@ -67,7 +67,9 @@ describe("MultiEdit tool", () => {
|
||||
await expect(
|
||||
multi_edit({
|
||||
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/);
|
||||
});
|
||||
@@ -79,7 +81,9 @@ describe("MultiEdit tool", () => {
|
||||
await expect(
|
||||
multi_edit({
|
||||
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/);
|
||||
});
|
||||
@@ -91,7 +95,11 @@ describe("MultiEdit tool", () => {
|
||||
await expect(
|
||||
multi_edit({
|
||||
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/);
|
||||
});
|
||||
|
||||
@@ -104,7 +104,7 @@ export default box;
|
||||
});
|
||||
|
||||
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/,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -53,7 +53,7 @@ describe("Write tool", () => {
|
||||
await expect(
|
||||
write({
|
||||
content: "Hello",
|
||||
} as any),
|
||||
} as Parameters<typeof write>[0]),
|
||||
).rejects.toThrow(/missing required parameter.*file_path/);
|
||||
});
|
||||
|
||||
@@ -64,7 +64,7 @@ describe("Write tool", () => {
|
||||
await expect(
|
||||
write({
|
||||
file_path: filePath,
|
||||
} as any),
|
||||
} as Parameters<typeof write>[0]),
|
||||
).rejects.toThrow(/missing required parameter.*content/);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user