From e3272da8485e512192a73ce4767abee46bd32d95 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Thu, 5 Nov 2026 16:51:27 -0800 Subject: [PATCH] fix(ui): keep chat text sent back to the model in English (#50304) The /chat request errors and deep research notice, and Aria's step limit and no answer replies, are written into assistant messages that are sent to the model as history on the next turn. Pin them to English with i18n.getFixedT("en") so a future UI language never changes the model payload. English output is unchanged. --- .../components/chat/runChatRequest.test.ts | 66 ++++++++++++++++++- .../src/components/chat/runChatRequest.ts | 6 +- .../src/components/liteadmin/agent.test.ts | 37 +++++++++++ .../src/components/liteadmin/agent.ts | 4 +- 4 files changed, 107 insertions(+), 6 deletions(-) diff --git a/ui/litellm-dashboard/src/components/chat/runChatRequest.test.ts b/ui/litellm-dashboard/src/components/chat/runChatRequest.test.ts index 60ab8772112..aca9364c52b 100644 --- a/ui/litellm-dashboard/src/components/chat/runChatRequest.test.ts +++ b/ui/litellm-dashboard/src/components/chat/runChatRequest.test.ts @@ -1,5 +1,6 @@ import OpenAI from "openai"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import i18n from "@/i18n/i18n"; import { makeOpenAIResponsesRequest } from "@/components/llm_calls/responses_api"; import { generateChatImages } from "./imageHelpers"; import { toResponsesMessages } from "./attachmentHelpers"; @@ -192,4 +193,67 @@ describe("runChatRequest", () => { expect(options.onUpdate).toHaveBeenCalledExactlyOnceWith({ content: " [stopped]" }); expect(options.onResponseId).toHaveBeenCalledWith(null); }); + + describe("when the UI language is not English", () => { + beforeEach(async () => { + i18n.addResourceBundle( + "de", + "translation", + { + chat: { + runChatRequest: { genericError: "Fehler", fileInputHint: " Hinweis", researchNeedsBackground: "Hinweis" }, + }, + }, + true, + true, + ); + await i18n.changeLanguage("de"); + }); + + afterEach(async () => { + await i18n.changeLanguage("en"); + i18n.removeResourceBundle("de", "translation"); + }); + + it("should keep the generic error in English because it is sent back to the model", async () => { + const options = requestOptions(); + vi.mocked(makeOpenAIResponsesRequest).mockRejectedValue("offline"); + await runChatRequest(options, i18n.t.bind(i18n)); + expect(options.onUpdate).toHaveBeenLastCalledWith({ content: "Something went wrong. Please try again." }); + }); + + it("should keep the file input hint in English because it is sent back to the model", async () => { + const options = requestOptions({ + messages: [ + { + role: "user", + content: "Read the report", + attachments: [ + { + id: "report", + kind: "file", + name: "report.pdf", + mimeType: "application/pdf", + url: "data:application/pdf;base64,JVBERg==", + }, + ], + }, + ], + }); + const error = new OpenAI.APIError(400, { message: "Provider detail" }, "Provider detail", new Headers()); + vi.mocked(makeOpenAIResponsesRequest).mockRejectedValue(error); + await runChatRequest(options, i18n.t.bind(i18n)); + expect(options.onUpdate).toHaveBeenLastCalledWith({ + content: error.message + " Try a model that supports file inputs, or paste the relevant text.", + }); + }); + + it("should keep the deep research notice in English because it is sent back to the model", async () => { + const options = requestOptions({ research: true }); + await runChatRequest(options, i18n.t.bind(i18n)); + expect(options.onUpdate).toHaveBeenCalledExactlyOnceWith({ + content: "Enable background chats before starting deep research.", + }); + }); + }); }); diff --git a/ui/litellm-dashboard/src/components/chat/runChatRequest.ts b/ui/litellm-dashboard/src/components/chat/runChatRequest.ts index 7758f33254d..dc30f7d68f6 100644 --- a/ui/litellm-dashboard/src/components/chat/runChatRequest.ts +++ b/ui/litellm-dashboard/src/components/chat/runChatRequest.ts @@ -31,8 +31,8 @@ function requestErrorMessage( error: unknown, messages: ChatRequestOptions["messages"], mode: ChatRequestOptions["mode"], - t: TFunction = i18n.t.bind(i18n), ): string { + const t = i18n.getFixedT("en"); const message = error instanceof Error ? error.message : t("chat.runChatRequest.genericError"); const hasFile = mode === "chat" && messages.some((item) => item.attachments?.some((attachment) => attachment.kind === "file")); @@ -67,7 +67,7 @@ export async function runChatRequest( try { signal.throwIfAborted(); if (research && !background) { - onUpdate({ content: t("chat.runChatRequest.researchNeedsBackground") }); + onUpdate({ content: i18n.getFixedT("en")("chat.runChatRequest.researchNeedsBackground") }); return; } if (mode === "image") { @@ -125,7 +125,7 @@ export async function runChatRequest( } catch (error: unknown) { onResponseId(null); const stopped = signal.aborted || (error instanceof Error && error.name === "AbortError"); - const errorMessage = requestErrorMessage(error, messages, mode, t); + const errorMessage = requestErrorMessage(error, messages, mode); onUpdate({ content: stopped ? content + " [stopped]" : content || errorMessage }); } finally { if (mcpEvents.length) onUpdate({ mcpEvents }); diff --git a/ui/litellm-dashboard/src/components/liteadmin/agent.test.ts b/ui/litellm-dashboard/src/components/liteadmin/agent.test.ts index e9fae756427..80c21e81e64 100644 --- a/ui/litellm-dashboard/src/components/liteadmin/agent.test.ts +++ b/ui/litellm-dashboard/src/components/liteadmin/agent.test.ts @@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { ChatCompletion, ChatCompletionCreateParamsNonStreaming } from "openai/resources/chat/completions"; import { createGatewayClient } from "@/components/llm_calls/gateway_client"; import { registerAuthHeaderNameGetter } from "@/lib/http/runtime"; +import i18n from "@/i18n/i18n"; import { getPreferredLiteAdminModel, MAX_INPUT_LENGTH, @@ -889,6 +890,42 @@ describe("SDK conversation and action lifecycle", () => { ); }); +describe("LiteAdmin replies sent back to the model when the UI language is not English", () => { + beforeEach(async () => { + i18n.addResourceBundle( + "de", + "translation", + { aria: { agent: { stepLimit: "Schrittlimit", noAnswer: "Keine Antwort" } } }, + true, + true, + ); + await i18n.changeLanguage("de"); + }); + + afterEach(async () => { + await i18n.changeLanguage("en"); + i18n.removeResourceBundle("de", "translation"); + }); + + it("keeps the step limit reply in English", async () => { + const input = options(); + const model = transport(Array.from({ length: 6 }, () => completion([call("teams_list", teamsArgs)]))); + await runLiteAdmin(input, model.client); + expect(input.onMessage).toHaveBeenCalledExactlyOnceWith( + "I reached the step limit. Any completed actions remain applied; check the relevant page before continuing.", + ); + }); + + it("keeps the no answer reply in English", async () => { + const input = options(); + const model = transport([completion([], null)]); + await runLiteAdmin(input, model.client); + expect(input.onMessage).toHaveBeenCalledExactlyOnceWith( + "The model returned no answer. Try another request or model.", + ); + }); +}); + describe("inference destination", () => { const destinations = [ ["", "/gateway", "https://dashboard.example/ui/", "https://dashboard.example/gateway", false], diff --git a/ui/litellm-dashboard/src/components/liteadmin/agent.ts b/ui/litellm-dashboard/src/components/liteadmin/agent.ts index b9b8420a6e5..9acce836323 100644 --- a/ui/litellm-dashboard/src/components/liteadmin/agent.ts +++ b/ui/litellm-dashboard/src/components/liteadmin/agent.ts @@ -141,8 +141,8 @@ export async function runLiteAdmin(options: LiteAdminOptions, client?: OpenAI): active(); const message = completion.choices[0]?.message; if (message?.tool_calls?.length) { - options.onMessage(i18n.t("aria.agent.stepLimit")); + options.onMessage(i18n.getFixedT("en")("aria.agent.stepLimit")); } else if (!message?.content && !message?.refusal) { - options.onMessage(i18n.t("aria.agent.noAnswer")); + options.onMessage(i18n.getFixedT("en")("aria.agent.noAnswer")); } }