feat: add search field to model selector on both tabs (#649)
Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
@@ -69,16 +69,62 @@ export function getModelUpdateArgs(
|
||||
return modelInfo?.updateArgs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a model entry by handle with fuzzy matching support
|
||||
* @param handle - The full model handle
|
||||
* @returns The model entry if found, null otherwise
|
||||
*/
|
||||
function findModelByHandle(handle: string): (typeof models)[number] | null {
|
||||
// Try exact match first
|
||||
const exactMatch = models.find((m) => m.handle === handle);
|
||||
if (exactMatch) return exactMatch;
|
||||
|
||||
// For handles like "bedrock/claude-opus-4-5-20251101" where the API returns without
|
||||
// vendor prefix or version suffix, but models.json has
|
||||
// "bedrock/us.anthropic.claude-opus-4-5-20251101-v1:0", try fuzzy matching
|
||||
const [provider, ...rest] = handle.split("/");
|
||||
if (provider && rest.length > 0) {
|
||||
const modelPortion = rest.join("/");
|
||||
// Find models with the same provider where the model portion is contained
|
||||
// in the models.json handle (handles vendor prefixes and version suffixes)
|
||||
const partialMatch = models.find((m) => {
|
||||
if (!m.handle.startsWith(`${provider}/`)) return false;
|
||||
const mModelPortion = m.handle.slice(provider.length + 1);
|
||||
// Check if either contains the other (handles both directions)
|
||||
return (
|
||||
mModelPortion.includes(modelPortion) ||
|
||||
modelPortion.includes(mModelPortion)
|
||||
);
|
||||
});
|
||||
if (partialMatch) return partialMatch;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a display-friendly name for a model by its handle
|
||||
* @param handle - The full model handle (e.g., "anthropic/claude-sonnet-4-5-20250929")
|
||||
* @returns The display name (e.g., "Sonnet 4.5") if found, null otherwise
|
||||
*/
|
||||
export function getModelDisplayName(handle: string): string | null {
|
||||
const model = models.find((m) => m.handle === handle);
|
||||
const model = findModelByHandle(handle);
|
||||
return model?.label ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a short display name for a model (for status bar)
|
||||
* Falls back to full label if no shortLabel is defined
|
||||
* @param handle - The full model handle
|
||||
* @returns The short name (e.g., "Opus 4.5 BR") if found, null otherwise
|
||||
*/
|
||||
export function getModelShortName(handle: string): string | null {
|
||||
const model = findModelByHandle(handle);
|
||||
if (!model) return null;
|
||||
// Use shortLabel if available, otherwise fall back to label
|
||||
return (model as { shortLabel?: string }).shortLabel ?? model.label;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a model ID from the llm_config.model value
|
||||
* The llm_config.model is the model portion without the provider prefix
|
||||
|
||||
@@ -43,7 +43,7 @@ import { getCurrentAgentId, setCurrentAgentId } from "../agent/context";
|
||||
import { type AgentProvenance, createAgent } from "../agent/create";
|
||||
import { ISOLATED_BLOCK_LABELS } from "../agent/memory";
|
||||
import { sendMessageStream } from "../agent/message";
|
||||
import { getModelDisplayName, getModelInfo } from "../agent/model";
|
||||
import { getModelInfo, getModelShortName } from "../agent/model";
|
||||
import { SessionStats } from "../agent/stats";
|
||||
import {
|
||||
INTERRUPTED_BY_USER,
|
||||
@@ -1047,7 +1047,7 @@ export default function App({
|
||||
? `${llmConfig.model_endpoint_type}/${llmConfig.model}`
|
||||
: (llmConfig?.model ?? null);
|
||||
const currentModelDisplay = currentModelLabel
|
||||
? (getModelDisplayName(currentModelLabel) ??
|
||||
? (getModelShortName(currentModelLabel) ??
|
||||
currentModelLabel.split("/").pop())
|
||||
: null;
|
||||
const currentModelProvider = llmConfig?.provider_name ?? null;
|
||||
|
||||
@@ -129,10 +129,20 @@ export function ModelSelector({
|
||||
m.handle.startsWith(`${filterProvider}/`),
|
||||
);
|
||||
}
|
||||
// Apply search filter
|
||||
if (searchQuery) {
|
||||
const query = searchQuery.toLowerCase();
|
||||
available = available.filter(
|
||||
(m) =>
|
||||
m.label.toLowerCase().includes(query) ||
|
||||
m.description.toLowerCase().includes(query) ||
|
||||
m.handle.toLowerCase().includes(query),
|
||||
);
|
||||
}
|
||||
const featured = available.filter((m) => m.isFeatured);
|
||||
const nonFeatured = available.filter((m) => !m.isFeatured);
|
||||
return [...featured, ...nonFeatured];
|
||||
}, [typedModels, availableHandles, filterProvider]);
|
||||
}, [typedModels, availableHandles, filterProvider, searchQuery]);
|
||||
|
||||
// All other models: API handles not in models.json
|
||||
const otherModelHandles = useMemo(() => {
|
||||
@@ -158,8 +168,8 @@ export function ModelSelector({
|
||||
}));
|
||||
}, [category, supportedModels, otherModelHandles]);
|
||||
|
||||
// Show 1 fewer item in "all" category because Search line takes space
|
||||
const visibleCount = category === "all" ? VISIBLE_ITEMS - 1 : VISIBLE_ITEMS;
|
||||
// Show 1 fewer item because Search line takes space
|
||||
const visibleCount = VISIBLE_ITEMS - 1;
|
||||
|
||||
// Scrolling - keep selectedIndex in view
|
||||
const startIndex = useMemo(() => {
|
||||
@@ -276,8 +286,8 @@ export function ModelSelector({
|
||||
if (selectedModel) {
|
||||
onSelect(selectedModel.id);
|
||||
}
|
||||
} else if (category === "all" && input && input.length === 1) {
|
||||
// Capture text input for search (only in "all" category)
|
||||
} else if (input && input.length === 1) {
|
||||
// Capture text input for search
|
||||
setSearchQuery((prev) => prev + input);
|
||||
setSelectedIndex(0);
|
||||
}
|
||||
@@ -328,9 +338,7 @@ export function ModelSelector({
|
||||
{!isLoading && !refreshing && (
|
||||
<Box flexDirection="column" paddingLeft={1}>
|
||||
{renderTabBar()}
|
||||
{category === "all" && (
|
||||
<Text dimColor> Search: {searchQuery || "(type to filter)"}</Text>
|
||||
)}
|
||||
<Text dimColor> Search: {searchQuery || "(type to filter)"}</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -38,7 +38,8 @@
|
||||
{
|
||||
"id": "bedrock-opus",
|
||||
"handle": "bedrock/us.anthropic.claude-opus-4-5-20251101-v1:0",
|
||||
"label": "Bedrock Claude Opus 4.5",
|
||||
"label": "Bedrock Opus 4.5",
|
||||
"shortLabel": "Opus 4.5 BR",
|
||||
"description": "Anthropic's best model (via AWS Bedrock)",
|
||||
"isFeatured": true,
|
||||
"updateArgs": {
|
||||
|
||||
Reference in New Issue
Block a user