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")); } }