feat: add letta/auto and letta/auto-fast model support (#1259)
Co-authored-by: Letta Code <noreply@letta.com>
This commit is contained in:
@@ -54,6 +54,27 @@ type UiModel = {
|
||||
updateArgs?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
const API_GATED_MODEL_HANDLES = new Set(["letta/auto", "letta/auto-fast"]);
|
||||
|
||||
export function filterModelsByAvailabilityForSelector<
|
||||
T extends { handle: string },
|
||||
>(
|
||||
typedModels: T[],
|
||||
availableHandles: Set<string> | null,
|
||||
allApiHandles: string[],
|
||||
): T[] {
|
||||
if (availableHandles === null) {
|
||||
return typedModels.filter((m) => {
|
||||
if (!API_GATED_MODEL_HANDLES.has(m.handle)) {
|
||||
return true;
|
||||
}
|
||||
return allApiHandles.includes(m.handle);
|
||||
});
|
||||
}
|
||||
|
||||
return typedModels.filter((m) => availableHandles.has(m.handle));
|
||||
}
|
||||
|
||||
interface ModelSelectorProps {
|
||||
currentModelId?: string;
|
||||
onSelect: (modelId: string) => void;
|
||||
@@ -181,10 +202,11 @@ export function ModelSelector({
|
||||
const isFreeTier = billingTier?.toLowerCase() === "free";
|
||||
const supportedModels = useMemo(() => {
|
||||
if (availableHandles === undefined) return [];
|
||||
let available =
|
||||
availableHandles === null
|
||||
? typedModels // fallback
|
||||
: typedModels.filter((m) => availableHandles.has(m.handle));
|
||||
let available = filterModelsByAvailabilityForSelector(
|
||||
typedModels,
|
||||
availableHandles,
|
||||
allApiHandles,
|
||||
);
|
||||
// Apply provider filter if specified
|
||||
if (filterProvider) {
|
||||
available = available.filter((m) =>
|
||||
@@ -228,6 +250,7 @@ export function ModelSelector({
|
||||
}, [
|
||||
typedModels,
|
||||
availableHandles,
|
||||
allApiHandles,
|
||||
filterProvider,
|
||||
searchQuery,
|
||||
isFreeTier,
|
||||
|
||||
@@ -1,4 +1,18 @@
|
||||
[
|
||||
{
|
||||
"id": "auto",
|
||||
"handle": "letta/auto",
|
||||
"label": "Auto",
|
||||
"description": "Automatically select the best model",
|
||||
"isFeatured": true
|
||||
},
|
||||
{
|
||||
"id": "auto-fast",
|
||||
"handle": "letta/auto-fast",
|
||||
"label": "Auto Fast",
|
||||
"description": "Automatically select the best fast model",
|
||||
"isFeatured": true
|
||||
},
|
||||
{
|
||||
"id": "sonnet",
|
||||
"handle": "anthropic/claude-sonnet-4-6",
|
||||
|
||||
57
src/tests/agent/models-auto.integration.test.ts
Normal file
57
src/tests/agent/models-auto.integration.test.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Live API regression test for Cloud model availability.
|
||||
*
|
||||
* Runs only when:
|
||||
* - LETTA_API_KEY is set
|
||||
* - LETTA_BASE_URL points to Letta Cloud (api.letta.com)
|
||||
*/
|
||||
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import Letta from "@letta-ai/letta-client";
|
||||
|
||||
const LETTA_API_KEY = process.env.LETTA_API_KEY;
|
||||
const LETTA_BASE_URL = process.env.LETTA_BASE_URL || "https://api.letta.com";
|
||||
|
||||
function isCloudBaseUrl(value: string): boolean {
|
||||
try {
|
||||
return new URL(value).hostname === "api.letta.com";
|
||||
} catch {
|
||||
return value.includes("api.letta.com");
|
||||
}
|
||||
}
|
||||
|
||||
const describeIntegration =
|
||||
LETTA_API_KEY && isCloudBaseUrl(LETTA_BASE_URL) ? describe : describe.skip;
|
||||
|
||||
async function listModelHandlesWithRetry(
|
||||
client: Letta,
|
||||
maxAttempts = 3,
|
||||
): Promise<string[]> {
|
||||
let lastError: unknown;
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
||||
try {
|
||||
const models = await client.models.list();
|
||||
return models.map((m) => m.handle).filter((h): h is string => Boolean(h));
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
if (attempt < maxAttempts) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500 * attempt));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
describeIntegration("cloud models list", () => {
|
||||
test("includes letta/auto handle", async () => {
|
||||
const client = new Letta({
|
||||
baseURL: LETTA_BASE_URL,
|
||||
apiKey: LETTA_API_KEY,
|
||||
});
|
||||
|
||||
const handles = await listModelHandlesWithRetry(client);
|
||||
expect(handles).toContain("letta/auto");
|
||||
});
|
||||
});
|
||||
55
src/tests/cli/model-selector-availability.test.ts
Normal file
55
src/tests/cli/model-selector-availability.test.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { filterModelsByAvailabilityForSelector } from "../../cli/components/ModelSelector";
|
||||
|
||||
type StubModel = { handle: string; label: string };
|
||||
|
||||
const MODELS: StubModel[] = [
|
||||
{ handle: "letta/auto", label: "Auto" },
|
||||
{ handle: "letta/auto-fast", label: "Auto Fast" },
|
||||
{ handle: "anthropic/claude-sonnet-4-6", label: "Sonnet 4.6" },
|
||||
];
|
||||
|
||||
describe("ModelSelector availability gating", () => {
|
||||
test("includes letta/auto when API availability includes it", () => {
|
||||
const availableHandles = new Set([
|
||||
"letta/auto",
|
||||
"anthropic/claude-sonnet-4-6",
|
||||
]);
|
||||
|
||||
const result = filterModelsByAvailabilityForSelector(
|
||||
MODELS,
|
||||
availableHandles,
|
||||
Array.from(availableHandles),
|
||||
);
|
||||
|
||||
expect(result.map((m) => m.handle)).toContain("letta/auto");
|
||||
});
|
||||
|
||||
test("excludes letta/auto when API availability does not include it", () => {
|
||||
const availableHandles = new Set(["anthropic/claude-sonnet-4-6"]);
|
||||
|
||||
const result = filterModelsByAvailabilityForSelector(
|
||||
MODELS,
|
||||
availableHandles,
|
||||
Array.from(availableHandles),
|
||||
);
|
||||
|
||||
expect(result.map((m) => m.handle)).not.toContain("letta/auto");
|
||||
});
|
||||
|
||||
test("fallback mode hides letta/auto unless explicitly present in allApiHandles", () => {
|
||||
const hiddenResult = filterModelsByAvailabilityForSelector(MODELS, null, [
|
||||
"anthropic/claude-sonnet-4-6",
|
||||
]);
|
||||
expect(hiddenResult.map((m) => m.handle)).not.toContain("letta/auto");
|
||||
expect(hiddenResult.map((m) => m.handle)).toContain(
|
||||
"anthropic/claude-sonnet-4-6",
|
||||
);
|
||||
|
||||
const shownResult = filterModelsByAvailabilityForSelector(MODELS, null, [
|
||||
"letta/auto",
|
||||
"anthropic/claude-sonnet-4-6",
|
||||
]);
|
||||
expect(shownResult.map((m) => m.handle)).toContain("letta/auto");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user