From 32a3f7c7abae8d503ebad0b22ed92e60d8ee8d48 Mon Sep 17 00:00:00 2001 From: Devansh Jain <31609257+devanshrj@users.noreply.github.com> Date: Thu, 30 Oct 2025 20:23:21 -0700 Subject: [PATCH] fix: Model flag parsing and model args (#42) --- src/agent/create.ts | 12 +++++- src/agent/model.ts | 64 ++++++++++++++++++++++++++++ src/cli/components/ModelSelector.tsx | 2 +- src/headless.ts | 9 +++- src/index.ts | 7 ++- src/model.ts | 36 ---------------- src/models.json | 36 ++++++++-------- 7 files changed, 106 insertions(+), 60 deletions(-) create mode 100644 src/agent/model.ts delete mode 100644 src/model.ts diff --git a/src/agent/create.ts b/src/agent/create.ts index 503b886..f3d44eb 100644 --- a/src/agent/create.ts +++ b/src/agent/create.ts @@ -7,7 +7,6 @@ import type { Block, CreateBlock, } from "@letta-ai/letta-client/resources/agents/agents"; -import { formatAvailableModels, resolveModel } from "../model"; import { loadProjectSettings, updateProjectSettings, @@ -16,12 +15,15 @@ import { loadSettings, updateSettings } from "../settings"; import { getToolNames } from "../tools/manager"; import { getClient } from "./client"; import { getDefaultMemoryBlocks } from "./memory"; +import { formatAvailableModels, resolveModel } from "./model"; +import { updateAgentLLMConfig } from "./modify"; import { SYSTEM_PROMPT } from "./promptAssets"; export async function createAgent( name = "letta-cli-agent", model?: string, embeddingModel = "openai/text-embedding-3-small", + updateArgs?: Record, ) { // Resolve model identifier to handle let modelHandle: string; @@ -168,5 +170,13 @@ export async function createAgent( include_base_tool_rules: false, initial_message_sequence: [], }); + + // Apply updateArgs if provided (e.g., reasoningEffort, contextWindow, etc.) + if (updateArgs && Object.keys(updateArgs).length > 0) { + await updateAgentLLMConfig(agent.id, modelHandle, updateArgs); + // Refresh agent state to get updated config + return await client.agents.retrieve(agent.id); + } + return agent; // { id, ... } } diff --git a/src/agent/model.ts b/src/agent/model.ts new file mode 100644 index 0000000..062b623 --- /dev/null +++ b/src/agent/model.ts @@ -0,0 +1,64 @@ +/** + * 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"); +} + +/** + * Get model info 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 info if found, null otherwise + */ +export function getModelInfo(modelIdentifier: string) { + const byId = models.find((m) => m.id === modelIdentifier); + if (byId) return byId; + + const byHandle = models.find((m) => m.handle === modelIdentifier); + if (byHandle) return byHandle; + + return null; +} + +/** + * Get updateArgs for 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 updateArgs if found, undefined otherwise + */ +export function getModelUpdateArgs( + modelIdentifier?: string, +): Record | undefined { + if (!modelIdentifier) return undefined; + const modelInfo = getModelInfo(modelIdentifier); + return modelInfo?.updateArgs; +} diff --git a/src/cli/components/ModelSelector.tsx b/src/cli/components/ModelSelector.tsx index a8e1abb..23df8ce 100644 --- a/src/cli/components/ModelSelector.tsx +++ b/src/cli/components/ModelSelector.tsx @@ -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 "../../model"; +import { models } from "../../agent/model"; import { colors } from "./colors"; interface ModelSelectorProps { diff --git a/src/headless.ts b/src/headless.ts index dd5a6f1..b3d77c3 100644 --- a/src/headless.ts +++ b/src/headless.ts @@ -8,6 +8,7 @@ import type { StopReasonType } from "@letta-ai/letta-client/resources/runs/runs" import { getClient } from "./agent/client"; import { createAgent } from "./agent/create"; import { sendMessageStream } from "./agent/message"; +import { getModelUpdateArgs } from "./agent/model"; import { SessionStats } from "./agent/stats"; import { createBuffers, toLines } from "./cli/helpers/accumulator"; import { safeJsonParseOr } from "./cli/helpers/safeJsonParse"; @@ -25,6 +26,8 @@ export async function handleHeadlessCommand(argv: string[], model?: string) { continue: { type: "boolean", short: "c" }, new: { type: "boolean" }, agent: { type: "string", short: "a" }, + model: { type: "string", short: "m" }, + prompt: { type: "boolean", short: "p" }, "output-format": { type: "string" }, }, strict: false, @@ -70,7 +73,8 @@ export async function handleHeadlessCommand(argv: string[], model?: string) { // Priority 2: Check if --new flag was passed (skip all resume logic) if (!agent && forceNew) { - agent = await createAgent(undefined, model); + const updateArgs = getModelUpdateArgs(model); + agent = await createAgent(undefined, model, undefined, updateArgs); } // Priority 3: Try to resume from project settings (.letta/settings.local.json) @@ -101,7 +105,8 @@ export async function handleHeadlessCommand(argv: string[], model?: string) { // Priority 5: Create a new agent if (!agent) { - agent = await createAgent(undefined, model); + const updateArgs = getModelUpdateArgs(model); + agent = await createAgent(undefined, model, undefined, updateArgs); } // Save agent ID to both project and global settings diff --git a/src/index.ts b/src/index.ts index ad28898..2e0be05 100755 --- a/src/index.ts +++ b/src/index.ts @@ -218,6 +218,7 @@ async function main() { setLoadingState("initializing"); const { createAgent } = await import("./agent/create"); + const { getModelUpdateArgs } = await import("./agent/model"); const { updateSettings, loadProjectSettings, updateProjectSettings } = await import("./settings"); @@ -238,7 +239,8 @@ 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(undefined, model); + const updateArgs = getModelUpdateArgs(model); + agent = await createAgent(undefined, model, undefined, updateArgs); } // Priority 3: Try to resume from project settings (.letta/settings.local.json) @@ -270,7 +272,8 @@ async function main() { // Priority 5: Create a new agent if (!agent) { - agent = await createAgent(undefined, model); + const updateArgs = getModelUpdateArgs(model); + agent = await createAgent(undefined, model, undefined, updateArgs); } // Save agent ID to both project and global settings diff --git a/src/model.ts b/src/model.ts deleted file mode 100644 index 7248199..0000000 --- a/src/model.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * 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"); -} diff --git a/src/models.json b/src/models.json index 1b52128..fe1963e 100644 --- a/src/models.json +++ b/src/models.json @@ -5,21 +5,21 @@ "label": "Claude Sonnet 4.5 (default)", "description": "The recommended default model (currently Sonnet 4.5)", "isDefault": true, - "updateArgs": { "contextWindow": 180000 } + "updateArgs": { "context_window": 180000 } }, { "id": "opus", "handle": "anthropic/claude-opus-4-1-20250805", "label": "Claude Opus 4.1", "description": "Anthropic's smartest (and slowest) model", - "updateArgs": { "contextWindow": 180000 } + "updateArgs": { "context_window": 180000 } }, { "id": "haiku", "handle": "anthropic/claude-haiku-4-5-20251001", "label": "Claude Haiku 4.5", "description": "Anthropic's fastest model", - "updateArgs": { "contextWindow": 180000 } + "updateArgs": { "context_window": 180000 } }, { "id": "gpt-5-codex", @@ -27,9 +27,9 @@ "label": "GPT-5-Codex", "description": "A variant of GPT-5 optimized for agentic coding", "updateArgs": { - "reasoningEffort": "medium", + "reasoning_effort": "medium", "verbosity": "medium", - "contextWindow": 272000 + "context_window": 272000 } }, { @@ -38,7 +38,7 @@ "label": "GLM-4.6", "description": "The best open weights coding model", "updateArgs": { - "contextWindow": 200000 + "context_window": 200000 } }, { @@ -47,9 +47,9 @@ "label": "GPT-5 (minimal)", "description": "OpenAI's latest model (limited reasoning, fastest GPT-5 option)", "updateArgs": { - "reasoningEffort": "minimal", + "reasoning_effort": "minimal", "verbosity": "medium", - "contextWindow": 272000 + "context_window": 272000 } }, { @@ -58,9 +58,9 @@ "label": "GPT-5 (low)", "description": "OpenAI's latest model (some reasoning enabled)", "updateArgs": { - "reasoningEffort": "low", + "reasoning_effort": "low", "verbosity": "medium", - "contextWindow": 272000 + "context_window": 272000 } }, { @@ -69,9 +69,9 @@ "label": "GPT-5 (medium)", "description": "OpenAI's latest model (using their recommended reasoning level)", "updateArgs": { - "reasoningEffort": "medium", + "reasoning_effort": "medium", "verbosity": "medium", - "contextWindow": 272000 + "context_window": 272000 } }, { @@ -80,9 +80,9 @@ "label": "GPT-5 (high)", "description": "OpenAI's latest model (maximum reasoning depth)", "updateArgs": { - "reasoningEffort": "high", + "reasoning_effort": "high", "verbosity": "medium", - "contextWindow": 272000 + "context_window": 272000 } }, { @@ -90,27 +90,27 @@ "handle": "google_ai/gemini-2.5-flash", "label": "Gemini 2.5 Flash", "description": "Google's fastest model", - "updateArgs": { "contextWindow": 180000 } + "updateArgs": { "context_window": 180000 } }, { "id": "gemini-pro", "handle": "google_ai/gemini-2.5-pro", "label": "Gemini 2.5 Pro", "description": "Google's smartest model", - "updateArgs": { "contextWindow": 180000 } + "updateArgs": { "context_window": 180000 } }, { "id": "gpt-4.1", "handle": "openai/gpt-4.1", "label": "GPT-4.1", "description": "OpenAI's most recent non-reasoner model", - "updateArgs": { "contextWindow": 180000 } + "updateArgs": { "context_window": 180000 } }, { "id": "o4-mini", "handle": "openai/o4-mini", "label": "o4-mini", "description": "OpenAI's latest o-series reasoning model", - "updateArgs": { "contextWindow": 180000 } + "updateArgs": { "context_window": 180000 } } ]