mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-23 05:34:41 +00:00
fix(chat): ignore response IDs from previous model selections
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
08b21713b3
commit
9530fbe2bf
2 changed files with 47 additions and 2 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { fireEvent, screen, waitFor, within } from "@testing-library/react";
|
||||
import { act, fireEvent, screen, waitFor, within } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { useChatHistory } from "@/components/chat/useChatHistory";
|
||||
import ChatConversationPage from "./page";
|
||||
|
|
@ -154,6 +154,44 @@ describe("/ui/chat request metrics", () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it.each([false, true])("ignores late response IDs after model changes ($0)", async (switchBack) => {
|
||||
vi.mocked(fetchAvailableModels).mockResolvedValue([
|
||||
{ model_group: "gpt-5.4-mini", providers: ["openai"] },
|
||||
{ model_group: "second-model", providers: ["openai"] },
|
||||
]);
|
||||
const firstCompletion = Promise.withResolvers<void>();
|
||||
mockMakeOpenAIResponsesRequest.mockImplementationOnce(async (...args: unknown[]) => {
|
||||
await firstCompletion.promise;
|
||||
(args[1] as (role: string, delta: string) => void)("assistant", "First answer");
|
||||
(args[15] as (id: string) => void)("response-first");
|
||||
});
|
||||
mockMakeOpenAIResponsesRequest.mockResolvedValue(undefined);
|
||||
|
||||
await sendOneMessage();
|
||||
fireEvent.click(screen.getByRole("button", { name: /gpt-5\.4-mini/ }));
|
||||
fireEvent.click(await screen.findByRole("button", { name: "second-model" }));
|
||||
if (switchBack) {
|
||||
fireEvent.click(screen.getByRole("button", { name: "second-model" }));
|
||||
fireEvent.click(await screen.findByRole("button", { name: "gpt-5.4-mini" }));
|
||||
}
|
||||
|
||||
await act(async () => firstCompletion.resolve());
|
||||
expect(await screen.findByText("First answer")).toBeVisible();
|
||||
fireEvent.change(screen.getByRole("textbox"), { target: { value: "Follow up" } });
|
||||
await waitFor(() => expect(screen.getByRole("button", { name: "Send" })).toBeEnabled());
|
||||
fireEvent.click(screen.getByRole("button", { name: "Send" }));
|
||||
await waitFor(() => expect(mockMakeOpenAIResponsesRequest).toHaveBeenCalledTimes(2));
|
||||
|
||||
const second = mockMakeOpenAIResponsesRequest.mock.calls[1];
|
||||
expect(second[2]).toBe(switchBack ? "gpt-5.4-mini" : "second-model");
|
||||
expect(second[14]).toBeNull();
|
||||
expect(second[0]).toEqual([
|
||||
{ role: "user", content: "How much did this cost?" },
|
||||
{ role: "assistant", content: "First answer" },
|
||||
{ role: "user", content: "Follow up" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("renders latency, TTFT, token counts and cost reported for the assistant turn", async () => {
|
||||
mockMakeOpenAIResponsesRequest.mockImplementation(async (...args: unknown[]) => {
|
||||
const updateTextUI = args[1] as (role: string, delta: string) => void;
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ export default function ChatConversationPage() {
|
|||
const [modelSearchText, setModelSearchText] = useState("");
|
||||
|
||||
const [responsesSessionId, setResponsesSessionId] = useState<string | null>(null);
|
||||
const responsesSessionGenerationRef = useRef(0);
|
||||
const [prevConversationIdForSessionReset, setPrevConversationIdForSessionReset] = useState(activeConversationId);
|
||||
const [isStreaming, setIsStreaming] = useState(false);
|
||||
const [inputText, setInputText] = useState("");
|
||||
|
|
@ -129,6 +130,7 @@ export default function ChatConversationPage() {
|
|||
}
|
||||
|
||||
const selectModel = useCallback((model: string) => {
|
||||
responsesSessionGenerationRef.current += 1;
|
||||
setSelectedModel(model);
|
||||
setResponsesSessionId(null);
|
||||
localStorage.setItem(LOCALSTORAGE_MODEL_KEY, model);
|
||||
|
|
@ -141,6 +143,7 @@ export default function ChatConversationPage() {
|
|||
const trimmed = text.trim();
|
||||
if (!trimmed || !selectedModel || isStreaming) return;
|
||||
const model = selectedModel;
|
||||
const sessionGeneration = responsesSessionGenerationRef.current;
|
||||
setInputText("");
|
||||
|
||||
let convId = activeConversationId;
|
||||
|
|
@ -213,7 +216,11 @@ export default function ChatConversationPage() {
|
|||
undefined,
|
||||
selectedMCPServers.length > 0 ? selectedMCPServers : undefined,
|
||||
previousResponseId,
|
||||
(id: string) => setResponsesSessionId(id),
|
||||
(id: string) => {
|
||||
if (responsesSessionGenerationRef.current === sessionGeneration) {
|
||||
setResponsesSessionId(id);
|
||||
}
|
||||
},
|
||||
(event: MCPEvent) => {
|
||||
// Accumulate locally only — persisted once in finally to avoid
|
||||
// one full localStorage write per MCP event during streaming.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue