diff --git a/src/cli/App.tsx b/src/cli/App.tsx
index ce5a0db..5ce2410 100644
--- a/src/cli/App.tsx
+++ b/src/cli/App.tsx
@@ -9097,10 +9097,9 @@ ${SYSTEM_REMINDER_CLOSE}
}
if (!selectedModel) {
- const cmd =
- overlayCommand ??
- commandRunner.start("/model", `Model not found: ${modelId}`);
- cmd.fail(`Model not found: ${modelId}`);
+ const output = `Model not found: ${modelId}. Run /model and press R to refresh available models.`;
+ const cmd = overlayCommand ?? commandRunner.start("/model", output);
+ cmd.fail(output);
return;
}
const model = selectedModel;
@@ -9191,10 +9190,18 @@ ${SYSTEM_REMINDER_CLOSE}
});
} catch (error) {
const errorDetails = formatErrorDetails(error, agentId);
+ const modelLabel = selectedModel?.label ?? modelId;
+ const guidance =
+ "Run /model and press R to refresh available models. If the model is still unavailable, choose another model or connect a provider with /connect.";
const cmd =
overlayCommand ??
- commandRunner.start("/model", "Failed to switch model.");
- cmd.fail(`Failed to switch model: ${errorDetails}`);
+ commandRunner.start(
+ "/model",
+ `Failed to switch model to ${modelLabel}.`,
+ );
+ cmd.fail(
+ `Failed to switch model to ${modelLabel}: ${errorDetails}\n${guidance}`,
+ );
}
},
[
diff --git a/src/cli/components/ModelSelector.tsx b/src/cli/components/ModelSelector.tsx
index 0a2520b..9d05b76 100644
--- a/src/cli/components/ModelSelector.tsx
+++ b/src/cli/components/ModelSelector.tsx
@@ -543,26 +543,26 @@ export function ModelSelector({
const getCategoryDescription = (cat: ModelCategory) => {
if (cat === "server-recommended") {
- return "Recommended models on the server";
+ return "Recommended models currently available for this account";
}
if (cat === "server-all") {
- return "All models on the server";
+ return "All models currently available for this account";
}
if (cat === "supported") {
return isFreeTier
? "Upgrade your account to access more models"
- : "Recommended models on the Letta API";
+ : "Recommended Letta API models currently available for this account";
}
if (cat === "byok")
- return "Recommended models via your API keys (use /connect to add more)";
+ return "Recommended models via your connected API keys (use /connect to add more)";
if (cat === "byok-all")
- return "All models via your API keys (use /connect to add more)";
+ return "All models via your connected API keys (use /connect to add more)";
if (cat === "all") {
return isFreeTier
? "Upgrade your account to access more models"
- : "All models on the Letta API";
+ : "All Letta API models currently available for this account";
}
- return "All models on the Letta API";
+ return "All Letta API models currently available for this account";
};
// Render tab bar (matches AgentSelector style)
@@ -700,7 +700,7 @@ export function ModelSelector({
{" "}
{currentList.length} models{isCached ? " · cached" : ""} · R to
- refresh
+ refresh availability
{" "}Enter select · ↑↓ navigate · ←→/Tab switch · Esc cancel
diff --git a/src/cli/helpers/errorFormatter.ts b/src/cli/helpers/errorFormatter.ts
index 5f58cbe..7eb40a4 100644
--- a/src/cli/helpers/errorFormatter.ts
+++ b/src/cli/helpers/errorFormatter.ts
@@ -5,6 +5,64 @@ const LETTA_USAGE_URL = "https://app.letta.com/settings/organization/usage";
const LETTA_AGENTS_URL =
"https://app.letta.com/projects/default-project/agents";
+function extractReasonList(value: unknown): string[] {
+ if (!Array.isArray(value)) return [];
+
+ return value
+ .filter((reason): reason is string => typeof reason === "string")
+ .map((reason) => reason.toLowerCase());
+}
+
+function getErrorReasons(e: APIError): string[] {
+ const reasons = new Set();
+
+ const errorBody = e.error;
+ if (errorBody && typeof errorBody === "object") {
+ const body = errorBody as Record;
+
+ for (const reason of extractReasonList(body.reasons)) {
+ reasons.add(reason);
+ }
+
+ if (body.error && typeof body.error === "object") {
+ const nested = body.error as Record;
+ for (const reason of extractReasonList(nested.reasons)) {
+ reasons.add(reason);
+ }
+ }
+ }
+
+ // Fallback: infer known reasons from message text.
+ const message = e.message?.toLowerCase() ?? "";
+ for (const knownReason of [
+ "not-enough-credits",
+ "model-unknown",
+ "byok-not-available-on-free-tier",
+ "free-usage-exceeded",
+ "premium-usage-exceeded",
+ "standard-usage-exceeded",
+ "basic-usage-exceeded",
+ "context-window-size-not-supported",
+ "agents-limit-exceeded",
+ "exceeded-quota",
+ ]) {
+ if (message.includes(knownReason)) {
+ reasons.add(knownReason);
+ }
+ }
+
+ return Array.from(reasons);
+}
+
+function hasErrorReason(
+ e: APIError,
+ reason: string,
+ reasons?: string[],
+): boolean {
+ const allReasons = reasons ?? getErrorReasons(e);
+ return allReasons.includes(reason.toLowerCase());
+}
+
/**
* Check if the error is a rate limit error (429 with exceeded-quota)
* Returns the timeToQuotaResetMs if it's a rate limit error, undefined otherwise
@@ -94,53 +152,18 @@ function getResourceLimitMessage(e: APIError): string | undefined {
/**
* Check if the error is an agent limit error (429 with agents-limit-exceeded)
*/
-function isAgentLimitError(e: APIError): boolean {
+function isAgentLimitError(e: APIError, reasons?: string[]): boolean {
if (e.status !== 429) return false;
-
- const errorBody = e.error;
- if (errorBody && typeof errorBody === "object") {
- if ("reasons" in errorBody && Array.isArray(errorBody.reasons)) {
- if (errorBody.reasons.includes("agents-limit-exceeded")) {
- return true;
- }
- }
- }
- return false;
+ return hasErrorReason(e, "agents-limit-exceeded", reasons);
}
/**
* Check if the error is a credit exhaustion error (402 with not-enough-credits)
*/
-function isCreditExhaustedError(e: APIError): boolean {
+function isCreditExhaustedError(e: APIError, reasons?: string[]): boolean {
// Check status code
if (e.status !== 402) return false;
-
- // Check for "not-enough-credits" in various places it could appear
- const errorBody = e.error;
- if (errorBody && typeof errorBody === "object") {
- // Check reasons array: {"error":"Rate limited","reasons":["not-enough-credits"]}
- if ("reasons" in errorBody && Array.isArray(errorBody.reasons)) {
- if (errorBody.reasons.includes("not-enough-credits")) {
- return true;
- }
- }
- // Check nested error.reasons
- if ("error" in errorBody && typeof errorBody.error === "object") {
- const nested = errorBody.error as Record;
- if ("reasons" in nested && Array.isArray(nested.reasons)) {
- if (nested.reasons.includes("not-enough-credits")) {
- return true;
- }
- }
- }
- }
-
- // Also check the message for "not-enough-credits" as a fallback
- if (e.message?.includes("not-enough-credits")) {
- return true;
- }
-
- return false;
+ return hasErrorReason(e, "not-enough-credits", reasons);
}
/**
@@ -159,6 +182,8 @@ export function formatErrorDetails(
// Handle APIError from streaming (event: error)
if (e instanceof APIError) {
+ const reasons = getErrorReasons(e);
+
// Check for rate limit error first - provide a friendly message with reset time
const rateLimitResetMs = getRateLimitResetMs(e);
if (rateLimitResetMs !== undefined) {
@@ -170,7 +195,7 @@ export function formatErrorDetails(
}
// Check for agent limit error (free tier agent count limit)
- if (isAgentLimitError(e)) {
+ if (isAgentLimitError(e, reasons)) {
const { billingTier } = getErrorContext();
if (billingTier?.toLowerCase() === "free") {
@@ -181,6 +206,14 @@ export function formatErrorDetails(
return `You've reached your agent limit. Delete agents at: ${LETTA_AGENTS_URL}\nOr check your plan at: ${LETTA_USAGE_URL}`;
}
+ if (hasErrorReason(e, "model-unknown", reasons)) {
+ return `The selected model is not currently available for this account or provider. Run /model and press R to refresh available models, then choose an available model or connect a provider with /connect.`;
+ }
+
+ if (hasErrorReason(e, "context-window-size-not-supported", reasons)) {
+ return `The selected context window is not supported for this model. Switch models with /model or pick a model with a larger context window.`;
+ }
+
// Check for resource limit error (e.g., "You have reached your limit for agents")
const resourceLimitMsg = getResourceLimitMessage(e);
if (resourceLimitMsg) {
@@ -191,16 +224,26 @@ export function formatErrorDetails(
}
// Check for credit exhaustion error - provide a friendly message
- if (isCreditExhaustedError(e)) {
- const { billingTier, modelDisplayName } = getErrorContext();
+ if (isCreditExhaustedError(e, reasons)) {
+ return `Your account is out of credits for hosted inference. Add credits, enable auto-recharge, or upgrade at ${LETTA_USAGE_URL}. You can also connect your own provider keys with /connect.`;
+ }
- // Free plan users get a special message about BYOK and free models
- if (billingTier?.toLowerCase() === "free") {
- const modelInfo = modelDisplayName ? ` (${modelDisplayName})` : "";
- return `Selected hosted model${modelInfo} not available on Free plan. Switch to a free model with /model (glm-4.7 or minimax-m2.1), upgrade your account at ${LETTA_USAGE_URL}, or connect your own API keys with /connect.`;
- }
+ if (
+ hasErrorReason(e, "premium-usage-exceeded", reasons) ||
+ hasErrorReason(e, "standard-usage-exceeded", reasons) ||
+ hasErrorReason(e, "basic-usage-exceeded", reasons)
+ ) {
+ return `You've reached your hosted model usage limit. View your plan and usage at ${LETTA_USAGE_URL}, or connect your own provider keys with /connect.`;
+ }
- return `Your account is out of credits. Redeem additional credits or configure auto-recharge on your account page: ${LETTA_USAGE_URL}`;
+ if (hasErrorReason(e, "byok-not-available-on-free-tier", reasons)) {
+ const { modelDisplayName } = getErrorContext();
+ const modelInfo = modelDisplayName ? ` (${modelDisplayName})` : "";
+ return `Selected BYOK model${modelInfo} is not available on the Free plan. Switch to a free hosted model with /model (glm-4.7 or minimax-m2.1), or upgrade at ${LETTA_USAGE_URL}.`;
+ }
+
+ if (hasErrorReason(e, "free-usage-exceeded", reasons)) {
+ return `You've reached the Free plan hosted model usage limit. Switch to free hosted models with /model (glm-4.7 or minimax-m2.1), upgrade at ${LETTA_USAGE_URL}, or connect your own provider keys with /connect.`;
}
// Check for nested error structure: e.error.error
if (e.error && typeof e.error === "object" && "error" in e.error) {
diff --git a/src/tests/cli/errorFormatter.test.ts b/src/tests/cli/errorFormatter.test.ts
new file mode 100644
index 0000000..8cab50b
--- /dev/null
+++ b/src/tests/cli/errorFormatter.test.ts
@@ -0,0 +1,106 @@
+import { beforeEach, describe, expect, test } from "bun:test";
+import { APIError } from "@letta-ai/letta-client/core/error";
+import {
+ clearErrorContext,
+ setErrorContext,
+} from "../../cli/helpers/errorContext";
+import { formatErrorDetails } from "../../cli/helpers/errorFormatter";
+
+describe("formatErrorDetails", () => {
+ beforeEach(() => {
+ clearErrorContext();
+ });
+
+ test("uses neutral credit exhaustion copy for free tier not-enough-credits", () => {
+ setErrorContext({ billingTier: "free", modelDisplayName: "Kimi K2.5" });
+
+ const error = new APIError(
+ 402,
+ {
+ error: "Rate limited",
+ reasons: ["not-enough-credits"],
+ },
+ undefined,
+ new Headers(),
+ );
+
+ const message = formatErrorDetails(error);
+
+ expect(message).toContain("out of credits");
+ expect(message).toContain("/connect");
+ expect(message).not.toContain("not available on Free plan");
+ expect(message).not.toContain("Selected hosted model");
+ });
+
+ test("handles nested reasons for credit exhaustion", () => {
+ const error = new APIError(
+ 402,
+ {
+ error: {
+ reasons: ["not-enough-credits"],
+ },
+ },
+ undefined,
+ new Headers(),
+ );
+
+ const message = formatErrorDetails(error);
+ expect(message).toContain("out of credits");
+ });
+
+ test("shows explicit model availability guidance for model-unknown", () => {
+ const error = new APIError(
+ 429,
+ {
+ error: "Rate limited",
+ reasons: ["model-unknown"],
+ },
+ undefined,
+ new Headers(),
+ );
+
+ const message = formatErrorDetails(error);
+
+ expect(message).toContain("not currently available");
+ expect(message).toContain("Run /model");
+ expect(message).toContain("press R");
+ });
+
+ test("keeps canonical free model pair for byok-not-available-on-free-tier", () => {
+ setErrorContext({ modelDisplayName: "GPT-5" });
+
+ const error = new APIError(
+ 403,
+ {
+ error: "Forbidden",
+ reasons: ["byok-not-available-on-free-tier"],
+ },
+ undefined,
+ new Headers(),
+ );
+
+ const message = formatErrorDetails(error);
+
+ expect(message).toContain("glm-4.7");
+ expect(message).toContain("minimax-m2.1");
+ expect(message).toContain("Free plan");
+ });
+
+ test("keeps canonical free model pair for free-usage-exceeded", () => {
+ const error = new APIError(
+ 429,
+ {
+ error: "Rate limited",
+ reasons: ["free-usage-exceeded"],
+ },
+ undefined,
+ new Headers(),
+ );
+
+ const message = formatErrorDetails(error);
+
+ expect(message).toContain("glm-4.7");
+ expect(message).toContain("minimax-m2.1");
+ expect(message).toContain("/model");
+ });
+});