FN-6969: show provider icons beside Command Center models

Show inferred provider icons beside model names across Command Center bar and table surfaces.

- Add dashboard-side provider-icon inference for model/provider strings.
- Render provider icons on Command Center model bar rows and token table rows while keeping pie chart labels text-only.
- Cover overview, token, and ecosystem model-icon surfaces with tests and add a changeset.

Files changed:
 .changeset/fn-6969-provider-icons.md               |   7 ++
 .../components/command-center/CommandCenter.tsx    |  15 ++-
 .../__tests__/CommandCenter.test.tsx               |  33 ++++++-
 .../command-center/areas/EcosystemArea.tsx         |  15 ++-
 .../components/command-center/areas/TokensArea.tsx |  37 +++++++-
 .../areas/__tests__/TokensArea.test.tsx            | 105 +++++++++++++++++++++
 .../command-center/areas/__tests__/areas.test.tsx  |  42 ++++++++-
 .../app/components/command-center/charts/Bar.tsx   |  25 ++++-
 .../components/command-center/charts/charts.css    |  42 +++++++++
 .../app/utils/__tests__/providerIconKey.test.ts    |  32 +++++++
 packages/dashboard/app/utils/providerIconKey.ts    |  45 +++++++++
 11 files changed, 377 insertions(+), 21 deletions(-)

Fusion-Task-Id: FN-6969

Fusion-Task-Lineage: 6caf8864-1ecc-46e3-b7fa-bfa8e9a70aaa
This commit is contained in:
gsxdsm
2026-06-25 21:27:19 -07:00
parent 433ccce0b5
commit 9a2709dde9
11 changed files with 377 additions and 21 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": minor
---
summary: Show provider icons next to Command Center model names.
category: feature
dev: Infers provider icons from model ids for Command Center model tables and bar charts; pie charts remain text-only.

View File

@@ -21,6 +21,7 @@ import { NodesView } from "../NodesView";
import type { ToastType } from "../../hooks/useToast";
import type { TaskView } from "../../hooks/useViewState";
import { SdlcFunnel } from "./SdlcFunnel";
import { inferProviderIconKey } from "../../utils/providerIconKey";
import { Bar, type BarDatum } from "./charts/Bar";
import { Sparkline } from "./charts/Sparkline";
import { LineChart as RechartsLineChart, PieChart } from "./charts/recharts";
@@ -182,11 +183,15 @@ function OverviewTab({
[...(tokens.data?.groups ?? [])]
.sort((a, b) => b.totalTokens - a.totalTokens || (a.key ?? "").localeCompare(b.key ?? ""))
.slice(0, 8)
.map((g) => ({
label: g.key ?? t("commandCenter.tokens.unknownModel", "(unknown)"),
value: g.totalTokens,
valueLabel: formatCount(g.totalTokens),
})),
.map((g) => {
const label = g.key ?? t("commandCenter.tokens.unknownModel", "(unknown)");
return {
label,
value: g.totalTokens,
valueLabel: formatCount(g.totalTokens),
iconProvider: inferProviderIconKey(g.key ?? ""),
};
}),
[tokens.data?.groups, t],
);
const toolCategoryData = useMemo<BarDatum[]>(

View File

@@ -44,6 +44,10 @@ vi.mock("../../NodesView", () => ({
),
}));
function providerIconIn(element: HTMLElement, provider: string): Element | null {
return element.querySelector(`.provider-icon[data-provider="${provider}"]`);
}
function tokenFixture(totalTokens = 1_500) {
return {
from: "2026-06-08",
@@ -514,9 +518,15 @@ describe("CommandCenter shell", () => {
const charts = screen.getByTestId("command-center-overview-charts");
expect(within(charts).getByText("Tokens by model")).toBeTruthy();
expect(within(screen.getByTestId("command-center-overview-chart-tokens")).getByText("gpt-4o")).toBeTruthy();
const overviewTokenChart = screen.getByTestId("command-center-overview-chart-tokens");
expect(within(overviewTokenChart).getByText("gpt-4o")).toBeTruthy();
expect(providerIconIn(overviewTokenChart, "openai")).toBeTruthy();
expect(providerIconIn(overviewTokenChart, "anthropic")).toBeTruthy();
expect(screen.getByRole("img", { name: "openai gpt-4o: 900" })).toBeTruthy();
expect(within(screen.getByTestId("command-center-overview-chart-tools")).getByText("read")).toBeTruthy();
expect(screen.getByTestId("cc-overview-pie")).toBeTruthy();
expect(providerIconIn(screen.getByTestId("cc-overview-pie"), "openai")).toBeNull();
expect(providerIconIn(screen.getByTestId("cc-overview-pie"), "anthropic")).toBeNull();
expect(screen.getByTestId("cc-overview-line")).toBeTruthy();
expectDailyActivityLineBeforeTrend();
expect(screen.getByRole("img", { name: "Token share by model" })).toBeTruthy();
@@ -525,6 +535,27 @@ describe("CommandCenter shell", () => {
expectThroughputLastAfter("command-center-stat-tokens", "command-center-live-strip", "command-center-overview-charts");
});
it("renders Overview providerless and unknown model icons without touching pie labels", async () => {
mockOverviewApi({
tokens: {
...tokenFixture(),
groups: [
{ ...tokenFixture().groups[0], key: "legacy-model", totalTokens: 50, nTasks: 1 },
{ ...tokenFixture().groups[1], key: null, totalTokens: 25, nTasks: 1 },
],
},
});
render(<CommandCenter />);
const overviewTokenChart = await screen.findByTestId("command-center-overview-chart-tokens");
expect(providerIconIn(overviewTokenChart, "legacy-model")).toBeTruthy();
expect(providerIconIn(overviewTokenChart, "")).toBeTruthy();
expect(screen.getByRole("img", { name: "legacy-model: 50" })).toBeTruthy();
expect(within(overviewTokenChart).getByText("legacy-model").closest(".cc-bar-label")).toBeTruthy();
expect(within(overviewTokenChart).getByText("(unknown)").closest(".cc-bar-label")).toBeTruthy();
expect(providerIconIn(screen.getByTestId("cc-overview-pie"), "legacy-model")).toBeNull();
});
it("renders large comma-grouped token totals unchanged in Overview stat surfaces", async () => {
mockOverviewApi({ tokens: tokenFixture(1_234_567_890) });
render(<CommandCenter />);

View File

@@ -2,6 +2,7 @@ import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import type { PluginActivationAnalytics, TokenAnalytics } from "@fusion/core";
import type { DateRange } from "../DateRangePicker";
import { inferProviderIconKey } from "../../../utils/providerIconKey";
import { Bar } from "../charts/Bar";
import { LineChart, PieChart } from "../charts/recharts";
import { AreaShell } from "./AreaShell";
@@ -43,11 +44,15 @@ export function EcosystemArea({ range }: { range: DateRange }) {
[...models]
.sort((a, b) => b.nTasks - a.nTasks || (a.key ?? "").localeCompare(b.key ?? ""))
.slice(0, 12)
.map((g) => ({
label: g.key ?? t("commandCenter.tokens.unknownModel", "(unknown)"),
value: g.nTasks,
valueLabel: formatCount(g.nTasks),
})),
.map((g) => {
const label = g.key ?? t("commandCenter.tokens.unknownModel", "(unknown)");
return {
label,
value: g.nTasks,
valueLabel: formatCount(g.nTasks),
iconProvider: inferProviderIconKey(g.key ?? ""),
};
}),
[models, t],
);
/*

View File

@@ -11,6 +11,8 @@ import type {
TokenTimeGranularity,
} from "@fusion/core";
import type { DateRange } from "../DateRangePicker";
import { ProviderIcon } from "../../ProviderIcon";
import { inferProviderIconKey } from "../../../utils/providerIconKey";
import { Bar } from "../charts/Bar";
import { TokenSeriesChart } from "../charts/TokenSeriesChart";
import { LineChart as RechartsLineChart, PieChart } from "../charts/recharts";
@@ -36,6 +38,32 @@ function costSortValue(cost: CostResult): number {
return cost.unavailable || cost.usd === null ? -1 : cost.usd;
}
function modelGroupIdentity(group: TokenGroupSummary): string {
return group.key ?? "unknown";
}
function modelGroupDisplayLabel(group: TokenGroupSummary, unknownLabel: string): string {
return group.key ?? unknownLabel;
}
function modelGroupIconProvider(group: TokenGroupSummary): string {
return inferProviderIconKey(group.key ?? "");
}
/*
FNXC:CommandCenterProviderIcons 2026-06-25-00:00:
Every textual Command Center model-name surface needs an adjacent ProviderIcon inferred from the model id, including null/unknown ids where ProviderIcon's CPU fallback is the intended safe state.
*/
function ModelNameWithProviderIcon({ group, unknownLabel }: { group: TokenGroupSummary; unknownLabel: string }) {
const label = group.key ?? unknownLabel;
return (
<span className="cc-model-label">
<ProviderIcon provider={modelGroupIconProvider(group)} size="sm" />
<span className="cc-model-label-text">{label}</span>
</span>
);
}
function sortGroups(groups: TokenGroupSummary[], key: SortKey, dir: 1 | -1): TokenGroupSummary[] {
const sorted = [...groups];
sorted.sort((a, b) => {
@@ -99,9 +127,10 @@ export function TokensArea({ range }: { range: DateRange }) {
.sort((a, b) => b.totalTokens - a.totalTokens)
.slice(0, 12)
.map((g) => ({
label: g.key ?? t("commandCenter.tokens.unknownModel", "(unknown)"),
label: modelGroupDisplayLabel(g, t("commandCenter.tokens.unknownModel", "(unknown)")),
value: g.totalTokens,
valueLabel: formatCount(g.totalTokens),
iconProvider: modelGroupIconProvider(g),
})),
[groups, t],
);
@@ -112,7 +141,7 @@ export function TokensArea({ range }: { range: DateRange }) {
.sort((a, b) => b.totalTokens - a.totalTokens)
.slice(0, 12)
.map((g) => ({
label: g.key ?? t("commandCenter.tokens.unknownModel", "(unknown)"),
label: modelGroupDisplayLabel(g, t("commandCenter.tokens.unknownModel", "(unknown)")),
value: g.totalTokens,
})),
[groups, t],
@@ -244,8 +273,8 @@ export function TokensArea({ range }: { range: DateRange }) {
</thead>
<tbody>
{sortedGroups.map((g) => (
<tr key={g.key ?? "∅"} data-testid={`cc-tokens-row-${g.key ?? "unknown"}`}>
<td>{g.key ?? t("commandCenter.tokens.unknownModel", "(unknown)")}</td>
<tr key={modelGroupIdentity(g)} data-testid={`cc-tokens-row-${modelGroupIdentity(g)}`}>
<td><ModelNameWithProviderIcon group={g} unknownLabel={t("commandCenter.tokens.unknownModel", "(unknown)")} /></td>
<td>{formatCount(g.inputTokens)}</td>
<td>{formatCount(g.outputTokens)}</td>
<td>{formatCount(g.cachedTokens)}</td>

View File

@@ -0,0 +1,105 @@
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import { render, screen, within } from "@testing-library/react";
import { TokensArea } from "../TokensArea";
import type { DateRange } from "../../DateRangePicker";
const apiMock = vi.fn();
vi.mock("../../../../api/legacy", () => ({
api: (path: string, opts?: RequestInit) => apiMock(path, opts),
}));
vi.mock("../../../ProviderIcon", () => ({
ProviderIcon: ({ provider, size }: { provider: string; size?: "sm" | "md" | "lg" }) => (
<span className="provider-icon" data-provider={provider.toLowerCase()} data-size={size} data-testid={`provider-icon-${provider}`} />
),
}));
const range7d: DateRange = { from: "2026-06-08", to: null, preset: "7d" };
function tokenFixture() {
return {
from: "2026-06-08",
to: null,
groupBy: "model",
totals: {
inputTokens: 1_350,
outputTokens: 675,
cachedTokens: 225,
cacheWriteTokens: 0,
totalTokens: 2_250,
nTasks: 5,
},
cost: { usd: 12.5, unavailable: false, stale: false },
series: [],
groups: [
{
key: "claude-sonnet-4-5",
inputTokens: 600,
outputTokens: 300,
cachedTokens: 100,
cacheWriteTokens: 0,
totalTokens: 1_000,
nTasks: 2,
cost: { usd: 3.5, unavailable: false, stale: false },
},
{
key: "gpt-4o-mini",
inputTokens: 500,
outputTokens: 250,
cachedTokens: 100,
cacheWriteTokens: 0,
totalTokens: 850,
nTasks: 2,
cost: { usd: 7, unavailable: false, stale: false },
},
{
key: null,
inputTokens: 250,
outputTokens: 125,
cachedTokens: 25,
cacheWriteTokens: 0,
totalTokens: 400,
nTasks: 1,
cost: { usd: null, unavailable: true, stale: false },
},
],
};
}
beforeEach(() => {
apiMock.mockReset();
apiMock.mockResolvedValue(tokenFixture());
});
afterEach(() => {
vi.useRealTimers();
});
describe("TokensArea provider model icons", () => {
it("renders inferred provider icons in the per-model table and Tokens by model bars", async () => {
render(<TokensArea range={range7d} />);
const table = await screen.findByTestId("cc-tokens-table");
const claudeRow = screen.getByTestId("cc-tokens-row-claude-sonnet-4-5");
const gptRow = screen.getByTestId("cc-tokens-row-gpt-4o-mini");
const unknownRow = screen.getByTestId("cc-tokens-row-unknown");
expect(within(claudeRow).getByText("claude-sonnet-4-5")).toBeTruthy();
expect(within(claudeRow).getByTestId("provider-icon-anthropic")).toBeTruthy();
expect(within(gptRow).getByText("gpt-4o-mini")).toBeTruthy();
expect(within(gptRow).getByTestId("provider-icon-openai")).toBeTruthy();
expect(within(unknownRow).getByText("(unknown)")).toBeTruthy();
expect(within(unknownRow).getByTestId("provider-icon-")).toBeTruthy();
const byModelChart = screen.getByRole("list", { name: "Tokens by model" });
const claudeBarLabel = within(byModelChart).getByText("claude-sonnet-4-5").closest(".cc-bar-label");
const gptBarLabel = within(byModelChart).getByText("gpt-4o-mini").closest(".cc-bar-label");
const unknownBarLabel = within(byModelChart).getByText("(unknown)").closest(".cc-bar-label");
expect(claudeBarLabel?.firstElementChild).toHaveAttribute("data-testid", "provider-icon-anthropic");
expect(gptBarLabel?.firstElementChild).toHaveAttribute("data-testid", "provider-icon-openai");
expect(unknownBarLabel?.firstElementChild).toHaveAttribute("data-testid", "provider-icon-");
expect(table.querySelectorAll(".provider-icon").length).toBeGreaterThanOrEqual(3);
});
});

View File

@@ -63,6 +63,10 @@ import { defaultPresets, rangeFromPreset, type DateRange } from "../../DateRange
const range7d: DateRange = { from: "2026-06-08", to: null, preset: "7d" };
const customRange = (from: string, to: string): DateRange => ({ from, to, preset: "custom" });
function providerIconIn(element: HTMLElement, provider: string): Element | null {
return element.querySelector(`.provider-icon[data-provider="${provider}"]`);
}
function tokenFixture(totalTokens = 1500) {
return {
from: "2026-06-08",
@@ -760,8 +764,36 @@ describe("TokensArea", () => {
expect(screen.getByRole("img", { name: "Tokens trend" })).toBeTruthy();
expect(screen.getByRole("img", { name: "Token share by model" })).toBeTruthy();
expect(screen.getByLabelText("2026-06-09: 900")).toBeTruthy();
expect(screen.getByTestId("cc-tokens-row-gpt-4o")).toBeTruthy();
expect(screen.getByTestId("cc-tokens-row-claude-sonnet")).toBeTruthy();
const tokensBar = screen.getByRole("list", { name: "Tokens by model" }) as HTMLElement;
expect(providerIconIn(tokensBar, "openai")).toBeTruthy();
expect(providerIconIn(tokensBar, "anthropic")).toBeTruthy();
expect(screen.getByRole("img", { name: "openai gpt-4o: 900" })).toBeTruthy();
expect(providerIconIn(screen.getByTestId("cc-tokens-pie"), "openai")).toBeNull();
expect(providerIconIn(screen.getByTestId("cc-tokens-pie"), "anthropic")).toBeNull();
expect(providerIconIn(screen.getByTestId("cc-tokens-row-gpt-4o"), "openai")).toBeTruthy();
expect(providerIconIn(screen.getByTestId("cc-tokens-row-claude-sonnet"), "anthropic")).toBeTruthy();
});
it("renders providerless and unknown model rows correctly", async () => {
apiMock.mockResolvedValue({
...tokenFixture(),
totals: { ...tokenFixture().totals, totalTokens: 75, nTasks: 2 },
groups: [
{ ...tokenFixture().groups[0], key: "legacy-model", totalTokens: 50, nTasks: 1 },
{ ...tokenFixture().groups[1], key: null, totalTokens: 25, nTasks: 1 },
],
});
render(<TokensArea range={range7d} />);
await screen.findByTestId("cc-area-tokens");
expect(screen.getByRole("img", { name: "legacy-model: 50" })).toBeTruthy();
const legacyRow = screen.getByTestId("cc-tokens-row-legacy-model");
const unknownRow = screen.getByTestId("cc-tokens-row-unknown");
expect(legacyRow.textContent).toContain("legacy-model");
expect(unknownRow.textContent).toContain("(unknown)");
expect(providerIconIn(legacyRow, "legacy-model")).toBeTruthy();
expect(providerIconIn(unknownRow, "")).toBeTruthy();
});
it("renders a large comma-grouped total unchanged in the total tokens card", async () => {
@@ -1623,6 +1655,12 @@ describe("EcosystemArea", () => {
expect(screen.getByTestId("cc-ecosystem-pie")).toBeTruthy();
expect(screen.getByTestId("cc-ecosystem-line")).toBeTruthy();
expect(screen.getByRole("img", { name: "Task share by model" })).toBeTruthy();
const ecosystemBar = screen.getByRole("list", { name: "Tasks per model" }) as HTMLElement;
expect(providerIconIn(ecosystemBar, "openai")).toBeTruthy();
expect(providerIconIn(ecosystemBar, "anthropic")).toBeTruthy();
expect(providerIconIn(screen.getByTestId("cc-ecosystem-pie"), "openai")).toBeNull();
expect(providerIconIn(screen.getByTestId("cc-ecosystem-pie"), "anthropic")).toBeNull();
expect(screen.getByRole("img", { name: "openai gpt-4o: 3" })).toBeTruthy();
expect(screen.getByRole("img", { name: "Ecosystem trend" })).toBeTruthy();
expect(screen.getByTestId("cc-ecosystem-plugins-unavailable").textContent).toBe("—");
expect(screen.getByTestId("cc-area-ecosystem").textContent).not.toContain("NaN");

View File

@@ -1,10 +1,21 @@
import { ProviderIcon } from "../../ProviderIcon";
import "./charts.css";
/*
* FNXC:CommandCenterModelLabels 2026-06-25-00:00:
* Command Center model-heavy bar analytics must render provider marks immediately before model names. Keep provider icons opt-in on BarDatum so non-model chart rows (tool categories, activity series, plugin labels) continue rendering as plain strings with no provider icon shell.
*/
function hasIconProvider(datum: BarDatum): boolean {
return Object.prototype.hasOwnProperty.call(datum, "iconProvider");
}
export interface BarDatum {
label: string;
value: number;
/** Optional display string for the value (defaults to the number). */
valueLabel?: string;
/** Optional ProviderIcon provider key for model-name rows; omitted for non-model charts. */
iconProvider?: string;
}
export interface BarProps {
@@ -35,18 +46,24 @@ export function Bar({ data, max, ariaLabel }: BarProps) {
return (
<ul className="cc-bar-chart" role="list" aria-label={ariaLabel}>
{data.map((d) => {
{data.map((d, index) => {
const width = safeWidthPercent(d.value, computedMax);
const valueText = d.valueLabel ?? String(Number.isFinite(d.value) ? d.value : 0);
const labelText = d.label;
const iconLabelText = d.iconProvider && d.iconProvider !== labelText ? `${d.iconProvider} ${labelText}` : labelText;
const accessibleLabel = hasIconProvider(d) ? iconLabelText : labelText;
return (
<li key={d.label} className="cc-bar-row">
<span className="cc-bar-label">{d.label}</span>
<li key={`${accessibleLabel}-${d.value}-${index}`} className="cc-bar-row">
<span className="cc-bar-label">
{hasIconProvider(d) ? <ProviderIcon provider={d.iconProvider ?? ""} size="sm" /> : null}
<span className="cc-bar-label-text">{d.label}</span>
</span>
<div className="cc-bar-track">
<div
className="cc-bar-fill"
style={{ width: `${width}%` }}
role="img"
aria-label={`${d.label}: ${valueText}`}
aria-label={`${accessibleLabel}: ${valueText}`}
/>
</div>
<span className="cc-bar-value">{valueText}</span>

View File

@@ -57,6 +57,9 @@ FN-6680 re-checked FN-6664 in a real Blink layout engine because jsdom does not
.cc-bar-label {
min-inline-size: 0;
display: inline-flex;
align-items: center;
gap: var(--space-xs);
font-size: 0.8125rem;
color: var(--text-muted);
overflow: hidden;
@@ -64,6 +67,37 @@ FN-6680 re-checked FN-6664 in a real Blink layout engine because jsdom does not
white-space: nowrap;
}
.cc-bar-label > .provider-icon {
flex: 0 0 auto;
}
.cc-bar-label-text {
min-inline-size: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cc-model-label {
min-inline-size: 0;
max-inline-size: 100%;
display: inline-flex;
align-items: center;
gap: var(--space-xs);
vertical-align: middle;
}
.cc-model-label .provider-icon {
flex: 0 0 auto;
}
.cc-model-label-text {
min-inline-size: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cc-bar-track {
min-inline-size: 0;
position: relative;
@@ -473,6 +507,10 @@ On narrow mobile, Recharts' default legend measured to 0px wide and positioned i
.cc-recharts-empty {
block-size: calc(var(--space-2xl) * 8 + var(--space-md));
}
.cc-model-label {
max-inline-size: 100%;
}
}
/* ---- RadialGauge ---- */
@@ -679,6 +717,10 @@ On narrow mobile, Recharts' default legend measured to 0px wide and positioned i
align-items: start;
}
.cc-bar-label {
max-inline-size: 100%;
}
.cc-bar-value {
grid-column: 1 / -1;
justify-self: end;

View File

@@ -0,0 +1,32 @@
import { describe, expect, it } from "vitest";
import { inferProviderIconKey } from "../providerIconKey";
describe("inferProviderIconKey", () => {
it.each([
["claude-sonnet-4-5", "anthropic"],
["Anthropic Claude", "anthropic"],
["gpt-4o", "openai"],
["openai-codex", "openai"],
["gemini-2.5-pro", "google"],
["google-antigravity", "google"],
["ollama/llama3", "ollama"],
["minimax-text-01", "minimax"],
["zhipu-glm-4", "zai"],
["zai/glm-4.5", "zai"],
["kimi-k2", "kimi"],
["moonshot-v1", "kimi"],
["amazon-bedrock-claude", "anthropic"],
["bedrock-titan", "bedrock"],
["xai-grok-4", "xai"],
["opencode", "opencode"],
["github copilot", "github-copilot"],
["copilot-gpt-4", "openai"],
])("maps %s to %s", (input, expected) => {
expect(inferProviderIconKey(input)).toBe(expected);
});
it("returns unknown ids unchanged so ProviderIcon can render its fallback", () => {
expect(inferProviderIconKey("custom-model-v1")).toBe("custom-model-v1");
expect(inferProviderIconKey("")).toBe("");
});
});

View File

@@ -0,0 +1,45 @@
/*
FNXC:ProviderIcons 2026-06-25-00:00:
Command Center model analytics group rows by model id only, so dashboard model-name surfaces must infer the provider icon from the model/provider text before handing off to ProviderIcon's fallback-safe renderer.
*/
export function inferProviderIconKey(modelOrProviderName: string): string {
const normalized = modelOrProviderName.toLowerCase();
// Map common provider/model names to their icon keys.
if (normalized.includes("claude") || normalized.includes("anthropic")) {
return "anthropic";
}
if (normalized.includes("codex") || normalized.includes("openai") || normalized.includes("gpt")) {
return "openai";
}
if (normalized.includes("gemini") || normalized.includes("google") || normalized.includes("antigravity")) {
return "google";
}
if (normalized.includes("ollama")) {
return "ollama";
}
if (normalized.includes("minimax")) {
return "minimax";
}
if (normalized.includes("zai") || normalized.includes("zhipu")) {
return "zai";
}
if (normalized.includes("kimi") || normalized.includes("moonshot")) {
return "kimi";
}
if (normalized.includes("bedrock") || normalized.includes("amazon")) {
return "bedrock";
}
if (normalized.includes("xai") || normalized.includes("grok")) {
return "xai";
}
if (normalized.includes("opencode")) {
return "opencode";
}
if (normalized.includes("copilot") || normalized === "github copilot") {
return "github-copilot";
}
// Return the original name as fallback (ProviderIcon will show a default icon).
return modelOrProviderName;
}