fix: render memory engine status detail strings in English (#5596) (#5685)

This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-30 13:35:34 -03:00
committed by GitHub
parent 8a9524b364
commit c568ab9cbb
5 changed files with 62 additions and 25 deletions

View File

@@ -10,6 +10,8 @@
### 🔧 Bug Fixes
- **dashboard (memory engine):** the memory engine status page no longer mixes English and Portuguese. The embedding / vector-store / rerank **status detail strings** were hardcoded in Portuguese in the backend (`resolveEmbeddingSource`, `engineStatus`), e.g. `auto: nenhuma fonte de embedding disponível` and `sqlite-vec ativo, dim=…`, while the surrounding UI labels render from the English i18n bundle — so an English user saw a half-translated page. The backend detail strings are now English (`auto: no embedding source available`, `sqlite-vec active, dim=…`, etc.), matching the rest of the page. Regression guard: `tests/unit/memory-engine-status.test.ts`. ([#5596](https://github.com/diegosouzapw/OmniRoute/issues/5596))
- **providers (cline):** stop falsely mapping valid **Cline** (OAuth) responses to `502 empty_choices` + account cooldown. `detectMalformedNonStream` only recognized `choices[].message.content` as a **string**, but some OpenAI-compatible upstreams — Cline via OAuth among them — return `content` as an **array of Anthropic-style text blocks** inside an OpenAI envelope. A non-empty response (recvBytes > 0) was therefore classified as `empty_choices` and turned into a 502 that also cooled the account down. The malformed-response detector now also treats a content array carrying at least one non-empty `text` block as real output. Regression guard: `tests/unit/diagnostics.test.ts`. ([#5559](https://github.com/diegosouzapw/OmniRoute/issues/5559))
- **embedded services (Windows):** fix **CLIProxyAPI install failing instantly with `spawn unzip ENOENT`** on Windows. The binary extractor spawned `unzip`, which is not a Windows system command — it only ships inside Git for Windows' `usr/bin`, a directory Node's `spawn` PATH never sees, so even users with Git installed hit the error. On Windows the extractor now uses PowerShell's built-in `Expand-Archive` (via `execFileAsync`, no shell — paths pass as a single non-interpreted arg, with `''`-escaping + `-LiteralPath` as defense in depth); other platforms keep using `unzip`. This is distinct from #5379 (that was `npm.cmd` needing `shell: true`). Regression guard: `tests/unit/binary-manager-extract-zip-5590.test.ts`. ([#5590](https://github.com/diegosouzapw/OmniRoute/issues/5590))

View File

@@ -56,7 +56,7 @@ export function resolveEmbeddingSource(settings: MemorySettingsExtended): Embedd
model: null,
dimensions: null,
signature: makeSignature(null, null, null),
reason: "no_key: embeddingProviderModel não configurado",
reason: "no_key: embeddingProviderModel not configured",
};
}
// We can't do async here, so we report it as potentially available
@@ -67,7 +67,7 @@ export function resolveEmbeddingSource(settings: MemorySettingsExtended): Embedd
model,
dimensions: null,
signature: makeSignature("remote", model, null),
reason: `provider remoto configurado: ${model}`,
reason: `remote provider configured: ${model}`,
};
}
@@ -78,7 +78,7 @@ export function resolveEmbeddingSource(settings: MemorySettingsExtended): Embedd
model: null,
dimensions: null,
signature: makeSignature(null, null, null),
reason: "static desabilitado nas configurações",
reason: "static disabled in settings",
};
}
return {
@@ -86,7 +86,7 @@ export function resolveEmbeddingSource(settings: MemorySettingsExtended): Embedd
model: STATIC_MODEL,
dimensions: 256,
signature: makeSignature("static", STATIC_MODEL, 256),
reason: "static (potion-base-8M) selecionado explicitamente",
reason: "static (potion-base-8M) explicitly selected",
};
}
@@ -97,7 +97,7 @@ export function resolveEmbeddingSource(settings: MemorySettingsExtended): Embedd
model: null,
dimensions: null,
signature: makeSignature(null, null, null),
reason: "transformers desabilitado nas configurações",
reason: "transformers disabled in settings",
};
}
return {
@@ -105,7 +105,7 @@ export function resolveEmbeddingSource(settings: MemorySettingsExtended): Embedd
model: TRANSFORMERS_MODEL,
dimensions: 384,
signature: makeSignature("transformers", TRANSFORMERS_MODEL, 384),
reason: "transformers.js (MiniLM-L6-v2) selecionado explicitamente",
reason: "transformers.js (MiniLM-L6-v2) explicitly selected",
};
}
@@ -128,7 +128,7 @@ export function resolveEmbeddingSource(settings: MemorySettingsExtended): Embedd
model: providerModel,
dimensions: null,
signature: makeSignature("remote", providerModel, null),
reason: `auto: provider ${providerId} configurado`,
reason: `auto: provider ${providerId} configured`,
};
}
}
@@ -139,7 +139,7 @@ export function resolveEmbeddingSource(settings: MemorySettingsExtended): Embedd
model: STATIC_MODEL,
dimensions: 256,
signature: makeSignature("static", STATIC_MODEL, 256),
reason: "auto: potion-base-8M (static) disponível",
reason: "auto: potion-base-8M (static) available",
};
}
@@ -149,14 +149,14 @@ export function resolveEmbeddingSource(settings: MemorySettingsExtended): Embedd
model: TRANSFORMERS_MODEL,
dimensions: 384,
signature: makeSignature("transformers", TRANSFORMERS_MODEL, 384),
reason: "auto: transformers.js (MiniLM-L6-v2) disponível",
reason: "auto: transformers.js (MiniLM-L6-v2) available",
};
}
return noSource("auto: nenhuma fonte de embedding disponível");
return noSource("auto: no embedding source available");
}
return noSource("fonte de embedding desconhecida");
return noSource("unknown embedding source");
}
/**

View File

@@ -843,7 +843,7 @@ export async function retrievePreview(
}
// Qdrant returned nothing — fall through to sqlite-vec for parity
// with production (so the Playground reflects the same fallback).
fallbackReason = "Qdrant retornou 0 resultados — fallback p/ sqlite-vec";
fallbackReason = "Qdrant returned 0 results — falling back to sqlite-vec";
} catch (err: unknown) {
fallbackReason = sanitizeErrorMessage(
err instanceof Error ? err.message : String(err)
@@ -969,7 +969,7 @@ export async function retrievePreview(
log.warn("memory.preview.vector.fail", { error: fallbackReason });
}
} else {
fallbackReason = "sqlite-vec não disponível (degradado para FTS5)";
fallbackReason = "sqlite-vec not available (degraded to FTS5)";
}
} else {
// EmbeddingError
@@ -1073,7 +1073,7 @@ export async function engineStatus(): Promise<MemoryEngineStatus> {
let vecAvailable = false;
let vecRowCount = 0;
let vecNeedsReindex = 0;
let vecReason = "sqlite-vec não disponível";
let vecReason = "sqlite-vec not available";
if (vec) {
vecBackend = "sqlite-vec";
@@ -1082,12 +1082,12 @@ export async function engineStatus(): Promise<MemoryEngineStatus> {
const s = await vec.stats();
vecRowCount = s.rowCount;
vecNeedsReindex = s.needsReindex;
vecReason = `sqlite-vec ativo, dim=${s.activeDim ?? "null"}`;
vecReason = `sqlite-vec active, dim=${s.activeDim ?? "null"}`;
} catch {
vecReason = "sqlite-vec ativo mas stats falharam";
vecReason = "sqlite-vec active but stats failed";
}
} else {
vecReason = "sqlite-vec não disponível — usando apenas FTS5";
vecReason = "sqlite-vec not available — using FTS5 only";
}
// Qdrant
@@ -1112,7 +1112,7 @@ export async function engineStatus(): Promise<MemoryEngineStatus> {
if (qdrantHealthy && settings.vectorStore === "qdrant") {
vecBackend = "qdrant";
vecAvailable = true;
vecReason = `Qdrant configurado em ${qdrantCfg.host}:${qdrantCfg.port}`;
vecReason = `Qdrant configured at ${qdrantCfg.host}:${qdrantCfg.port}`;
}
}
} catch (err: unknown) {
@@ -1121,12 +1121,12 @@ export async function engineStatus(): Promise<MemoryEngineStatus> {
// Rerank
let rerankAvailable = false;
let rerankReason = "rerank desabilitado";
let rerankReason = "rerank disabled";
if (settings.rerankEnabled && settings.rerankProviderModel) {
rerankAvailable = true;
rerankReason = `rerank ativo: ${settings.rerankProviderModel}`;
rerankReason = `rerank active: ${settings.rerankProviderModel}`;
} else if (settings.rerankEnabled && !settings.rerankProviderModel) {
rerankReason = "rerank habilitado mas provider não configurado";
rerankReason = "rerank enabled but provider not configured";
}
const rerankParts = settings.rerankProviderModel?.split("/") ?? [];

View File

@@ -27,8 +27,8 @@ describe("resolveEmbeddingSource", () => {
assert.strictEqual(res.source, null);
// The reason must indicate the lack of any source — not just be non-empty.
assert.ok(
res.reason.toLowerCase().includes("nenhuma"),
`expected reason to mention "nenhuma", got: ${res.reason}`
res.reason.toLowerCase().includes("no embedding source"),
`expected reason to mention "no embedding source", got: ${res.reason}`
);
});
@@ -77,8 +77,8 @@ describe("resolveEmbeddingSource", () => {
assert.strictEqual(res.source, null);
// The reason must reference the missing key, not just be non-empty.
assert.ok(
res.reason.includes("no_key") || res.reason.includes("configurado"),
`expected reason to mention "no_key" or "configurado", got: ${res.reason}`
res.reason.includes("no_key") || res.reason.includes("configured"),
`expected reason to mention "no_key" or "configured", got: ${res.reason}`
);
});

View File

@@ -129,6 +129,41 @@ test("engineStatus(): rerank section when not configured", async () => {
assert.equal(typeof status.rerank.reason, "string", "rerank.reason must be a string");
});
test("engineStatus(): detail strings are English, not mixed Portuguese (#5596)", async () => {
core.getDbInstance();
const { engineStatus } = await import("../../src/lib/memory/retrieval.ts");
const status = await engineStatus();
// Exact English values for the default (no-config, vec-disabled) state the
// reporter saw rendered in Portuguese next to English UI labels.
assert.equal(status.embedding.reason, "auto: no embedding source available");
assert.equal(status.vectorStore.reason, "sqlite-vec not available — using FTS5 only");
assert.equal(status.rerank.reason, "rerank disabled");
// Guard: no leftover Portuguese in any surfaced reason string.
const ptWords = [
"não",
"disponível",
"configurado",
"desabilitado",
"nenhuma",
"desconhecida",
"habilitado",
"degradado",
"selecionado",
];
for (const reason of [
status.embedding.reason,
status.vectorStore.reason,
status.rerank.reason,
]) {
for (const w of ptWords) {
assert.ok(!reason.includes(w), `reason "${reason}" still contains Portuguese "${w}"`);
}
}
});
test("engineStatus(): no throw when called multiple times", async () => {
core.getDbInstance();