fix(dashboard): show custom provider_nodes providers in the Topology view instead of only AI_PROVIDERS (#8328) (#8357)

This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-24 11:44:29 -03:00
committed by GitHub
parent 4323dba518
commit 70275be59b
4 changed files with 123 additions and 1 deletions

View File

@@ -0,0 +1 @@
- fix(dashboard): show custom provider_nodes providers in the Topology view instead of only AI_PROVIDERS (#8328)

View File

@@ -8,6 +8,7 @@ import ProviderIcon from "@/shared/components/ProviderIcon";
import { FlowCanvas } from "@/shared/components/flow/FlowCanvas";
import { StatusDot } from "@/shared/components/flow/StatusDot";
import { edgeStyle, FLOW_EDGE_COLORS } from "@/shared/components/flow/edgeStyles";
import { getFallbackProviderColor } from "@/shared/utils/providerFallbackColor";
import { resolveTopologyNodeLabel } from "./topologyLabel";
// Rings: [capacity, rx, ry]. Each successive ring fits ~6 more nodes.
@@ -23,9 +24,12 @@ const RINGS: [number, number, number][] = [
type ProviderConfig = { color?: string; name?: string; textIcon?: string };
function getProviderConfig(providerId: string): ProviderConfig {
// Predefined providers keep their registry color/name untouched. Anything else (custom
// openai-compatible-*/anthropic-compatible-* provider_nodes) gets a deterministic,
// per-id fallback color instead of one shared gray — see #8328.
return (
(AI_PROVIDERS as Record<string, ProviderConfig>)[providerId] || {
color: "#6b7280",
color: getFallbackProviderColor(providerId),
name: providerId,
}
);

View File

@@ -0,0 +1,41 @@
/**
* Deterministic fallback color for provider ids that are not present in the static
* AI_PROVIDERS registry — e.g. user-defined `openai-compatible-*` / `anthropic-compatible-*`
* providers backed by the `provider_nodes` table. Without this, every such custom provider
* rendered as the same anonymous gray node in the Topology view, indistinguishable from any
* other custom provider even though its label was already resolved correctly. See #8328.
*/
// Small palette of visually distinguishable, saturated colors — spread far enough apart in
// hue to stay distinguishable side-by-side on both light and dark topology backgrounds.
export const FALLBACK_COLOR_PALETTE: readonly string[] = [
"#0ea5e9", // sky
"#a855f7", // purple
"#f59e0b", // amber
"#10b981", // emerald
"#ec4899", // pink
"#6366f1", // indigo
"#14b8a6", // teal
"#f97316", // orange
];
/** FNV-1a hash — fast, deterministic, and well-distributed for short id strings. */
function hashProviderId(value: string): number {
let hash = 0x811c9dc5;
for (let i = 0; i < value.length; i++) {
hash ^= value.charCodeAt(i);
hash = Math.imul(hash, 0x01000193);
}
return hash >>> 0;
}
/**
* Returns a deterministic color for a provider id not present in the static AI_PROVIDERS
* registry: the same id always maps to the same palette entry, and different ids spread
* across the palette so distinct custom providers stay visually distinguishable.
*/
export function getFallbackProviderColor(providerId: string): string {
if (!providerId) return FALLBACK_COLOR_PALETTE[0];
const index = hashProviderId(providerId) % FALLBACK_COLOR_PALETTE.length;
return FALLBACK_COLOR_PALETTE[index];
}

View File

@@ -0,0 +1,76 @@
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
// #8328 — the Topology view's local getProviderConfig() only looked up the static
// AI_PROVIDERS registry; any id not present there (every custom/compatible provider
// backed by the provider_nodes table) fell back to a single hardcoded gray color
// (#6b7280), so every custom provider rendered as the same anonymous gray node —
// visually indistinguishable from any other custom provider — even though presence
// and label resolution were already correct. This guards the fix: unknown ids get a
// deterministic, distinguishable color instead of one shared gray fallback, while
// predefined AI_PROVIDERS entries stay byte-identical.
const read = (rel: string) => readFileSync(fileURLToPath(new URL(rel, import.meta.url)), "utf8");
const providerTopologySrc = read("../../src/app/(dashboard)/home/ProviderTopology.tsx");
test("getFallbackProviderColor gives two different custom provider ids distinct colors", async () => {
const { getFallbackProviderColor } = await import(
"../../src/shared/utils/providerFallbackColor.ts"
);
const colorAlpha = getFallbackProviderColor("openai-compatible-my-custom-alpha-node");
const colorBeta = getFallbackProviderColor("openai-compatible-my-custom-beta-node");
assert.notEqual(
colorAlpha,
colorBeta,
"two distinct custom provider ids must not resolve to the same fallback color"
);
});
test("getFallbackProviderColor is deterministic for the same id", async () => {
const { getFallbackProviderColor } = await import(
"../../src/shared/utils/providerFallbackColor.ts"
);
const first = getFallbackProviderColor("anthropic-compatible-my-node-abc123");
const second = getFallbackProviderColor("anthropic-compatible-my-node-abc123");
assert.equal(first, second, "the same provider id must always resolve to the same color");
});
test("getFallbackProviderColor spreads ids across more than one palette entry", async () => {
const { getFallbackProviderColor, FALLBACK_COLOR_PALETTE } = await import(
"../../src/shared/utils/providerFallbackColor.ts"
);
assert.ok(
FALLBACK_COLOR_PALETTE.length > 1,
"the fallback palette must offer more than one distinguishable color"
);
const sampleIds = Array.from({ length: 12 }, (_, i) => `openai-compatible-custom-${i}`);
const distinctColors = new Set(sampleIds.map((id) => getFallbackProviderColor(id)));
assert.ok(
distinctColors.size > 1,
"a spread of custom provider ids must not all collapse onto a single color"
);
});
test("ProviderTopology's getProviderConfig no longer hardcodes a single gray fallback", () => {
assert.doesNotMatch(
providerTopologySrc,
/\|\|\s*\{\s*color:\s*"#6b7280"/,
"the unknown-provider branch must no longer fall back to one hardcoded gray color"
);
assert.match(
providerTopologySrc,
/getFallbackProviderColor/,
"getProviderConfig must resolve unknown provider ids through the deterministic fallback helper"
);
});
test("ProviderTopology's getProviderConfig still resolves predefined AI_PROVIDERS entries first", () => {
assert.match(
providerTopologySrc,
/\(AI_PROVIDERS as Record<string, ProviderConfig>\)\[providerId\]/,
"a predefined provider id must still resolve from the static registry, unaffected by the fallback"
);
});