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 # 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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
14
src/index.ts
14
src/index.ts
@@ -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
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 () => {
|
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/,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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/,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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/,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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/);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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/,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user