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.
This commit is contained in:
yuneng-jiang 2026-11-05 16:51:27 -08:00 • committed by GitHub
parent 80b1b9b188
commit e3272da848
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 107 additions and 6 deletions

View file

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

View file

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

View file

@ -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],

View file

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