fix: localhost improvements (#667)
Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
@@ -281,9 +281,11 @@ export async function createAgent(
|
||||
blockProvenance.push({ label: blockId, source: "shared" });
|
||||
}
|
||||
|
||||
// Get the model's context window from its configuration
|
||||
// Get the model's context window from its configuration (if known)
|
||||
// For unknown models (e.g., from self-hosted servers), don't set a context window
|
||||
// and let the server use its default
|
||||
const modelUpdateArgs = getModelUpdateArgs(modelHandle);
|
||||
const contextWindow = (modelUpdateArgs?.context_window as number) || 200_000;
|
||||
const contextWindow = modelUpdateArgs?.context_window as number | undefined;
|
||||
|
||||
// Resolve system prompt content:
|
||||
// 1. If systemPromptCustom is provided, use it as-is
|
||||
@@ -319,7 +321,7 @@ export async function createAgent(
|
||||
description: agentDescription,
|
||||
embedding: embeddingModelVal || undefined,
|
||||
model: modelHandle,
|
||||
context_window_limit: contextWindow,
|
||||
...(contextWindow && { context_window_limit: contextWindow }),
|
||||
tools: toolNames,
|
||||
// New blocks created inline with agent (saves ~2s of sequential API calls)
|
||||
memory_blocks:
|
||||
|
||||
@@ -17,6 +17,12 @@ export function resolveModel(modelIdentifier: string): string | null {
|
||||
const byHandle = models.find((m) => m.handle === modelIdentifier);
|
||||
if (byHandle) return byHandle.handle;
|
||||
|
||||
// For self-hosted servers: if it looks like a handle (contains /), pass it through
|
||||
// This allows using models not in models.json (e.g., from server's /v1/models)
|
||||
if (modelIdentifier.includes("/")) {
|
||||
return modelIdentifier;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -9143,6 +9143,14 @@ Plan file path: ${planFilePath}`;
|
||||
filterProvider={modelSelectorOptions.filterProvider}
|
||||
forceRefresh={modelSelectorOptions.forceRefresh}
|
||||
billingTier={billingTier ?? undefined}
|
||||
isSelfHosted={(() => {
|
||||
const settings = settingsManager.getSettings();
|
||||
const baseURL =
|
||||
process.env.LETTA_BASE_URL ||
|
||||
settings.env?.LETTA_BASE_URL ||
|
||||
"https://api.letta.com";
|
||||
return !baseURL.includes("api.letta.com");
|
||||
})()}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -15,13 +15,26 @@ const SOLID_LINE = "─";
|
||||
|
||||
const VISIBLE_ITEMS = 8;
|
||||
|
||||
type ModelCategory = "supported" | "byok" | "byok-all" | "all";
|
||||
type ModelCategory =
|
||||
| "supported"
|
||||
| "byok"
|
||||
| "byok-all"
|
||||
| "all"
|
||||
| "server-recommended"
|
||||
| "server-all";
|
||||
|
||||
// BYOK provider prefixes (ChatGPT OAuth + lc-* providers from /connect)
|
||||
const BYOK_PROVIDER_PREFIXES = ["chatgpt-plus-pro/", "lc-"];
|
||||
|
||||
// Get tab order based on billing tier (free = BYOK first, paid = BYOK last)
|
||||
function getModelCategories(billingTier?: string): ModelCategory[] {
|
||||
// For self-hosted servers, only show server-specific tabs
|
||||
function getModelCategories(
|
||||
billingTier?: string,
|
||||
isSelfHosted?: boolean,
|
||||
): ModelCategory[] {
|
||||
if (isSelfHosted) {
|
||||
return ["server-recommended", "server-all"];
|
||||
}
|
||||
const isFreeTier = billingTier?.toLowerCase() === "free";
|
||||
return isFreeTier
|
||||
? ["byok", "byok-all", "supported", "all"]
|
||||
@@ -49,6 +62,8 @@ interface ModelSelectorProps {
|
||||
forceRefresh?: boolean;
|
||||
/** User's billing tier - affects tab ordering (free = BYOK first) */
|
||||
billingTier?: string;
|
||||
/** Whether connected to a self-hosted server (not api.letta.com) */
|
||||
isSelfHosted?: boolean;
|
||||
}
|
||||
|
||||
export function ModelSelector({
|
||||
@@ -58,15 +73,17 @@ export function ModelSelector({
|
||||
filterProvider,
|
||||
forceRefresh: forceRefreshOnMount,
|
||||
billingTier,
|
||||
isSelfHosted,
|
||||
}: ModelSelectorProps) {
|
||||
const terminalWidth = useTerminalWidth();
|
||||
const solidLine = SOLID_LINE.repeat(Math.max(terminalWidth, 10));
|
||||
const typedModels = models as UiModel[];
|
||||
|
||||
// Tab order depends on billing tier (free = BYOK first)
|
||||
// For self-hosted, only show server-specific tabs
|
||||
const modelCategories = useMemo(
|
||||
() => getModelCategories(billingTier),
|
||||
[billingTier],
|
||||
() => getModelCategories(billingTier, isSelfHosted),
|
||||
[billingTier, isSelfHosted],
|
||||
);
|
||||
const defaultCategory = modelCategories[0] ?? "supported";
|
||||
|
||||
@@ -296,6 +313,40 @@ export function ModelSelector({
|
||||
toBaseHandle,
|
||||
]);
|
||||
|
||||
// Server-recommended models: models.json entries available on the server (for self-hosted)
|
||||
// Filter out letta/letta-free legacy model
|
||||
const serverRecommendedModels = useMemo(() => {
|
||||
if (!isSelfHosted || availableHandles === undefined) return [];
|
||||
const available = typedModels.filter(
|
||||
(m) =>
|
||||
availableHandles !== null &&
|
||||
availableHandles.has(m.handle) &&
|
||||
m.handle !== "letta/letta-free",
|
||||
);
|
||||
if (searchQuery) {
|
||||
const query = searchQuery.toLowerCase();
|
||||
return available.filter(
|
||||
(m) =>
|
||||
m.label.toLowerCase().includes(query) ||
|
||||
m.description.toLowerCase().includes(query) ||
|
||||
m.handle.toLowerCase().includes(query),
|
||||
);
|
||||
}
|
||||
return available;
|
||||
}, [isSelfHosted, typedModels, availableHandles, searchQuery]);
|
||||
|
||||
// Server-all models: ALL handles from the server (for self-hosted)
|
||||
// Filter out letta/letta-free legacy model
|
||||
const serverAllModels = useMemo(() => {
|
||||
if (!isSelfHosted) return [];
|
||||
let handles = allApiHandles.filter((h) => h !== "letta/letta-free");
|
||||
if (searchQuery) {
|
||||
const query = searchQuery.toLowerCase();
|
||||
handles = handles.filter((h) => h.toLowerCase().includes(query));
|
||||
}
|
||||
return handles;
|
||||
}, [isSelfHosted, allApiHandles, searchQuery]);
|
||||
|
||||
// Get the list for current category
|
||||
const currentList: UiModel[] = useMemo(() => {
|
||||
if (category === "supported") {
|
||||
@@ -313,6 +364,18 @@ export function ModelSelector({
|
||||
description: "",
|
||||
}));
|
||||
}
|
||||
if (category === "server-recommended") {
|
||||
return serverRecommendedModels;
|
||||
}
|
||||
if (category === "server-all") {
|
||||
// Convert raw handles to UiModel
|
||||
return serverAllModels.map((handle) => ({
|
||||
id: handle,
|
||||
handle,
|
||||
label: handle,
|
||||
description: "",
|
||||
}));
|
||||
}
|
||||
// For "all" category, convert handles to simple UiModel objects
|
||||
return otherModelHandles.map((handle) => ({
|
||||
id: handle,
|
||||
@@ -320,7 +383,15 @@ export function ModelSelector({
|
||||
label: handle,
|
||||
description: "",
|
||||
}));
|
||||
}, [category, supportedModels, byokModels, byokAllModels, otherModelHandles]);
|
||||
}, [
|
||||
category,
|
||||
supportedModels,
|
||||
byokModels,
|
||||
byokAllModels,
|
||||
otherModelHandles,
|
||||
serverRecommendedModels,
|
||||
serverAllModels,
|
||||
]);
|
||||
|
||||
// Show 1 fewer item because Search line takes space
|
||||
const visibleCount = VISIBLE_ITEMS - 1;
|
||||
@@ -466,10 +537,19 @@ export function ModelSelector({
|
||||
if (cat === "supported") return `Letta API [${supportedModels.length}]`;
|
||||
if (cat === "byok") return `BYOK [${byokModels.length}]`;
|
||||
if (cat === "byok-all") return `BYOK (all) [${byokAllModels.length}]`;
|
||||
if (cat === "server-recommended")
|
||||
return `Recommended [${serverRecommendedModels.length}]`;
|
||||
if (cat === "server-all") return `All models [${serverAllModels.length}]`;
|
||||
return `Letta API (all) [${otherModelHandles.length}]`;
|
||||
};
|
||||
|
||||
const getCategoryDescription = (cat: ModelCategory) => {
|
||||
if (cat === "server-recommended") {
|
||||
return "Recommended models on the server";
|
||||
}
|
||||
if (cat === "server-all") {
|
||||
return "All models on the server";
|
||||
}
|
||||
if (cat === "supported") {
|
||||
return isFreeTier
|
||||
? "Upgrade your account to access more models"
|
||||
|
||||
@@ -20,12 +20,15 @@ interface ProfileOption {
|
||||
}
|
||||
|
||||
interface ProfileSelectionResult {
|
||||
type: "select" | "new" | "exit";
|
||||
type: "select" | "new" | "new_with_model" | "exit";
|
||||
agentId?: string;
|
||||
profileName?: string | null;
|
||||
model?: string;
|
||||
}
|
||||
|
||||
const MAX_DISPLAY = 3;
|
||||
const MAX_VISIBLE_MODELS = 8;
|
||||
const MODEL_SEARCH_THRESHOLD = 10; // Show search input when more than this many models
|
||||
|
||||
function formatRelativeTime(dateStr: string | null | undefined): string {
|
||||
if (!dateStr) return "Never";
|
||||
@@ -65,11 +68,19 @@ function ProfileSelectionUI({
|
||||
lruAgentId,
|
||||
externalLoading,
|
||||
externalFreshRepoMode,
|
||||
failedAgentMessage,
|
||||
serverModelsForNewAgent,
|
||||
defaultModelHandle,
|
||||
serverBaseUrl,
|
||||
onComplete,
|
||||
}: {
|
||||
lruAgentId: string | null;
|
||||
externalLoading?: boolean;
|
||||
externalFreshRepoMode?: boolean;
|
||||
failedAgentMessage?: string;
|
||||
serverModelsForNewAgent?: string[];
|
||||
defaultModelHandle?: string;
|
||||
serverBaseUrl?: string;
|
||||
onComplete: (result: ProfileSelectionResult) => void;
|
||||
}) {
|
||||
const [options, setOptions] = useState<ProfileOption[]>([]);
|
||||
@@ -77,6 +88,13 @@ function ProfileSelectionUI({
|
||||
const loading = externalLoading || internalLoading;
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const [showAll, setShowAll] = useState(false);
|
||||
// Model selection mode for self-hosted servers
|
||||
// Start in model selection mode if serverModelsForNewAgent is provided and no agents to show
|
||||
const [selectingModel, setSelectingModel] = useState(
|
||||
!!(serverModelsForNewAgent && serverModelsForNewAgent.length > 0),
|
||||
);
|
||||
const [modelSelectedIndex, setModelSelectedIndex] = useState(0);
|
||||
const [modelSearchQuery, setModelSearchQuery] = useState("");
|
||||
|
||||
const loadOptions = useCallback(async () => {
|
||||
setInternalLoading(true);
|
||||
@@ -146,9 +164,78 @@ function ProfileSelectionUI({
|
||||
const hasMore = options.length > MAX_DISPLAY;
|
||||
const totalItems = displayOptions.length + 1 + (hasMore && !showAll ? 1 : 0);
|
||||
|
||||
// Model selection - filter out legacy models and apply search
|
||||
const allServerModels =
|
||||
serverModelsForNewAgent?.filter((h) => h !== "letta/letta-free") ?? [];
|
||||
const showModelSearch = allServerModels.length > MODEL_SEARCH_THRESHOLD;
|
||||
const filteredModels = modelSearchQuery
|
||||
? allServerModels.filter((h) =>
|
||||
h.toLowerCase().includes(modelSearchQuery.toLowerCase()),
|
||||
)
|
||||
: allServerModels;
|
||||
const modelCount = filteredModels.length;
|
||||
|
||||
// Model selection scrolling
|
||||
const modelStartIndex = Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
modelSelectedIndex - MAX_VISIBLE_MODELS + 1,
|
||||
modelCount - MAX_VISIBLE_MODELS,
|
||||
),
|
||||
);
|
||||
const visibleModels = filteredModels.slice(
|
||||
modelStartIndex,
|
||||
modelStartIndex + MAX_VISIBLE_MODELS,
|
||||
);
|
||||
const showModelScrollDown = modelStartIndex + MAX_VISIBLE_MODELS < modelCount;
|
||||
const modelsBelow = modelCount - modelStartIndex - MAX_VISIBLE_MODELS;
|
||||
|
||||
useInput((_input, key) => {
|
||||
if (loading) return;
|
||||
|
||||
// Model selection mode
|
||||
if (selectingModel && serverModelsForNewAgent) {
|
||||
if (key.upArrow) {
|
||||
setModelSelectedIndex((prev) => Math.max(0, prev - 1));
|
||||
} else if (key.downArrow) {
|
||||
setModelSelectedIndex((prev) =>
|
||||
Math.min(filteredModels.length - 1, prev + 1),
|
||||
);
|
||||
} else if (key.return) {
|
||||
const selected = filteredModels[modelSelectedIndex];
|
||||
if (selected) {
|
||||
onComplete({ type: "new_with_model", model: selected });
|
||||
}
|
||||
} else if (key.escape || (key.ctrl && _input === "c")) {
|
||||
// Go back to agent selection or exit
|
||||
if (options.length > 0) {
|
||||
setSelectingModel(false);
|
||||
setModelSearchQuery("");
|
||||
setModelSelectedIndex(0);
|
||||
} else {
|
||||
onComplete({ type: "exit" });
|
||||
}
|
||||
} else if (key.backspace || key.delete) {
|
||||
// Handle backspace for search
|
||||
if (showModelSearch && modelSearchQuery.length > 0) {
|
||||
setModelSearchQuery((prev) => prev.slice(0, -1));
|
||||
setModelSelectedIndex(0);
|
||||
}
|
||||
} else if (
|
||||
showModelSearch &&
|
||||
_input &&
|
||||
_input.length === 1 &&
|
||||
!key.ctrl &&
|
||||
!key.meta
|
||||
) {
|
||||
// Handle typing for search
|
||||
setModelSearchQuery((prev) => prev + _input);
|
||||
setModelSelectedIndex(0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Agent selection mode
|
||||
if (key.upArrow) {
|
||||
setSelectedIndex((prev) => Math.max(0, prev - 1));
|
||||
} else if (key.downArrow) {
|
||||
@@ -171,9 +258,16 @@ function ProfileSelectionUI({
|
||||
setShowAll(true);
|
||||
setSelectedIndex(0);
|
||||
} else {
|
||||
onComplete({ type: "new" });
|
||||
// "Create new agent" selected
|
||||
if (serverModelsForNewAgent && serverModelsForNewAgent.length > 0) {
|
||||
// Need to pick a model first
|
||||
setSelectingModel(true);
|
||||
setModelSelectedIndex(0);
|
||||
} else {
|
||||
onComplete({ type: "new" });
|
||||
}
|
||||
}
|
||||
} else if (key.escape) {
|
||||
} else if (key.escape || (key.ctrl && _input === "c")) {
|
||||
onComplete({ type: "exit" });
|
||||
}
|
||||
});
|
||||
@@ -196,12 +290,86 @@ function ProfileSelectionUI({
|
||||
/>
|
||||
<Box height={1} />
|
||||
|
||||
{failedAgentMessage && (
|
||||
<>
|
||||
<Text color="yellow">{failedAgentMessage}</Text>
|
||||
<Box height={1} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<Text dimColor>Loading pinned agents...</Text>
|
||||
) : selectingModel && serverModelsForNewAgent ? (
|
||||
// Model selection mode
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Text bold color={colors.selector.title}>
|
||||
Select a model
|
||||
</Text>
|
||||
<Text dimColor>
|
||||
The default model ({defaultModelHandle || "unknown"}) is not
|
||||
available on this server.
|
||||
</Text>
|
||||
|
||||
{showModelSearch && (
|
||||
<Box>
|
||||
<Text dimColor>Search: </Text>
|
||||
<Text>{modelSearchQuery || ""}</Text>
|
||||
<Text dimColor>█</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{allServerModels.length === 0 ? (
|
||||
<Box flexDirection="column">
|
||||
<Text color="yellow">No models found on server.</Text>
|
||||
<Text dimColor>Server: {serverBaseUrl || "unknown"}</Text>
|
||||
<Text dimColor>
|
||||
Did you remember to start the server with your LLM API keys?
|
||||
</Text>
|
||||
</Box>
|
||||
) : filteredModels.length === 0 ? (
|
||||
<Text dimColor>No models matching "{modelSearchQuery}"</Text>
|
||||
) : (
|
||||
<Box flexDirection="column">
|
||||
{visibleModels.map((handle, index) => {
|
||||
const actualIndex = modelStartIndex + index;
|
||||
const isSelected = actualIndex === modelSelectedIndex;
|
||||
return (
|
||||
<Box key={handle}>
|
||||
<Text
|
||||
color={
|
||||
isSelected ? colors.selector.itemHighlighted : undefined
|
||||
}
|
||||
>
|
||||
{isSelected ? "> " : " "}
|
||||
{handle}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
{/* Phantom space or scroll indicator - always reserve the line */}
|
||||
{showModelScrollDown ? (
|
||||
<Text dimColor> ↓ {modelsBelow} more</Text>
|
||||
) : modelCount > MAX_VISIBLE_MODELS ? (
|
||||
<Text> </Text>
|
||||
) : null}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box>
|
||||
<Text dimColor>
|
||||
↑↓ navigate · Enter select
|
||||
{showModelSearch ? " · Type to search" : ""} · Esc{" "}
|
||||
{options.length > 0 ? "back" : "exit"}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
) : (
|
||||
// Agent selection mode
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Text dimColor>{contextMessage}</Text>
|
||||
<Text bold>Which agent would you like to use?</Text>
|
||||
{options.length > 0 && (
|
||||
<Text bold>Which agent would you like to use?</Text>
|
||||
)}
|
||||
|
||||
<Box flexDirection="column" gap={1}>
|
||||
{displayOptions.map((option, index) => {
|
||||
@@ -218,7 +386,7 @@ function ProfileSelectionUI({
|
||||
isSelected ? colors.selector.itemHighlighted : undefined
|
||||
}
|
||||
>
|
||||
{isSelected ? "→ " : " "}
|
||||
{isSelected ? "> " : " "}
|
||||
</Text>
|
||||
<Text
|
||||
bold={isSelected}
|
||||
@@ -260,7 +428,7 @@ function ProfileSelectionUI({
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{selectedIndex === displayOptions.length ? "→ " : " "}
|
||||
{selectedIndex === displayOptions.length ? "> " : " "}
|
||||
View all {options.length} profiles
|
||||
</Text>
|
||||
</Box>
|
||||
@@ -274,14 +442,14 @@ function ProfileSelectionUI({
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{selectedIndex === totalItems - 1 ? "→ " : " "}
|
||||
{selectedIndex === totalItems - 1 ? "> " : " "}
|
||||
Create a new agent
|
||||
</Text>
|
||||
<Text dimColor> (--new)</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Box>
|
||||
<Text dimColor>↑↓ navigate · Enter select · Esc exit</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -297,15 +465,29 @@ export function ProfileSelectionInline({
|
||||
lruAgentId,
|
||||
loading: externalLoading,
|
||||
freshRepoMode,
|
||||
failedAgentMessage,
|
||||
serverModelsForNewAgent,
|
||||
defaultModelHandle,
|
||||
serverBaseUrl,
|
||||
onSelect,
|
||||
onCreateNew,
|
||||
onCreateNewWithModel,
|
||||
onExit,
|
||||
}: {
|
||||
lruAgentId: string | null;
|
||||
loading?: boolean;
|
||||
freshRepoMode?: boolean;
|
||||
failedAgentMessage?: string;
|
||||
/** If provided, show model selector when user clicks "Create new" */
|
||||
serverModelsForNewAgent?: string[];
|
||||
/** The default model handle that wasn't available */
|
||||
defaultModelHandle?: string;
|
||||
/** The server base URL for error messages */
|
||||
serverBaseUrl?: string;
|
||||
onSelect: (agentId: string) => void;
|
||||
onCreateNew: () => void;
|
||||
/** Called when user selects a model from serverModelsForNewAgent */
|
||||
onCreateNewWithModel?: (model: string) => void;
|
||||
onExit: () => void;
|
||||
}) {
|
||||
const handleComplete = (result: ProfileSelectionResult) => {
|
||||
@@ -313,6 +495,8 @@ export function ProfileSelectionInline({
|
||||
onExit();
|
||||
} else if (result.type === "select" && result.agentId) {
|
||||
onSelect(result.agentId);
|
||||
} else if (result.type === "new_with_model" && result.model) {
|
||||
onCreateNewWithModel?.(result.model);
|
||||
} else {
|
||||
onCreateNew();
|
||||
}
|
||||
@@ -322,6 +506,10 @@ export function ProfileSelectionInline({
|
||||
lruAgentId,
|
||||
externalLoading,
|
||||
externalFreshRepoMode: freshRepoMode,
|
||||
failedAgentMessage,
|
||||
serverModelsForNewAgent,
|
||||
defaultModelHandle,
|
||||
serverBaseUrl,
|
||||
onComplete: handleComplete,
|
||||
});
|
||||
}
|
||||
|
||||
89
src/index.ts
89
src/index.ts
@@ -999,6 +999,23 @@ async function main(): Promise<void> {
|
||||
>(null);
|
||||
// Track when user explicitly requested new agent from selector (not via --new flag)
|
||||
const [userRequestedNewAgent, setUserRequestedNewAgent] = useState(false);
|
||||
// Message to show when LRU/selected agent failed to load
|
||||
const [failedAgentMessage, setFailedAgentMessage] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
// For self-hosted: available model handles from server and user's selection
|
||||
const [availableServerModels, setAvailableServerModels] = useState<
|
||||
string[]
|
||||
>([]);
|
||||
const [selectedServerModel, setSelectedServerModel] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [selfHostedDefaultModel, setSelfHostedDefaultModel] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [selfHostedBaseUrl, setSelfHostedBaseUrl] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
// Release notes to display (checked once on mount)
|
||||
const [releaseNotes, setReleaseNotes] = useState<string | null>(null);
|
||||
@@ -1095,6 +1112,35 @@ async function main(): Promise<void> {
|
||||
let globalPinned = settingsManager.getGlobalPinnedAgents();
|
||||
const client = await getClient();
|
||||
|
||||
// For self-hosted servers, pre-fetch available models
|
||||
// This is needed so ProfileSelectionInline can show model picker
|
||||
// if the default model isn't available
|
||||
const baseURL =
|
||||
process.env.LETTA_BASE_URL ||
|
||||
settings.env?.LETTA_BASE_URL ||
|
||||
LETTA_CLOUD_API_URL;
|
||||
const isSelfHosted = !baseURL.includes("api.letta.com");
|
||||
|
||||
if (isSelfHosted) {
|
||||
setSelfHostedBaseUrl(baseURL);
|
||||
try {
|
||||
const { getDefaultModel } = await import("./agent/model");
|
||||
const defaultModel = getDefaultModel();
|
||||
setSelfHostedDefaultModel(defaultModel);
|
||||
const modelsList = await client.models.list();
|
||||
const handles = modelsList
|
||||
.map((m) => m.handle)
|
||||
.filter((h): h is string => typeof h === "string");
|
||||
|
||||
// Only set if default model isn't available
|
||||
if (!handles.includes(defaultModel)) {
|
||||
setAvailableServerModels(handles);
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors - will fail naturally during agent creation if needed
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// TOP-LEVEL PATH: --conversation <id>
|
||||
// Conversation ID is unique, so we can derive the agent from it
|
||||
@@ -1158,12 +1204,12 @@ async function main(): Promise<void> {
|
||||
return;
|
||||
} catch {
|
||||
// Local agent doesn't exist, try global
|
||||
console.log(
|
||||
setFailedAgentMessage(
|
||||
`Unable to locate agent ${localAgentId} in .letta/, checking global (~/.letta)`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.log("No recent agent in .letta/, using global (~/.letta)");
|
||||
// No recent agent locally, silently fall through to global
|
||||
}
|
||||
|
||||
// Try global LRU
|
||||
@@ -1209,7 +1255,7 @@ async function main(): Promise<void> {
|
||||
return;
|
||||
} catch {
|
||||
// Local agent doesn't exist, try global
|
||||
console.log(
|
||||
setFailedAgentMessage(
|
||||
`Unable to locate agent ${localAgentId} in .letta/, checking global (~/.letta)`,
|
||||
);
|
||||
}
|
||||
@@ -1288,7 +1334,7 @@ async function main(): Promise<void> {
|
||||
return;
|
||||
} catch {
|
||||
// LRU agent doesn't exist, show message and fall through to selector
|
||||
console.log(
|
||||
setFailedAgentMessage(
|
||||
`Unable to locate recently used agent ${localSettings.lastAgent}`,
|
||||
);
|
||||
}
|
||||
@@ -1462,10 +1508,18 @@ async function main(): Promise<void> {
|
||||
|
||||
// Priority 3: Check if --new flag was passed or user requested new from selector
|
||||
if (!agent && shouldCreateNew) {
|
||||
const updateArgs = getModelUpdateArgs(model);
|
||||
// For self-hosted: if default model unavailable and no model selected yet, show picker
|
||||
if (availableServerModels.length > 0 && !selectedServerModel) {
|
||||
setLoadingState("selecting_global");
|
||||
return;
|
||||
}
|
||||
|
||||
// Use selected server model (from self-hosted model picker) if available
|
||||
const effectiveModel = selectedServerModel || model;
|
||||
const updateArgs = getModelUpdateArgs(effectiveModel);
|
||||
const result = await createAgent(
|
||||
undefined,
|
||||
model,
|
||||
effectiveModel,
|
||||
undefined,
|
||||
updateArgs,
|
||||
skillsDirectory,
|
||||
@@ -1818,9 +1872,17 @@ async function main(): Promise<void> {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Don't render anything during initial "selecting" phase - wait for checkAndStart
|
||||
// During initial "selecting" phase, render ProfileSelectionInline with loading state
|
||||
// to prevent component tree switch whitespace artifacts
|
||||
if (loadingState === "selecting") {
|
||||
return null;
|
||||
return React.createElement(ProfileSelectionInline, {
|
||||
lruAgentId: null,
|
||||
loading: true, // Show loading state while checking
|
||||
freshRepoMode: true,
|
||||
onSelect: () => {},
|
||||
onCreateNew: () => {},
|
||||
onExit: () => process.exit(0),
|
||||
});
|
||||
}
|
||||
|
||||
// Show conversation selector for --resume flag
|
||||
@@ -1849,6 +1911,12 @@ async function main(): Promise<void> {
|
||||
lruAgentId: null, // No LRU in fresh repo
|
||||
loading: false,
|
||||
freshRepoMode: true, // Hides "(global)" labels and simplifies context message
|
||||
failedAgentMessage: failedAgentMessage ?? undefined,
|
||||
// For self-hosted: pass available models so user can pick one when creating new agent
|
||||
serverModelsForNewAgent:
|
||||
availableServerModels.length > 0 ? availableServerModels : undefined,
|
||||
defaultModelHandle: selfHostedDefaultModel ?? undefined,
|
||||
serverBaseUrl: selfHostedBaseUrl ?? undefined,
|
||||
onSelect: (agentId: string) => {
|
||||
setSelectedGlobalAgentId(agentId);
|
||||
setLoadingState("assembling");
|
||||
@@ -1857,6 +1925,11 @@ async function main(): Promise<void> {
|
||||
setUserRequestedNewAgent(true);
|
||||
setLoadingState("assembling");
|
||||
},
|
||||
onCreateNewWithModel: (modelHandle: string) => {
|
||||
setUserRequestedNewAgent(true);
|
||||
setSelectedServerModel(modelHandle);
|
||||
setLoadingState("assembling");
|
||||
},
|
||||
onExit: () => {
|
||||
process.exit(0);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user