mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
fix(dashboard): show custom provider_nodes providers in the Topology view instead of only AI_PROVIDERS (#8328) (#8357)
This commit is contained in:
committed by
GitHub
parent
4323dba518
commit
70275be59b
1
changelog.d/fixes/8328-topology-custom-providers.md
Normal file
1
changelog.d/fixes/8328-topology-custom-providers.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(dashboard): show custom provider_nodes providers in the Topology view instead of only AI_PROVIDERS (#8328)
|
||||
@@ -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,
|
||||
}
|
||||
);
|
||||
|
||||
41
src/shared/utils/providerFallbackColor.ts
Normal file
41
src/shared/utils/providerFallbackColor.ts
Normal 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];
|
||||
}
|
||||
76
tests/unit/8328-topology-custom-providers.test.ts
Normal file
76
tests/unit/8328-topology-custom-providers.test.ts
Normal 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"
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user