diff --git a/CHANGELOG.md b/CHANGELOG.md index aba5c2dff6..eb55e69e19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) diff --git a/src/lib/memory/embedding/index.ts b/src/lib/memory/embedding/index.ts index 782cf4fbda..aaf645ddbc 100644 --- a/src/lib/memory/embedding/index.ts +++ b/src/lib/memory/embedding/index.ts @@ -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"); } /** diff --git a/src/lib/memory/retrieval.ts b/src/lib/memory/retrieval.ts index cbc2dec32a..d6886c5ec9 100644 --- a/src/lib/memory/retrieval.ts +++ b/src/lib/memory/retrieval.ts @@ -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 { 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 { 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 { 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 { // 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("/") ?? []; diff --git a/tests/unit/memory-embedding-resolve.test.ts b/tests/unit/memory-embedding-resolve.test.ts index 292650aaf1..7a38d6d0af 100644 --- a/tests/unit/memory-embedding-resolve.test.ts +++ b/tests/unit/memory-embedding-resolve.test.ts @@ -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}` ); }); diff --git a/tests/unit/memory-engine-status.test.ts b/tests/unit/memory-engine-status.test.ts index 04c4d97771..a3921b74a9 100644 --- a/tests/unit/memory-engine-status.test.ts +++ b/tests/unit/memory-engine-status.test.ts @@ -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();