fix(ui): restore playground model filtering by endpoint

Bring back the prior Chat model dropdown filter (including chat models
on responses/anthropic/interactions and image models on image_edits), and
map mode realtime so the realtime endpoint only lists compatible models
This commit is contained in:
mubashir1osmani 2026-08-06 14:40:16 -07:00
parent 5102b9c0d8
commit 4b2781ccb3
5 changed files with 163 additions and 42 deletions

View file

@ -117,6 +117,74 @@ describe("ChatUI", () => {
});
});
it("shows only endpoint-compatible models when chat endpoint is selected", async () => {
(fetchModelsModule.fetchAvailableModels as ReturnType<typeof vi.fn>).mockResolvedValueOnce([
{ model_group: "ChatModel", mode: "chat" },
{ model_group: "SpeechModel", mode: "audio_speech" },
{ model_group: "ImageModel", mode: "image_generation" },
{ model_group: "ResponsesModel", mode: "responses" },
{ model_group: "RealtimeModel", mode: "realtime" },
{ model_group: "NoModeModel" },
]);
render(
<ChatUI
accessToken="1234567890"
token="1234567890"
userRole="user"
userID="1234567890"
disabledPersonalKeyCreation={false}
/>,
);
await waitFor(() => {
expect(screen.getByText("Test Key")).toBeInTheDocument();
});
await selectComboboxOption("Select an endpoint", "/v1/chat/completions");
await openComboboxByPlaceholder("Select a Model");
await waitFor(() => {
expect(screen.getAllByText("ChatModel").length).toBeGreaterThan(0);
expect(screen.getAllByText("NoModeModel").length).toBeGreaterThan(0);
expect(screen.queryByText("SpeechModel")).toBeNull();
expect(screen.queryByText("ImageModel")).toBeNull();
expect(screen.queryByText("ResponsesModel")).toBeNull();
expect(screen.queryByText("RealtimeModel")).toBeNull();
});
});
it("shows only realtime models when realtime endpoint is selected", async () => {
(fetchModelsModule.fetchAvailableModels as ReturnType<typeof vi.fn>).mockResolvedValueOnce([
{ model_group: "ChatModel", mode: "chat" },
{ model_group: "RealtimeModel", mode: "realtime" },
{ model_group: "NoModeModel" },
]);
render(
<ChatUI
accessToken="1234567890"
token="1234567890"
userRole="user"
userID="1234567890"
disabledPersonalKeyCreation={false}
/>,
);
await waitFor(() => {
expect(screen.getByText("Test Key")).toBeInTheDocument();
});
await selectComboboxOption("Select an endpoint", "/v1/realtime");
await openComboboxByPlaceholder("Select a Model");
await waitFor(() => {
expect(screen.getAllByText("RealtimeModel").length).toBeGreaterThan(0);
expect(screen.getAllByText("NoModeModel").length).toBeGreaterThan(0);
expect(screen.queryByText("ChatModel")).toBeNull();
});
});
it("should show 'Enter custom model' option in model selector", async () => {
render(
<ChatUI

View file

@ -52,6 +52,7 @@ import { createChatDisplayMessage, createChatMultimodalMessage } from "./ChatIma
import CodeInterpreterTool from "./CodeInterpreterTool";
import { generateCodeSnippet } from "@/components/chat_ui/CodeSnippets";
import EndpointSelector from "./EndpointSelector";
import { filterModelsForEndpoint } from "./EndpointUtils";
import FilePreviewCard from "./FilePreviewCard";
import ChatMessageBubble from "./ChatMessageBubble";
import MCPEventsDisplay from "@/components/chat_ui/MCPEventsDisplay";
@ -1141,11 +1142,17 @@ const ChatUI: React.FC<ChatUIProps> = ({
};
const supportsStreamingToggle = endpointType === EndpointType.CHAT || endpointType === EndpointType.RESPONSES;
const modelsForEndpoint = useMemo(
() => filterModelsForEndpoint(modelInfo, endpointType as EndpointType),
[modelInfo, endpointType],
);
let modelEmptyText = "No models available for this key";
if (modelLoadError) {
modelEmptyText = "Unable to load models for this key";
} else if (apiKeySource === "custom" && !apiKey.trim()) {
modelEmptyText = "Enter a Virtual Key to load models";
} else if (modelInfo.length > 0 && modelsForEndpoint.length === 0) {
modelEmptyText = "No models available for this endpoint";
}
const inputPlaceholder =
@ -1394,7 +1401,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
onValueChange={onModelChange}
options={[
{ value: "custom", label: "Enter custom model" },
...modelInfo.map((model) => ({
...modelsForEndpoint.map((model) => ({
value: model.model_group,
label: model.model_group,
sublabel: model.mode ? `Mode: ${model.mode}` : undefined,

View file

@ -1,37 +1,16 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { ModelGroup } from "@/components/llm_calls/fetch_models";
import { determineEndpointType } from "./EndpointUtils";
import { determineEndpointType, filterModelsForEndpoint, isModelCompatibleWithEndpoint } from "./EndpointUtils";
import { EndpointType } from "@/components/chat_ui/mode_endpoint_mapping";
// Mock the getEndpointType function
vi.mock("@/components/chat_ui/mode_endpoint_mapping", () => ({
EndpointType: {
IMAGE: "image",
VIDEO: "video",
CHAT: "chat",
RESPONSES: "responses",
IMAGE_EDITS: "image_edits",
ANTHROPIC_MESSAGES: "anthropic_messages",
EMBEDDINGS: "embeddings",
SPEECH: "speech",
TRANSCRIPTION: "transcription",
A2A_AGENTS: "a2a_agents",
},
getEndpointType: vi.fn(),
ModelMode: {
AUDIO_SPEECH: "audio_speech",
AUDIO_TRANSCRIPTION: "audio_transcription",
IMAGE_GENERATION: "image_generation",
VIDEO_GENERATION: "video_generation",
CHAT: "chat",
RESPONSES: "responses",
IMAGE_EDITS: "image_edits",
ANTHROPIC_MESSAGES: "anthropic_messages",
EMBEDDING: "embedding",
},
}));
vi.mock("@/components/chat_ui/mode_endpoint_mapping", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/components/chat_ui/mode_endpoint_mapping")>();
return {
...actual,
getEndpointType: vi.fn(actual.getEndpointType),
};
});
// Import the mocked function
import { getEndpointType } from "@/components/chat_ui/mode_endpoint_mapping";
describe("determineEndpointType", () => {
@ -210,10 +189,61 @@ describe("determineEndpointType", () => {
vi.mocked(getEndpointType).mockReturnValue(EndpointType.CHAT);
// Test with different case - should not match
const result = determineEndpointType("gpt-3.5-turbo", mockModelInfo);
expect(getEndpointType).not.toHaveBeenCalled();
expect(result).toBe(EndpointType.CHAT);
});
});
describe("isModelCompatibleWithEndpoint / filterModelsForEndpoint", () => {
beforeEach(() => {
vi.mocked(getEndpointType).mockImplementation((mode: string) => {
const map: Record<string, EndpointType> = {
chat: EndpointType.CHAT,
responses: EndpointType.RESPONSES,
image_generation: EndpointType.IMAGE,
image_edits: EndpointType.IMAGE_EDITS,
audio_speech: EndpointType.SPEECH,
realtime: EndpointType.REALTIME,
embedding: EndpointType.EMBEDDINGS,
};
return map[mode] ?? EndpointType.CHAT;
});
});
it("keeps models with no mode for every endpoint", () => {
const model: ModelGroup = { model_group: "custom-proxy-model" };
expect(isModelCompatibleWithEndpoint(model, EndpointType.CHAT)).toBe(true);
expect(isModelCompatibleWithEndpoint(model, EndpointType.REALTIME)).toBe(true);
expect(isModelCompatibleWithEndpoint(model, EndpointType.SPEECH)).toBe(true);
});
it("keeps chat models for responses, anthropic messages, and interactions", () => {
const chatModel: ModelGroup = { model_group: "gpt-4o", mode: "chat" };
expect(isModelCompatibleWithEndpoint(chatModel, EndpointType.RESPONSES)).toBe(true);
expect(isModelCompatibleWithEndpoint(chatModel, EndpointType.ANTHROPIC_MESSAGES)).toBe(true);
expect(isModelCompatibleWithEndpoint(chatModel, EndpointType.INTERACTIONS)).toBe(true);
expect(isModelCompatibleWithEndpoint(chatModel, EndpointType.SPEECH)).toBe(false);
});
it("keeps image models for image_edits", () => {
const imageModel: ModelGroup = { model_group: "dall-e-3", mode: "image_generation" };
expect(isModelCompatibleWithEndpoint(imageModel, EndpointType.IMAGE_EDITS)).toBe(true);
expect(isModelCompatibleWithEndpoint(imageModel, EndpointType.IMAGE)).toBe(true);
expect(isModelCompatibleWithEndpoint(imageModel, EndpointType.CHAT)).toBe(false);
});
it("keeps only realtime models for the realtime endpoint", () => {
const models: ModelGroup[] = [
{ model_group: "gpt-4o", mode: "chat" },
{ model_group: "gpt-realtime", mode: "realtime" },
{ model_group: "no-mode" },
];
expect(filterModelsForEndpoint(models, EndpointType.REALTIME).map((m) => m.model_group)).toEqual([
"gpt-realtime",
"no-mode",
]);
});
});

View file

@ -1,22 +1,37 @@
import { ModelGroup } from "@/components/llm_calls/fetch_models";
import { EndpointType, getEndpointType } from "@/components/chat_ui/mode_endpoint_mapping";
/**
* Determines the appropriate endpoint type based on the selected model
*
* @param selectedModel - The model identifier string
* @param modelInfo - Array of model information
* @returns The appropriate endpoint type
*/
export const determineEndpointType = (selectedModel: string, modelInfo: ModelGroup[]): EndpointType => {
// Find the model information for the selected model
const selectedModelInfo = modelInfo.find((option) => option.model_group === selectedModel);
// If model info is found and it has a mode, determine the endpoint type
if (selectedModelInfo?.mode) {
return getEndpointType(selectedModelInfo.mode);
}
// Default to chat endpoint if no match is found
return EndpointType.CHAT;
};
export const isModelCompatibleWithEndpoint = (model: ModelGroup, endpointType: EndpointType): boolean => {
if (!model.mode) {
return true;
}
const optionEndpoint = getEndpointType(model.mode);
if (
endpointType === EndpointType.RESPONSES ||
endpointType === EndpointType.ANTHROPIC_MESSAGES ||
endpointType === EndpointType.INTERACTIONS
) {
return optionEndpoint === endpointType || optionEndpoint === EndpointType.CHAT;
}
if (endpointType === EndpointType.IMAGE_EDITS) {
return optionEndpoint === endpointType || optionEndpoint === EndpointType.IMAGE;
}
return optionEndpoint === endpointType;
};
export const filterModelsForEndpoint = (models: ModelGroup[], endpointType: EndpointType): ModelGroup[] =>
models.filter((model) => isModelCompatibleWithEndpoint(model, endpointType));

View file

@ -11,7 +11,7 @@ export enum ModelMode {
IMAGE_EDITS = "image_edits",
ANTHROPIC_MESSAGES = "anthropic_messages",
EMBEDDING = "embedding",
// add additional modes as needed
REALTIME = "realtime",
}
// Define an enum for the endpoint types your UI calls
@ -42,6 +42,7 @@ export const litellmModeMapping: Record<ModelMode, EndpointType> = {
[ModelMode.AUDIO_SPEECH]: EndpointType.SPEECH,
[ModelMode.AUDIO_TRANSCRIPTION]: EndpointType.TRANSCRIPTION,
[ModelMode.EMBEDDING]: EndpointType.EMBEDDINGS,
[ModelMode.REALTIME]: EndpointType.REALTIME,
};
export const getEndpointType = (mode: string): EndpointType => {