mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-07-09 17:18:24 +00:00
Show provider icons in the provider list
This commit is contained in:
parent
088ec97e7b
commit
9163e306c0
3 changed files with 90 additions and 18 deletions
|
|
@ -12,7 +12,7 @@ import {
|
||||||
providerAccountConnectorsTextWithNewApiUserBalanceTemplate, providerAccountSnapshotCredentialLabel, providerAccountSnapshotLabel, ProviderAccountTestPath,
|
providerAccountConnectorsTextWithNewApiUserBalanceTemplate, providerAccountSnapshotCredentialLabel, providerAccountSnapshotLabel, ProviderAccountTestPath,
|
||||||
ProviderAccountTestResult, providerBaseUrl, providerCapabilitiesSummary, ProviderCredentialDraft, ProviderDeepLinkPayload, ProviderDeepLinkRequest, providerDraftSafetyIssue, providerCredentialDraftPatchFromJson, providerHttpJsonConnectorFromDraft,
|
ProviderAccountTestResult, providerBaseUrl, providerCapabilitiesSummary, ProviderCredentialDraft, ProviderDeepLinkPayload, ProviderDeepLinkRequest, providerDraftSafetyIssue, providerCredentialDraftPatchFromJson, providerHttpJsonConnectorFromDraft,
|
||||||
ProviderConnectivityCheckReport, providerDeepLinkDisplayIcon, providerListItemKey, providerMatchesQuery, ProviderPreset, providerPresetIconUrls, providerProbeHasSupportedProtocol,
|
ProviderConnectivityCheckReport, providerDeepLinkDisplayIcon, providerListItemKey, providerMatchesQuery, ProviderPreset, providerPresetIconUrls, providerProbeHasSupportedProtocol,
|
||||||
providerModelDisplayName, providerModelDisplayTitle, providerSelectableProtocolsFromProbe, providerUsageFieldPatch, ProviderUsageFieldTarget, providerUsageMethodOptions, Search, SelectControl,
|
providerDisplayIcon, providerModelDisplayName, providerModelDisplayTitle, providerSelectableProtocolsFromProbe, providerUsageFieldPatch, ProviderUsageFieldTarget, providerUsageMethodOptions, Search, SelectControl,
|
||||||
resolveProviderDeepLinkPreset, ShieldCheck, splitLines, splitModelTagInput, Switch, Textarea, translatedProviderProtocolLabel, translateOptions,
|
resolveProviderDeepLinkPreset, ShieldCheck, splitLines, splitModelTagInput, Switch, Textarea, translatedProviderProtocolLabel, translateOptions,
|
||||||
translateProbeProtocolMessage, Trash2, uniqueProviderName, uniqueProviderProtocols, useAppErrorText, useAppText, useEffect, useMemo,
|
translateProbeProtocolMessage, Trash2, uniqueProviderName, uniqueProviderProtocols, useAppErrorText, useAppText, useEffect, useMemo,
|
||||||
useRef, useState, X, isPlainRecord
|
useRef, useState, X, isPlainRecord
|
||||||
|
|
@ -111,24 +111,25 @@ export function ProvidersView({ accountSnapshots, addProvider, editProvider, not
|
||||||
</div>
|
</div>
|
||||||
<div className="divide-y divide-border/60">
|
<div className="divide-y divide-border/60">
|
||||||
<AnimatePresence initial={false}>
|
<AnimatePresence initial={false}>
|
||||||
{visibleProviders.map(({ provider, index }) => {
|
{visibleProviders.map(({ provider, index }) => {
|
||||||
const itemKey = providerListItemKey(provider, index);
|
const itemKey = providerListItemKey(provider, index);
|
||||||
const expanded = expandedProviders.has(itemKey);
|
const expanded = expandedProviders.has(itemKey);
|
||||||
const providerAccountSnapshots = accountSnapshotsByProvider.get(provider.name) ?? [];
|
const providerAccountSnapshots = accountSnapshotsByProvider.get(provider.name) ?? [];
|
||||||
return (
|
const providerIconUrl = providerDisplayIcon(provider);
|
||||||
<AnimatedListItem key={itemKey}>
|
return (
|
||||||
<div
|
<AnimatedListItem key={itemKey}>
|
||||||
className="grid min-h-[58px] cursor-pointer grid-cols-[minmax(160px,0.8fr)_minmax(220px,1fr)_minmax(160px,0.7fr)_minmax(150px,0.65fr)_80px_84px] items-center gap-3 px-4 py-2.5 transition-colors hover:bg-muted/35"
|
<div
|
||||||
onClick={() => toggleProvider(provider, index)}
|
className="grid min-h-[58px] cursor-pointer grid-cols-[minmax(160px,0.8fr)_minmax(220px,1fr)_minmax(160px,0.7fr)_minmax(150px,0.65fr)_80px_84px] items-center gap-3 px-4 py-2.5 transition-colors hover:bg-muted/35"
|
||||||
onKeyDown={(event) => {
|
onClick={() => toggleProvider(provider, index)}
|
||||||
if (event.key === "Enter" || event.key === " ") {
|
onKeyDown={(event) => {
|
||||||
event.preventDefault();
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
toggleProvider(provider, index);
|
event.preventDefault();
|
||||||
}
|
toggleProvider(provider, index);
|
||||||
}}
|
}
|
||||||
role="button"
|
}}
|
||||||
tabIndex={0}
|
role="button"
|
||||||
>
|
tabIndex={0}
|
||||||
|
>
|
||||||
<div className="flex min-w-0 items-center gap-2">
|
<div className="flex min-w-0 items-center gap-2">
|
||||||
<button
|
<button
|
||||||
aria-expanded={expanded}
|
aria-expanded={expanded}
|
||||||
|
|
@ -143,6 +144,7 @@ export function ProvidersView({ accountSnapshots, addProvider, editProvider, not
|
||||||
>
|
>
|
||||||
{expanded ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />}
|
{expanded ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />}
|
||||||
</button>
|
</button>
|
||||||
|
<ProviderPresetIcon className="h-8 w-8 rounded-md" iconUrl={providerIconUrl} />
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="truncate text-[12px] font-semibold">{provider.name || t("Unnamed")}</div>
|
<div className="truncate text-[12px] font-semibold">{provider.name || t("Unnamed")}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -772,6 +772,16 @@ export function providerDeepLinkDisplayIcon(payload: ProviderDeepLinkPayload): s
|
||||||
return presetIcon || payload.icon?.trim() || "";
|
return presetIcon || payload.icon?.trim() || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function providerDisplayIcon(provider: GatewayProviderConfig): string {
|
||||||
|
const icon = provider.icon?.trim();
|
||||||
|
if (icon) {
|
||||||
|
return icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
const preset = findProviderPresetByBaseUrl(providerBaseUrl(provider));
|
||||||
|
return preset ? providerPresetIconUrls[preset.id] ?? "" : "";
|
||||||
|
}
|
||||||
|
|
||||||
export type ProviderDeepLinkCatalogModelsResolution = {
|
export type ProviderDeepLinkCatalogModelsResolution = {
|
||||||
modelDisplayNames?: Record<string, string>;
|
modelDisplayNames?: Record<string, string>;
|
||||||
models: string[];
|
models: string[];
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,23 @@
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
|
import * as React from "react";
|
||||||
|
import { renderToStaticMarkup } from "react-dom/server";
|
||||||
import { newApiKeyUsageAccountConfig } from "../../packages/core/src/providers/new-api.ts";
|
import { newApiKeyUsageAccountConfig } from "../../packages/core/src/providers/new-api.ts";
|
||||||
import { geminiProviderPreset } from "../../packages/core/src/providers/presets/gemini/index.ts";
|
import { geminiProviderPreset } from "../../packages/core/src/providers/presets/gemini/index.ts";
|
||||||
|
import { ProvidersView } from "../../packages/ui/src/pages/home/components/providers.tsx";
|
||||||
import {
|
import {
|
||||||
applyProviderProbeResult,
|
applyProviderProbeResult,
|
||||||
createProviderDraft,
|
createProviderDraft,
|
||||||
|
providerDisplayIcon,
|
||||||
providerAccountConnectorsTextWithNewApiUserBalanceTemplate,
|
providerAccountConnectorsTextWithNewApiUserBalanceTemplate,
|
||||||
|
providerPresetIconUrls,
|
||||||
providerProtocolOptions,
|
providerProtocolOptions,
|
||||||
providerProbeCandidates,
|
providerProbeCandidates,
|
||||||
setProviderPresets
|
setProviderPresets
|
||||||
} from "../../packages/ui/src/pages/home/shared/index.tsx";
|
} from "../../packages/ui/src/pages/home/shared/index.tsx";
|
||||||
|
import { installBrowserGlobals } from "./fixtures.ts";
|
||||||
|
|
||||||
|
installBrowserGlobals();
|
||||||
|
|
||||||
test("Gemini preset keeps full protocol probing candidates", () => {
|
test("Gemini preset keeps full protocol probing candidates", () => {
|
||||||
setProviderPresets([geminiProviderPreset]);
|
setProviderPresets([geminiProviderPreset]);
|
||||||
|
|
@ -112,6 +120,58 @@ test("provider probe result applies detected New API key quota account connector
|
||||||
assert.equal(connectors[0].mapping.meters[0].remaining, "$.data.total_available");
|
assert.equal(connectors[0].mapping.meters[0].remaining, "$.data.total_available");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("provider display icon prefers custom icons and falls back to preset icons", () => {
|
||||||
|
setProviderPresets([geminiProviderPreset]);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
providerDisplayIcon({
|
||||||
|
api_base_url: "https://custom.example/v1",
|
||||||
|
icon: "https://custom.example/icon.png",
|
||||||
|
models: [],
|
||||||
|
name: "Custom Provider",
|
||||||
|
type: "openai_chat_completions"
|
||||||
|
}),
|
||||||
|
"https://custom.example/icon.png"
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
providerDisplayIcon({
|
||||||
|
api_base_url: "https://generativelanguage.googleapis.com",
|
||||||
|
models: [],
|
||||||
|
name: "Google Gemini",
|
||||||
|
type: "gemini_generate_content"
|
||||||
|
}),
|
||||||
|
providerPresetIconUrls.gemini
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("ProvidersView renders configured provider icons in the list", () => {
|
||||||
|
const iconUrl = "https://custom.example/icon.png";
|
||||||
|
const html = renderToStaticMarkup(
|
||||||
|
React.createElement(ProvidersView, {
|
||||||
|
accountSnapshots: [],
|
||||||
|
addProvider: () => undefined,
|
||||||
|
editProvider: () => undefined,
|
||||||
|
notify: () => undefined,
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
index: 0,
|
||||||
|
provider: {
|
||||||
|
api_base_url: "https://custom.example/v1",
|
||||||
|
icon: iconUrl,
|
||||||
|
models: ["custom-model"],
|
||||||
|
name: "Custom Provider",
|
||||||
|
type: "openai_chat_completions"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
removeProvider: () => undefined
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.match(html, /Custom Provider/);
|
||||||
|
assert.match(html, /src="https:\/\/custom\.example\/icon\.png"/);
|
||||||
|
});
|
||||||
|
|
||||||
test("New API user balance template adds configurable user self connector", () => {
|
test("New API user balance template adds configurable user self connector", () => {
|
||||||
const account = newApiKeyUsageAccountConfig("https://gateway.example/v1");
|
const account = newApiKeyUsageAccountConfig("https://gateway.example/v1");
|
||||||
const text = providerAccountConnectorsTextWithNewApiUserBalanceTemplate(
|
const text = providerAccountConnectorsTextWithNewApiUserBalanceTemplate(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue