Add DeepSeek TUI to the ACP catalog (#1096)
* Consolidate provider selection handling * List DeepSeek TUI in docs and website
This commit is contained in:
@@ -33,9 +33,9 @@ import {
|
||||
import { getProviderIcon } from "@/components/provider-icons";
|
||||
import { CombinedModelSelector } from "@/components/combined-model-selector";
|
||||
import {
|
||||
buildModelSelectorProviders,
|
||||
type ModelSelectorProvider,
|
||||
} from "@/components/combined-model-selector.utils";
|
||||
buildProviderSelectorProviders,
|
||||
type ProviderSelectorProvider,
|
||||
} from "@/provider-selection/provider-selection";
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
import { useProvidersSnapshot } from "@/hooks/use-providers-snapshot";
|
||||
import { resolveProviderDefinition } from "@/utils/provider-definitions";
|
||||
@@ -97,7 +97,7 @@ interface ControlledAgentStatusBarProps {
|
||||
disabled?: boolean;
|
||||
isModelLoading?: boolean;
|
||||
providerDefinitions: AgentProviderDefinition[];
|
||||
modelSelectorProviders?: ModelSelectorProvider[];
|
||||
modelSelectorProviders?: ProviderSelectorProvider[];
|
||||
favoriteKeys?: Set<string>;
|
||||
onToggleFavoriteModel?: (provider: string, modelId: string) => void;
|
||||
features?: AgentFeature[];
|
||||
@@ -117,7 +117,7 @@ export interface DraftAgentStatusBarProps {
|
||||
selectedModel: string;
|
||||
onSelectModel: (modelId: string) => void;
|
||||
isModelLoading: boolean;
|
||||
modelSelectorProviders: ModelSelectorProvider[];
|
||||
modelSelectorProviders: ProviderSelectorProvider[];
|
||||
isAllModelsLoading: boolean;
|
||||
onSelectProviderAndModel: (provider: AgentProvider, modelId: string) => void;
|
||||
thinkingOptions: NonNullable<AgentModelDefinition["thinkingOptions"]>;
|
||||
@@ -237,7 +237,7 @@ function toComboboxOptions(options: StatusOption[] | undefined): ComboboxOption[
|
||||
function buildFallbackModelSelectorProviders(
|
||||
provider: string,
|
||||
modelOptions: StatusOption[] | undefined,
|
||||
): ModelSelectorProvider[] {
|
||||
): ProviderSelectorProvider[] {
|
||||
if (!modelOptions || modelOptions.length === 0) {
|
||||
return [];
|
||||
}
|
||||
@@ -245,13 +245,16 @@ function buildFallbackModelSelectorProviders(
|
||||
{
|
||||
id: provider,
|
||||
label: provider,
|
||||
rows: modelOptions.map((option) => ({
|
||||
favoriteKey: buildFavoriteModelKey({ provider, modelId: option.id }),
|
||||
provider,
|
||||
providerLabel: provider,
|
||||
modelId: option.id,
|
||||
modelLabel: option.label,
|
||||
})),
|
||||
modelSelection: {
|
||||
kind: "models",
|
||||
rows: modelOptions.map((option) => ({
|
||||
favoriteKey: buildFavoriteModelKey({ provider, modelId: option.id }),
|
||||
provider,
|
||||
providerLabel: provider,
|
||||
modelId: option.id,
|
||||
modelLabel: option.label,
|
||||
})),
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -794,7 +797,7 @@ interface DesktopStatusBarContentProps {
|
||||
canSelectMode: boolean;
|
||||
canSelectModel: boolean;
|
||||
canSelectThinking: boolean;
|
||||
modelSelectorProviders: ModelSelectorProvider[];
|
||||
modelSelectorProviders: ProviderSelectorProvider[];
|
||||
modelDisabled: boolean;
|
||||
comboboxProviderOptions: ComboboxOption[];
|
||||
comboboxModeOptions: ComboboxOption[];
|
||||
@@ -1058,7 +1061,7 @@ interface SheetStatusBarContentProps {
|
||||
canSelectMode: boolean;
|
||||
canSelectModel: boolean;
|
||||
canSelectThinking: boolean;
|
||||
modelSelectorProviders: ModelSelectorProvider[];
|
||||
modelSelectorProviders: ProviderSelectorProvider[];
|
||||
modelDisabled: boolean;
|
||||
comboboxModeOptions: ComboboxOption[];
|
||||
comboboxThinkingOptions: ComboboxOption[];
|
||||
@@ -1662,7 +1665,11 @@ export const AgentStatusBar = memo(function AgentStatusBar({
|
||||
[agent?.provider, models],
|
||||
);
|
||||
const agentModelSelectorProviders = useMemo(
|
||||
() => buildModelSelectorProviders(agentProviderDefinitions, agentProviderModels),
|
||||
() =>
|
||||
buildProviderSelectorProviders({
|
||||
providerDefinitions: agentProviderDefinitions,
|
||||
modelsByProvider: agentProviderModels,
|
||||
}),
|
||||
[agentProviderDefinitions, agentProviderModels],
|
||||
);
|
||||
|
||||
|
||||
@@ -1,183 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type {
|
||||
AgentModelDefinition,
|
||||
ProviderSnapshotEntry,
|
||||
} from "@server/server/agent/agent-sdk-types";
|
||||
import type { AgentProviderDefinition } from "@server/server/agent/provider-manifest";
|
||||
import {
|
||||
buildModelSelectorProviders,
|
||||
buildSelectableModelSelectorProviders,
|
||||
buildSelectedTriggerLabel,
|
||||
filterAndRankModelRows,
|
||||
matchesSearch,
|
||||
} from "./combined-model-selector.utils";
|
||||
|
||||
describe("combined model selector data", () => {
|
||||
const codexModel: AgentModelDefinition = {
|
||||
provider: "codex",
|
||||
id: "gpt-5.4",
|
||||
label: "GPT-5.4",
|
||||
};
|
||||
|
||||
function snapshotEntry(
|
||||
overrides: Partial<ProviderSnapshotEntry> & Pick<ProviderSnapshotEntry, "provider">,
|
||||
): ProviderSnapshotEntry {
|
||||
return {
|
||||
...overrides,
|
||||
provider: overrides.provider,
|
||||
status: overrides.status ?? "ready",
|
||||
enabled: overrides.enabled ?? true,
|
||||
label: overrides.label ?? overrides.provider,
|
||||
description: overrides.description ?? `${overrides.provider} provider`,
|
||||
defaultModeId: overrides.defaultModeId ?? "default",
|
||||
modes: overrides.modes ?? [],
|
||||
models: overrides.models ?? [codexModel],
|
||||
};
|
||||
}
|
||||
|
||||
it("builds selector providers from ready enabled snapshot entries", () => {
|
||||
expect(
|
||||
buildSelectableModelSelectorProviders([
|
||||
snapshotEntry({
|
||||
provider: "codex",
|
||||
label: "Codex",
|
||||
models: [codexModel],
|
||||
}),
|
||||
]),
|
||||
).toEqual([
|
||||
{
|
||||
id: "codex",
|
||||
label: "Codex",
|
||||
rows: [
|
||||
{
|
||||
favoriteKey: "codex:gpt-5.4",
|
||||
provider: "codex",
|
||||
providerLabel: "Codex",
|
||||
modelId: "gpt-5.4",
|
||||
modelLabel: "GPT-5.4",
|
||||
description: undefined,
|
||||
isDefault: undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps ready enabled providers with no models as model-less providers", () => {
|
||||
expect(
|
||||
buildSelectableModelSelectorProviders([
|
||||
snapshotEntry({
|
||||
provider: "deepseek-tui",
|
||||
label: "DeepSeek TUI",
|
||||
models: [],
|
||||
}),
|
||||
]),
|
||||
).toEqual([
|
||||
{
|
||||
id: "deepseek-tui",
|
||||
label: "DeepSeek TUI",
|
||||
rows: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("excludes disabled providers from selector data", () => {
|
||||
expect(
|
||||
buildSelectableModelSelectorProviders([
|
||||
snapshotEntry({
|
||||
provider: "deepseek-tui",
|
||||
label: "DeepSeek TUI",
|
||||
enabled: false,
|
||||
models: [],
|
||||
}),
|
||||
]),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("excludes providers that are not ready", () => {
|
||||
expect(
|
||||
buildSelectableModelSelectorProviders([
|
||||
snapshotEntry({ provider: "loading-provider", status: "loading", models: [] }),
|
||||
snapshotEntry({ provider: "error-provider", status: "error", models: [] }),
|
||||
snapshotEntry({ provider: "unavailable-provider", status: "unavailable", models: [] }),
|
||||
]),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("builds selector providers from an already-curated provider list", () => {
|
||||
const providerDefinitions: AgentProviderDefinition[] = [
|
||||
{
|
||||
id: "codex",
|
||||
label: "Codex",
|
||||
description: "Codex provider",
|
||||
defaultModeId: "auto",
|
||||
modes: [],
|
||||
},
|
||||
];
|
||||
|
||||
expect(
|
||||
buildModelSelectorProviders(providerDefinitions, new Map([["codex", [codexModel]]])),
|
||||
).toEqual([
|
||||
{
|
||||
id: "codex",
|
||||
label: "Codex",
|
||||
rows: [
|
||||
expect.objectContaining({
|
||||
provider: "codex",
|
||||
providerLabel: "Codex",
|
||||
modelId: "gpt-5.4",
|
||||
modelLabel: "GPT-5.4",
|
||||
}),
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("matches across label, provider, and description with multi-token fuzzy search", () => {
|
||||
const row = {
|
||||
favoriteKey: "opencode:opencode-zen/kimi-k2.5",
|
||||
provider: "opencode",
|
||||
providerLabel: "OpenCode",
|
||||
modelId: "opencode-zen/kimi-k2.5",
|
||||
modelLabel: "Kimi K2.5",
|
||||
description: "OpenCode Zen - kimi",
|
||||
};
|
||||
|
||||
expect(matchesSearch(row, "kimi zen")).toBe(true);
|
||||
expect(matchesSearch(row, "zen kimi")).toBe(true);
|
||||
expect(matchesSearch(row, "k2.5 zen")).toBe(true);
|
||||
expect(matchesSearch(row, "kimi gemini")).toBe(false);
|
||||
});
|
||||
|
||||
it("ranks model search results by fuzzy match quality", () => {
|
||||
const rows = [
|
||||
{
|
||||
favoriteKey: "openai:gpt-4.1",
|
||||
provider: "openai",
|
||||
providerLabel: "OpenAI",
|
||||
modelId: "gpt-4.1",
|
||||
modelLabel: "GPT-4.1",
|
||||
},
|
||||
{
|
||||
favoriteKey: "openai:gpt-5.4",
|
||||
provider: "openai",
|
||||
providerLabel: "OpenAI",
|
||||
modelId: "gpt-5.4",
|
||||
modelLabel: "GPT-5.4",
|
||||
},
|
||||
{
|
||||
favoriteKey: "google:gemini",
|
||||
provider: "google",
|
||||
providerLabel: "Google",
|
||||
modelId: "gemini",
|
||||
modelLabel: "Gemini",
|
||||
},
|
||||
];
|
||||
|
||||
expect(filterAndRankModelRows(rows, "gpt54").map((row) => row.modelId)).toEqual(["gpt-5.4"]);
|
||||
});
|
||||
|
||||
it("keeps the selected trigger label model-only", () => {
|
||||
expect(buildSelectedTriggerLabel("GPT-5.4")).toBe("GPT-5.4");
|
||||
});
|
||||
});
|
||||
@@ -47,9 +47,13 @@ import { getProviderIcon } from "@/components/provider-icons";
|
||||
import {
|
||||
buildSelectedTriggerLabel,
|
||||
filterAndRankModelRows,
|
||||
type ModelSelectorProvider,
|
||||
type SelectorModelRow,
|
||||
} from "./combined-model-selector.utils";
|
||||
getAllProviderModelRows,
|
||||
getProviderDefaultLabel,
|
||||
getProviderModelRows,
|
||||
resolveSelectedModelLabel,
|
||||
type ProviderSelectionModelRow,
|
||||
type ProviderSelectorProvider,
|
||||
} from "@/provider-selection/provider-selection";
|
||||
|
||||
// TODO: this should be configured per provider in the provider manifest
|
||||
const PROVIDERS_WITH_MODEL_DESCRIPTIONS = new Set(["opencode", "pi"]);
|
||||
@@ -59,7 +63,7 @@ type SelectorView =
|
||||
| { kind: "provider"; providerId: string; providerLabel: string };
|
||||
|
||||
interface CombinedModelSelectorProps {
|
||||
providers: ModelSelectorProvider[];
|
||||
providers: ProviderSelectorProvider[];
|
||||
selectedProvider: string;
|
||||
selectedModel: string;
|
||||
onSelect: (provider: AgentProvider, modelId: string) => void;
|
||||
@@ -79,7 +83,7 @@ interface CombinedModelSelectorProps {
|
||||
|
||||
interface SelectorContentProps {
|
||||
view: SelectorView;
|
||||
providers: ModelSelectorProvider[];
|
||||
providers: ProviderSelectorProvider[];
|
||||
selectedProvider: string;
|
||||
selectedModel: string;
|
||||
searchQuery: string;
|
||||
@@ -89,23 +93,16 @@ interface SelectorContentProps {
|
||||
onDrillDown: (providerId: string, providerLabel: string) => void;
|
||||
}
|
||||
|
||||
function resolveDefaultModelLabel(rows: SelectorModelRow[] | undefined): string {
|
||||
if (!rows || rows.length === 0) {
|
||||
return "Select model";
|
||||
}
|
||||
return (rows.find((row) => row.isDefault) ?? rows[0])?.modelLabel ?? "Select model";
|
||||
}
|
||||
|
||||
function normalizeSearchQuery(value: string): string {
|
||||
return value.trim().toLowerCase();
|
||||
}
|
||||
|
||||
function sortFavoritesFirst(
|
||||
rows: SelectorModelRow[],
|
||||
rows: ProviderSelectionModelRow[],
|
||||
favoriteKeys: Set<string>,
|
||||
): SelectorModelRow[] {
|
||||
const favorites: SelectorModelRow[] = [];
|
||||
const rest: SelectorModelRow[] = [];
|
||||
): ProviderSelectionModelRow[] {
|
||||
const favorites: ProviderSelectionModelRow[] = [];
|
||||
const rest: ProviderSelectionModelRow[] = [];
|
||||
for (const row of rows) {
|
||||
if (favoriteKeys.has(row.favoriteKey)) {
|
||||
favorites.push(row);
|
||||
@@ -124,7 +121,7 @@ function ModelRow({
|
||||
onPress,
|
||||
onToggleFavorite,
|
||||
}: {
|
||||
row: SelectorModelRow;
|
||||
row: ProviderSelectionModelRow;
|
||||
isSelected: boolean;
|
||||
isFavorite: boolean;
|
||||
elevated?: boolean;
|
||||
@@ -200,7 +197,7 @@ function ModelRow({
|
||||
}
|
||||
|
||||
interface SelectableModelRowProps {
|
||||
row: SelectorModelRow;
|
||||
row: ProviderSelectionModelRow;
|
||||
isSelected: boolean;
|
||||
isFavorite: boolean;
|
||||
elevated?: boolean;
|
||||
@@ -239,7 +236,7 @@ function FavoritesSection({
|
||||
onSelect,
|
||||
onToggleFavorite,
|
||||
}: {
|
||||
favoriteRows: SelectorModelRow[];
|
||||
favoriteRows: ProviderSelectionModelRow[];
|
||||
selectedProvider: string;
|
||||
selectedModel: string;
|
||||
favoriteKeys: Set<string>;
|
||||
@@ -274,7 +271,7 @@ interface GroupProviderButtonProps {
|
||||
providerId: string;
|
||||
providerLabel: string;
|
||||
rowCount: number;
|
||||
hasNoModels: boolean;
|
||||
defaultLabel: string | null;
|
||||
onDrillDown: (providerId: string, providerLabel: string) => void;
|
||||
onSelectDefault: (providerId: string) => void;
|
||||
}
|
||||
@@ -283,28 +280,28 @@ function GroupProviderButton({
|
||||
providerId,
|
||||
providerLabel,
|
||||
rowCount,
|
||||
hasNoModels,
|
||||
defaultLabel,
|
||||
onDrillDown,
|
||||
onSelectDefault,
|
||||
}: GroupProviderButtonProps) {
|
||||
const { theme } = useUnistyles();
|
||||
const ProvIcon = getProviderIcon(providerId);
|
||||
const handlePress = useCallback(() => {
|
||||
if (hasNoModels) {
|
||||
if (defaultLabel) {
|
||||
onSelectDefault(providerId);
|
||||
return;
|
||||
}
|
||||
onDrillDown(providerId, providerLabel);
|
||||
}, [hasNoModels, onDrillDown, onSelectDefault, providerId, providerLabel]);
|
||||
}, [defaultLabel, onDrillDown, onSelectDefault, providerId, providerLabel]);
|
||||
return (
|
||||
<Pressable onPress={handlePress} style={drillDownRowStyle}>
|
||||
<ProvIcon size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
||||
<Text style={styles.drillDownText}>{providerLabel}</Text>
|
||||
<View style={styles.drillDownTrailing}>
|
||||
<Text style={styles.drillDownCount}>
|
||||
{hasNoModels ? "Default" : `${rowCount} ${rowCount === 1 ? "model" : "models"}`}
|
||||
{defaultLabel ?? `${rowCount} ${rowCount === 1 ? "model" : "models"}`}
|
||||
</Text>
|
||||
{hasNoModels ? null : (
|
||||
{defaultLabel ? null : (
|
||||
<ChevronRight size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
||||
)}
|
||||
</View>
|
||||
@@ -317,22 +314,23 @@ function GroupedProviderRows({
|
||||
onDrillDown,
|
||||
onSelectDefault,
|
||||
}: {
|
||||
providers: ModelSelectorProvider[];
|
||||
providers: ProviderSelectorProvider[];
|
||||
onDrillDown: (providerId: string, providerLabel: string) => void;
|
||||
onSelectDefault: (providerId: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<View>
|
||||
{providers.map((provider, index) => {
|
||||
const hasNoModels = provider.rows.length === 0;
|
||||
const rows = getProviderModelRows(provider);
|
||||
const defaultLabel = getProviderDefaultLabel(provider);
|
||||
return (
|
||||
<View key={provider.id}>
|
||||
{index > 0 ? <View style={styles.separator} /> : null}
|
||||
<GroupProviderButton
|
||||
providerId={provider.id}
|
||||
providerLabel={provider.label}
|
||||
rowCount={provider.rows.length}
|
||||
hasNoModels={hasNoModels}
|
||||
rowCount={rows.length}
|
||||
defaultLabel={defaultLabel}
|
||||
onDrillDown={onDrillDown}
|
||||
onSelectDefault={onSelectDefault}
|
||||
/>
|
||||
@@ -381,7 +379,7 @@ function ProviderModelRows({
|
||||
onToggleFavorite,
|
||||
normalizedQuery,
|
||||
}: {
|
||||
rows: SelectorModelRow[];
|
||||
rows: ProviderSelectionModelRow[];
|
||||
selectedProvider: string;
|
||||
selectedModel: string;
|
||||
favoriteKeys: Set<string>;
|
||||
@@ -396,7 +394,7 @@ function ProviderModelRows({
|
||||
[favoriteKeys, normalizedQuery, rows],
|
||||
);
|
||||
const renderItem = useCallback(
|
||||
({ item }: { item: SelectorModelRow }) => (
|
||||
({ item }: { item: ProviderSelectionModelRow }) => (
|
||||
<SelectableModelRow
|
||||
row={item}
|
||||
isSelected={item.provider === selectedProvider && item.modelId === selectedModel}
|
||||
@@ -407,7 +405,7 @@ function ProviderModelRows({
|
||||
),
|
||||
[favoriteKeys, onSelect, onToggleFavorite, selectedModel, selectedProvider],
|
||||
);
|
||||
const keyExtractor = useCallback((row: SelectorModelRow) => row.favoriteKey, []);
|
||||
const keyExtractor = useCallback((row: ProviderSelectionModelRow) => row.favoriteKey, []);
|
||||
|
||||
if (useVirtualizedList) {
|
||||
return (
|
||||
@@ -455,15 +453,12 @@ function SelectorContent({
|
||||
const visibleRows = useMemo(
|
||||
() =>
|
||||
selectedViewProvider
|
||||
? filterAndRankModelRows(selectedViewProvider.rows, normalizedQuery)
|
||||
? filterAndRankModelRows(getProviderModelRows(selectedViewProvider), normalizedQuery)
|
||||
: [],
|
||||
[normalizedQuery, selectedViewProvider],
|
||||
);
|
||||
const favoriteRows = useMemo(
|
||||
() =>
|
||||
providers.flatMap((provider) =>
|
||||
provider.rows.filter((row) => favoriteKeys.has(row.favoriteKey)),
|
||||
),
|
||||
() => getAllProviderModelRows(providers).filter((row) => favoriteKeys.has(row.favoriteKey)),
|
||||
[favoriteKeys, providers],
|
||||
);
|
||||
const handleSelectDefaultProvider = useCallback(
|
||||
@@ -485,7 +480,7 @@ function SelectorContent({
|
||||
return emptyState;
|
||||
}
|
||||
|
||||
if (selectedViewProvider.rows.length === 0 && !normalizedQuery) {
|
||||
if (getProviderDefaultLabel(selectedViewProvider) && !normalizedQuery) {
|
||||
return (
|
||||
<DefaultProviderRow
|
||||
providerId={view.providerId}
|
||||
@@ -607,30 +602,20 @@ export function CombinedModelSelector({
|
||||
const ProviderIcon = hasSelectedProvider ? getProviderIcon(selectedProvider) : null;
|
||||
|
||||
const selectedModelLabel = useMemo(() => {
|
||||
if (!selectedModel) {
|
||||
if (!hasSelectedProvider) {
|
||||
return "Select model";
|
||||
}
|
||||
const provider = providers.find((entry) => entry.id === selectedProvider);
|
||||
if (provider?.rows.length === 0) {
|
||||
return "Default";
|
||||
}
|
||||
return isLoading ? "Loading..." : "Select model";
|
||||
}
|
||||
const provider = providers.find((entry) => entry.id === selectedProvider);
|
||||
if (!provider) {
|
||||
return isLoading ? "Loading..." : "Select model";
|
||||
}
|
||||
const model = provider.rows.find((entry) => entry.modelId === selectedModel);
|
||||
return model?.modelLabel ?? resolveDefaultModelLabel(provider.rows);
|
||||
}, [hasSelectedProvider, isLoading, providers, selectedModel, selectedProvider]);
|
||||
return resolveSelectedModelLabel({
|
||||
providers,
|
||||
selectedProvider,
|
||||
selectedModel,
|
||||
isLoading,
|
||||
});
|
||||
}, [isLoading, providers, selectedModel, selectedProvider]);
|
||||
|
||||
const desktopFixedHeight = useMemo(() => {
|
||||
if (view.kind !== "provider") {
|
||||
return undefined;
|
||||
}
|
||||
const modelCount =
|
||||
providers.find((provider) => provider.id === view.providerId)?.rows.length ?? 0;
|
||||
const provider = providers.find((entry) => entry.id === view.providerId);
|
||||
const modelCount = provider ? getProviderModelRows(provider).length : 0;
|
||||
return Math.min(80 + modelCount * 40, 400);
|
||||
}, [providers, view]);
|
||||
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
import type {
|
||||
AgentModelDefinition,
|
||||
ProviderSnapshotEntry,
|
||||
} from "@server/server/agent/agent-sdk-types";
|
||||
import type { AgentProviderDefinition } from "@server/server/agent/provider-manifest";
|
||||
import { buildFavoriteModelKey, type FavoriteModelRow } from "@/hooks/use-form-preferences";
|
||||
import { compareMatchScores, scoreTextFields } from "@/utils/score-match";
|
||||
|
||||
export type SelectorModelRow = FavoriteModelRow & { isDefault?: boolean };
|
||||
|
||||
export interface ModelSelectorProvider {
|
||||
id: string;
|
||||
label: string;
|
||||
rows: SelectorModelRow[];
|
||||
}
|
||||
|
||||
export function buildSelectedTriggerLabel(modelLabel: string): string {
|
||||
return modelLabel;
|
||||
}
|
||||
|
||||
export function buildModelSelectorProviders(
|
||||
providerDefinitions: AgentProviderDefinition[],
|
||||
allProviderModels: Map<string, AgentModelDefinition[]>,
|
||||
): ModelSelectorProvider[] {
|
||||
return providerDefinitions.map((definition) => ({
|
||||
id: definition.id,
|
||||
label: definition.label,
|
||||
rows: (allProviderModels.get(definition.id) ?? []).map((model) => ({
|
||||
favoriteKey: buildFavoriteModelKey({ provider: definition.id, modelId: model.id }),
|
||||
provider: definition.id,
|
||||
providerLabel: definition.label,
|
||||
modelId: model.id,
|
||||
modelLabel: model.label,
|
||||
description: model.description,
|
||||
isDefault: model.isDefault,
|
||||
})),
|
||||
}));
|
||||
}
|
||||
|
||||
export function buildSelectableModelSelectorProviders(
|
||||
entries: ProviderSnapshotEntry[] | undefined,
|
||||
): ModelSelectorProvider[] {
|
||||
return (entries ?? [])
|
||||
.filter((entry) => entry.enabled && entry.status === "ready")
|
||||
.map((entry) => ({
|
||||
id: entry.provider,
|
||||
label: entry.label ?? entry.provider,
|
||||
rows: (entry.models ?? []).map((model) => ({
|
||||
favoriteKey: buildFavoriteModelKey({ provider: entry.provider, modelId: model.id }),
|
||||
provider: entry.provider,
|
||||
providerLabel: entry.label ?? entry.provider,
|
||||
modelId: model.id,
|
||||
modelLabel: model.label,
|
||||
description: model.description,
|
||||
isDefault: model.isDefault,
|
||||
})),
|
||||
}));
|
||||
}
|
||||
|
||||
export function matchesSearch(row: SelectorModelRow, normalizedQuery: string): boolean {
|
||||
return scoreModelRow(row, normalizedQuery) !== null;
|
||||
}
|
||||
|
||||
function getModelRowSearchFields(row: SelectorModelRow): string[] {
|
||||
return [row.modelLabel, row.modelId, row.providerLabel, row.description ?? ""];
|
||||
}
|
||||
|
||||
export function scoreModelRow(row: SelectorModelRow, normalizedQuery: string) {
|
||||
return scoreTextFields(normalizedQuery, getModelRowSearchFields(row));
|
||||
}
|
||||
|
||||
export function filterAndRankModelRows(
|
||||
rows: SelectorModelRow[],
|
||||
normalizedQuery: string,
|
||||
): SelectorModelRow[] {
|
||||
if (!normalizedQuery) return rows;
|
||||
const scored = rows
|
||||
.map((row) => ({ row, score: scoreModelRow(row, normalizedQuery) }))
|
||||
.filter((entry): entry is { row: SelectorModelRow; score: NonNullable<typeof entry.score> } =>
|
||||
Boolean(entry.score),
|
||||
);
|
||||
|
||||
scored.sort((a, b) => {
|
||||
const cmp = compareMatchScores(a.score, b.score);
|
||||
if (cmp !== 0) return cmp;
|
||||
return a.row.modelLabel.localeCompare(b.row.modelLabel);
|
||||
});
|
||||
|
||||
return scored.map((entry) => entry.row);
|
||||
}
|
||||
Reference in New Issue
Block a user