From 46b9bd8b033a44a3625663886244f9e53443eab7 Mon Sep 17 00:00:00 2001 From: robbond Date: Sat, 5 Sep 2026 17:12:31 +0100 Subject: [PATCH] fix(confidence-engine): retain successful provider path --- docs/current-handoff.md | 5 +++++ lib/analysis.js | 17 ++++++++++++++++- lib/llm/provider.js | 5 ++++- tests/llm/provider.test.js | 6 +++++- tests/reconstruction/compatibility.test.js | 4 ++++ 5 files changed, 34 insertions(+), 3 deletions(-) diff --git a/docs/current-handoff.md b/docs/current-handoff.md index 81d0580..8c2541f 100644 --- a/docs/current-handoff.md +++ b/docs/current-handoff.md @@ -73,6 +73,11 @@ - The detector fix remains unverified live; a subsequent HTTP 500 left the actually attempted provider path unobservable. - Case-start failure diagnostics now preserve `providerApiPath` only when the provider reports an endpoint actually attempted during that request. The next step remains one production call. +## Successful provider-path observability + +- The 502 live run at `dabd9e2` was PATH NOT OBSERVABLE ON FAILURE: provider path survived provider errors but not successful-provider/later-validation failures. +- Successful provider results now retain their factual attempted path through later reconstruction-validation failure diagnostics. This corrects only that observability seam; the next unknown remains one real `/api/cases/start` call establishing `/api/chat` versus `/api/generate`. + ## Current product architecture Three distinct routes, not a single page: diff --git a/lib/analysis.js b/lib/analysis.js index 8050cb4..05401a5 100644 --- a/lib/analysis.js +++ b/lib/analysis.js @@ -72,11 +72,23 @@ export async function analyseScenario(scenario, opts = {}) { // ── Call provider ────────────────────────────────── const provider = getProvider(); let rawResponse; + let providerApiPath; try { - rawResponse = await provider.generateReconstruction( + const providerResult = await provider.generateReconstruction( promptObj.prompt, OLLAMA_MODEL, ); + if ( + providerResult && + typeof providerResult === "object" && + "response" in providerResult && + "providerApiPath" in providerResult + ) { + rawResponse = providerResult.response; + providerApiPath = providerResult.providerApiPath; + } else { + rawResponse = providerResult; + } } catch (e) { return buildErrorResponse( e.message || "Provider error during analysis", @@ -137,6 +149,7 @@ export async function analyseScenario(scenario, opts = {}) { duration, promptVersion, compatibility, + providerApiPath, ); } @@ -219,6 +232,7 @@ function buildPartialResult( duration, version, compatibility, + providerApiPath, ) { let errors = []; const validationIssues = error?.issues ?? []; @@ -245,6 +259,7 @@ function buildPartialResult( nextQuestion: undefined, errors, validationIssues, + providerApiPath, ...buildCompatibilityDiagnostics(compatibility), }; } diff --git a/lib/llm/provider.js b/lib/llm/provider.js index 3974de8..4d1057b 100644 --- a/lib/llm/provider.js +++ b/lib/llm/provider.js @@ -247,7 +247,10 @@ class OllamaLlmProvider { // Step 5: Parse and return // ================================================================ try { - return recoverJson(rawResponse); + return { + response: recoverJson(rawResponse), + providerApiPath: apiUsed, + }; } catch (e) { if (e instanceof SyntaxError) { const error = new Error( diff --git a/tests/llm/provider.test.js b/tests/llm/provider.test.js index 98cfc29..60056c6 100644 --- a/tests/llm/provider.test.js +++ b/tests/llm/provider.test.js @@ -14,7 +14,7 @@ describe("OllamaLlmProvider chat capability detection", () => { try { const { getProvider } = await import("@/lib/llm/provider.js"); - await getProvider().generateReconstruction("prompt", "configured-model"); + const result = await getProvider().generateReconstruction("prompt", "configured-model"); expect(JSON.parse(fetchSpy.mock.calls[0][1].body)).toMatchObject({ model: "configured-model", @@ -23,6 +23,10 @@ describe("OllamaLlmProvider chat capability detection", () => { expect(fetchSpy.mock.calls[0][0]).toBe("http://ollama.test/api/chat"); expect(fetchSpy.mock.calls[1][0]).toBe("http://ollama.test/api/chat"); expect(fetchSpy.mock.calls[1][0]).not.toContain("/api/generate"); + expect(result).toMatchObject({ + response: {}, + providerApiPath: "/api/chat", + }); } finally { vi.unstubAllGlobals(); if (originalBaseUrl === undefined) delete process.env.OLLAMA_BASE_URL; diff --git a/tests/reconstruction/compatibility.test.js b/tests/reconstruction/compatibility.test.js index fe48086..23ebbfe 100644 --- a/tests/reconstruction/compatibility.test.js +++ b/tests/reconstruction/compatibility.test.js @@ -210,6 +210,8 @@ describe("analyseScenario compatibility", () => { it("preserves nested validation issues and complete raw output on reconstruction failure", async () => { mockGenerateReconstruction.mockResolvedValue({ + providerApiPath: "/api/chat", + response: { inputClassification: { primaryType: "unexplained_change", secondaryTypes: [], @@ -239,6 +241,7 @@ describe("analyseScenario compatibility", () => { reason: "reason", expectedInformationValue: "high", }, + }, }); const { analyseScenario } = await import("@/lib/analysis.js"); @@ -253,6 +256,7 @@ describe("analyseScenario compatibility", () => { code: "invalid_type", message: "Required", })); + expect(result.providerApiPath).toBe("/api/chat"); expect(result.errors).toContain("reconstruction: Required"); });