Browse Source

fix(dashboard): resolve hedge winner provider incorrectly displayed in logs table

The logs table computed finalProvider as the last entry in the provider
chain, which returns the hedge loser (cancelled provider) instead of
the actual winner. Add getFinalProviderName() utility with 3-tier
priority: hedge_winner > last successful request > last entry fallback.

Replace inline chain[length-1].name logic in both virtualized-logs-table
and usage-logs-table with the new utility.
ding113 1 month ago
parent
commit
49ff9368b0

+ 2 - 4
src/app/[locale]/dashboard/logs/_components/usage-logs-table.tsx

@@ -26,7 +26,7 @@ import {
   shouldHideOutputRate,
 } from "@/lib/utils/performance-formatter";
 import { shouldShowCostBadgeInCell } from "@/lib/utils/provider-chain-display";
-import { formatProviderSummary } from "@/lib/utils/provider-chain-formatter";
+import { formatProviderSummary, getFinalProviderName } from "@/lib/utils/provider-chain-formatter";
 import {
   getPricingResolutionSpecialSetting,
   hasPriorityServiceTierSpecialSetting,
@@ -202,9 +202,7 @@ export function UsageLogsTable({
                               <ProviderChainPopover
                                 chain={log.providerChain ?? []}
                                 finalProvider={
-                                  (log.providerChain && log.providerChain.length > 0
-                                    ? log.providerChain[log.providerChain.length - 1].name
-                                    : null) ||
+                                  getFinalProviderName(log.providerChain ?? []) ||
                                   log.providerName ||
                                   tChain("circuit.unknown")
                                 }

+ 4 - 3
src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.test.tsx

@@ -53,6 +53,7 @@ vi.mock("@/hooks/use-virtualizer", () => ({
 
 vi.mock("@/lib/utils/provider-chain-formatter", () => ({
   formatProviderSummary: () => "provider summary",
+  getFinalProviderName: () => "mock-provider",
   getRetryCount: () => 0,
   isHedgeRace: () => false,
   isActualRequest: () => true,
@@ -303,9 +304,9 @@ describe("virtualized-logs-table multiplier badge", () => {
     const html = renderToStaticMarkup(
       <VirtualizedLogsTable filters={{}} autoRefreshEnabled={false} />
     );
-    // VirtualizedLogsTable uses ProviderChainPopover which renders the provider name directly,
-    // not via formatProviderSummary (which is only used in other contexts)
-    expect(html).toContain("p1");
+    // VirtualizedLogsTable uses ProviderChainPopover which renders the provider name
+    // via getFinalProviderName (mocked to return "mock-provider")
+    expect(html).toContain("mock-provider");
     expect(html).toContain("logs.table.loadingMore");
   });
 

+ 2 - 3
src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx

@@ -23,6 +23,7 @@ import {
   shouldHideOutputRate,
 } from "@/lib/utils/performance-formatter";
 import { shouldShowCostBadgeInCell } from "@/lib/utils/provider-chain-display";
+import { getFinalProviderName } from "@/lib/utils/provider-chain-formatter";
 import { isProviderFinalized } from "@/lib/utils/provider-display";
 import {
   getPricingResolutionSpecialSetting,
@@ -490,9 +491,7 @@ export function VirtualizedLogsTable({
                                       <ProviderChainPopover
                                         chain={log.providerChain ?? []}
                                         finalProvider={
-                                          (log.providerChain && log.providerChain.length > 0
-                                            ? log.providerChain[log.providerChain.length - 1].name
-                                            : null) ||
+                                          getFinalProviderName(log.providerChain ?? []) ||
                                           log.providerName ||
                                           tChain("circuit.unknown")
                                         }

+ 98 - 0
src/lib/utils/provider-chain-formatter.test.ts

@@ -6,6 +6,7 @@ import {
   formatProviderDescription,
   formatProviderSummary,
   formatProviderTimeline,
+  getFinalProviderName,
   getRetryCount,
   isActualRequest,
   isHedgeRace,
@@ -797,3 +798,100 @@ describe("Edge cases for hedge race detection", () => {
     ).toBe(true);
   });
 });
+
+// =============================================================================
+// getFinalProviderName tests
+// =============================================================================
+
+describe("getFinalProviderName", () => {
+  test("returns null for empty chain", () => {
+    expect(getFinalProviderName([])).toBeNull();
+  });
+
+  test("returns null for null/undefined chain", () => {
+    expect(getFinalProviderName(null as unknown as ProviderChainItem[])).toBeNull();
+    expect(getFinalProviderName(undefined as unknown as ProviderChainItem[])).toBeNull();
+  });
+
+  test("returns provider name for single request_success", () => {
+    const chain: ProviderChainItem[] = [
+      { id: 1, name: "provider-a", reason: "request_success", statusCode: 200, timestamp: 1000 },
+    ];
+    expect(getFinalProviderName(chain)).toBe("provider-a");
+  });
+
+  test("returns hedge_winner provider when hedge_loser_cancelled is last", () => {
+    const chain: ProviderChainItem[] = [
+      { id: 1, name: "provider-a", reason: "initial_selection", timestamp: 0 },
+      { id: 1, name: "provider-a", reason: "hedge_triggered", timestamp: 1000 },
+      { id: 2, name: "provider-b", reason: "hedge_launched", timestamp: 1001 },
+      {
+        id: 2,
+        name: "provider-b",
+        reason: "hedge_winner",
+        statusCode: 200,
+        timestamp: 2000,
+      },
+      { id: 1, name: "provider-a", reason: "hedge_loser_cancelled", timestamp: 2001 },
+    ];
+    expect(getFinalProviderName(chain)).toBe("provider-b");
+  });
+
+  test("returns retry_success provider for retry chain", () => {
+    const chain: ProviderChainItem[] = [
+      { id: 1, name: "provider-a", reason: "retry_failed", timestamp: 1000 },
+      {
+        id: 2,
+        name: "provider-b",
+        reason: "retry_success",
+        statusCode: 200,
+        timestamp: 2000,
+      },
+    ];
+    expect(getFinalProviderName(chain)).toBe("provider-b");
+  });
+
+  test("returns last entry name when all entries are failures", () => {
+    const chain: ProviderChainItem[] = [
+      { id: 1, name: "provider-a", reason: "retry_failed", timestamp: 1000 },
+      { id: 2, name: "provider-b", reason: "retry_failed", timestamp: 2000 },
+    ];
+    expect(getFinalProviderName(chain)).toBe("provider-b");
+  });
+
+  test("returns last entry name for intermediate-only chain", () => {
+    const chain: ProviderChainItem[] = [
+      { id: 1, name: "provider-a", reason: "initial_selection", timestamp: 0 },
+    ];
+    expect(getFinalProviderName(chain)).toBe("provider-a");
+  });
+
+  test("returns fallback for retry_success without statusCode", () => {
+    const chain: ProviderChainItem[] = [
+      { id: 1, name: "provider-a", reason: "retry_success", timestamp: 1000 },
+    ];
+    // No statusCode means it's an intermediate state, falls through to last-entry fallback
+    expect(getFinalProviderName(chain)).toBe("provider-a");
+  });
+
+  test("hedge_winner takes priority over request_success earlier in chain", () => {
+    // Edge case: both hedge_winner and request_success present
+    const chain: ProviderChainItem[] = [
+      {
+        id: 1,
+        name: "provider-a",
+        reason: "request_success",
+        statusCode: 200,
+        timestamp: 500,
+      },
+      {
+        id: 2,
+        name: "provider-b",
+        reason: "hedge_winner",
+        statusCode: 200,
+        timestamp: 2000,
+      },
+    ];
+    expect(getFinalProviderName(chain)).toBe("provider-b");
+  });
+});

+ 29 - 0
src/lib/utils/provider-chain-formatter.ts

@@ -151,6 +151,35 @@ export function isHedgeRace(chain: ProviderChainItem[]): boolean {
   );
 }
 
+/**
+ * Determine the final (winning) provider from a decision chain.
+ *
+ * Priority order:
+ *  1. hedge_winner  -- the provider that won a hedge race
+ *  2. Last request_success / retry_success with a statusCode
+ *  3. Fallback to the last entry's name
+ *
+ * Returns null for empty / nullish chains.
+ */
+export function getFinalProviderName(chain: ProviderChainItem[] | null | undefined): string | null {
+  if (!chain || chain.length === 0) return null;
+
+  // Priority 1: hedge_winner
+  const hedgeWinner = chain.find((item) => item.reason === "hedge_winner");
+  if (hedgeWinner) return hedgeWinner.name;
+
+  // Priority 2: last successful request (must have statusCode)
+  for (let i = chain.length - 1; i >= 0; i--) {
+    const item = chain[i];
+    if ((item.reason === "request_success" || item.reason === "retry_success") && item.statusCode) {
+      return item.name;
+    }
+  }
+
+  // Priority 3: fallback to last entry
+  return chain[chain.length - 1].name;
+}
+
 /**
  * Count real retries (excluding hedge race concurrent attempts).
  *