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>;
|
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 {
|
interface ModelSelectorProps {
|
||||||
currentModelId?: string;
|
currentModelId?: string;
|
||||||
onSelect: (modelId: string) => void;
|
onSelect: (modelId: string) => void;
|
||||||
@@ -181,10 +202,11 @@ export function ModelSelector({
|
|||||||
const isFreeTier = billingTier?.toLowerCase() === "free";
|
const isFreeTier = billingTier?.toLowerCase() === "free";
|
||||||
const supportedModels = useMemo(() => {
|
const supportedModels = useMemo(() => {
|
||||||
if (availableHandles === undefined) return [];
|
if (availableHandles === undefined) return [];
|
||||||
let available =
|
let available = filterModelsByAvailabilityForSelector(
|
||||||
availableHandles === null
|
typedModels,
|
||||||
? typedModels // fallback
|
availableHandles,
|
||||||
: typedModels.filter((m) => availableHandles.has(m.handle));
|
allApiHandles,
|
||||||
|
);
|
||||||
// Apply provider filter if specified
|
// Apply provider filter if specified
|
||||||
if (filterProvider) {
|
if (filterProvider) {
|
||||||
available = available.filter((m) =>
|
available = available.filter((m) =>
|
||||||
@@ -228,6 +250,7 @@ export function ModelSelector({
|
|||||||
}, [
|
}, [
|
||||||
typedModels,
|
typedModels,
|
||||||
availableHandles,
|
availableHandles,
|
||||||
|
allApiHandles,
|
||||||
filterProvider,
|
filterProvider,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
isFreeTier,
|
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",
|
"id": "sonnet",
|
||||||
"handle": "anthropic/claude-sonnet-4-6",
|
"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