diff --git a/changelog.d/fixes/8867-rejected-combo-provider-label.md b/changelog.d/fixes/8867-rejected-combo-provider-label.md new file mode 100644 index 0000000000..670a943c79 --- /dev/null +++ b/changelog.d/fixes/8867-rejected-combo-provider-label.md @@ -0,0 +1 @@ +- **Logs**: a failed `auto/` request no longer writes every attempted model into `call_logs.provider` — the logs page builds its quick-filter pills from that column, so one failure produced a giant chip that flooded the filter row. Rejected combo requests are now labelled `auto` (for `auto/*`) or by the combo's own name diff --git a/src/sse/handlers/chat.ts b/src/sse/handlers/chat.ts index 1eedbe86e2..da10e9a550 100644 --- a/src/sse/handlers/chat.ts +++ b/src/sse/handlers/chat.ts @@ -908,13 +908,13 @@ export async function handleChat( // (success:false) so gate/breaker-rejected traffic is counted per key — support-mesh 2026-07-08. if (!response.ok) { try { - const { recordRejectedRequestUsage, summarizeComboAttemptedModels } = + const { recordRejectedRequestUsage, resolveRejectedComboProvider } = await import("./rejectedRequestUsage"); await recordRejectedRequestUsage({ status: response.status, model: body?.model || resolvedModelStr, requestedModel: body?.model || resolvedModelStr, - provider: summarizeComboAttemptedModels(combo?.models), + provider: resolveRejectedComboProvider(body?.model || resolvedModelStr, combo.name), endpoint: clientRawRequest?.endpoint, error: await getComboFailureLogError(response, combo.name), comboName: combo.name, diff --git a/src/sse/handlers/rejectedRequestUsage.ts b/src/sse/handlers/rejectedRequestUsage.ts index dc70e09e76..46b8099014 100644 --- a/src/sse/handlers/rejectedRequestUsage.ts +++ b/src/sse/handlers/rejectedRequestUsage.ts @@ -127,3 +127,20 @@ export function summarizeComboAttemptedModels(models: unknown): string { .filter((entry): entry is string => Boolean(entry)); return modelStrings.length > 0 ? modelStrings.join(", ") : "-"; } + +/** + * Provider label for a REJECTED combo request (#8867). + * + * summarizeComboAttemptedModels() joins every attempted model, which is right for + * diagnostics but wrong for `call_logs.provider`: the logs page builds its quick-filter + * pills from that column, so one failed `auto/gemma` turned into a chip listing a dozen + * models and flooded the filter row. Keep it short and categorical instead — `auto` for + * auto/* requests, otherwise the combo's own name. + */ +export function resolveRejectedComboProvider( + model: string | null | undefined, + comboName: string | null | undefined +): string { + if (typeof model === "string" && model.startsWith("auto/")) return "auto"; + return comboName || "combo"; +} diff --git a/tests/unit/rejected-request-usage.test.ts b/tests/unit/rejected-request-usage.test.ts index 44aa110ee5..a25ada202b 100644 --- a/tests/unit/rejected-request-usage.test.ts +++ b/tests/unit/rejected-request-usage.test.ts @@ -24,7 +24,7 @@ process.env.DATA_DIR = TEST_DATA_DIR; const core = await import("../../src/lib/db/core.ts"); const usageHistory = await import("../../src/lib/usage/usageHistory.ts"); const callLogs = await import("../../src/lib/usage/callLogs.ts"); -const { recordRejectedRequestUsage, summarizeComboAttemptedModels } = +const { recordRejectedRequestUsage, summarizeComboAttemptedModels, resolveRejectedComboProvider } = await import("../../src/sse/handlers/rejectedRequestUsage.ts"); test.beforeEach(() => { @@ -175,3 +175,24 @@ test("summarizeComboAttemptedModels falls back to '-' for empty, missing, or inv assert.equal(summarizeComboAttemptedModels("not-an-array"), "-"); assert.equal(summarizeComboAttemptedModels([{ kind: "combo-ref" }, { foo: "bar" }]), "-"); }); + +test("#8867: resolveRejectedComboProvider keeps the provider label short for auto/* failures", () => { + // The logs page builds quick-filter pills from call_logs.provider — a failed + // auto/ used to write every attempted model there and flood the filter row. + assert.equal(resolveRejectedComboProvider("auto/gemma", "my-combo"), "auto"); + assert.equal(resolveRejectedComboProvider("auto/gemini", null), "auto"); + assert.equal(resolveRejectedComboProvider("auto/anything-at-all", undefined), "auto"); +}); + +test("#8867: a non-auto combo failure is labelled with the combo name", () => { + assert.equal(resolveRejectedComboProvider("gpt-5.6-sol", "balanced-load"), "balanced-load"); + assert.equal(resolveRejectedComboProvider(null, "balanced-load"), "balanced-load"); +}); + +test("#8867: falls back to 'combo' when there is no name, and 'auto' never leaks into it", () => { + assert.equal(resolveRejectedComboProvider("gpt-5.6-sol", null), "combo"); + assert.equal(resolveRejectedComboProvider(undefined, undefined), "combo"); + assert.equal(resolveRejectedComboProvider("", ""), "combo"); + // bare "auto" (no slash) is NOT a family request — it must not be collapsed + assert.equal(resolveRejectedComboProvider("auto", "named-combo"), "named-combo"); +});