mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-07-09 17:18:24 +00:00
Persist provider model display names
This commit is contained in:
parent
c583f69666
commit
7cfe854336
12 changed files with 309 additions and 64 deletions
|
|
@ -1083,6 +1083,7 @@ function parseProviders(value: unknown): GatewayProviderConfig[] | undefined {
|
|||
const models = Array.isArray(item.models)
|
||||
? item.models.map((model) => readString(model)).filter((model): model is string => Boolean(model))
|
||||
: [];
|
||||
const modelDisplayNames = parseModelDisplayNames(item.modelDisplayNames ?? item.model_display_names, models);
|
||||
|
||||
if (!name) {
|
||||
return undefined;
|
||||
|
|
@ -1103,6 +1104,7 @@ function parseProviders(value: unknown): GatewayProviderConfig[] | undefined {
|
|||
extraHeaders: item.extraHeaders,
|
||||
icon: readString(item.icon),
|
||||
id: readString(item.id),
|
||||
modelDisplayNames,
|
||||
models,
|
||||
name,
|
||||
provider: readString(item.provider),
|
||||
|
|
@ -1116,6 +1118,22 @@ function parseProviders(value: unknown): GatewayProviderConfig[] | undefined {
|
|||
return withProviderIds(providers);
|
||||
}
|
||||
|
||||
function parseModelDisplayNames(value: unknown, models: string[]): Record<string, string> | undefined {
|
||||
if (!isObject(value)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const modelIds = new Set(models);
|
||||
const entries = Object.entries(value)
|
||||
.map(([rawModel, rawDisplayName]) => [rawModel.trim(), readString(rawDisplayName)] as const)
|
||||
.filter((entry): entry is [string, string] => {
|
||||
const [model, displayName] = entry;
|
||||
return Boolean(model && displayName && modelIds.has(model) && model !== displayName);
|
||||
});
|
||||
|
||||
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
||||
}
|
||||
|
||||
function withProviderIds(providers: GatewayProviderConfig[]): GatewayProviderConfig[] {
|
||||
const counts = new Map<string, number>();
|
||||
return providers.map((provider) => {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@ import type {
|
|||
ProviderAccountMappingConfig
|
||||
} from "../../shared/app";
|
||||
import {
|
||||
isRecord,
|
||||
missingCandidate,
|
||||
modelDisplayNamesForModels,
|
||||
providerInternalNamePlaceholder,
|
||||
providerNamePlaceholder,
|
||||
providerNameSlugPlaceholder,
|
||||
|
|
@ -17,7 +19,6 @@ import {
|
|||
readString,
|
||||
uniqueProviderName,
|
||||
uniqueStrings,
|
||||
isRecord,
|
||||
type OAuthTokenSet
|
||||
} from "./shared";
|
||||
|
||||
|
|
@ -26,6 +27,11 @@ export const codexDefaultBaseUrl = "https://chatgpt.com/backend-api/codex";
|
|||
const codexAccountBaseUrl = "https://chatgpt.com/backend-api";
|
||||
const codexDefaultModels = ["gpt-5-codex"];
|
||||
|
||||
type LocalAgentModelCatalog = {
|
||||
modelDisplayNames?: Record<string, string>;
|
||||
models: string[];
|
||||
};
|
||||
|
||||
const codexAccountRateLimitMapping: ProviderAccountMappingConfig = {
|
||||
meters: [
|
||||
{
|
||||
|
|
@ -93,21 +99,22 @@ const codexAccountTokenUsageMapping: ProviderAccountMappingConfig = {
|
|||
|
||||
export function codexCandidate(): LocalAgentProviderCandidate {
|
||||
const auth = readCodexAuth();
|
||||
const models = readCodexModels();
|
||||
const catalog = readCodexModelCatalog();
|
||||
if (auth?.refreshToken || auth?.accessToken) {
|
||||
return {
|
||||
detail: "ChatGPT login detected. Click Import to add it as a gateway provider.",
|
||||
id: "codex-api",
|
||||
importable: true,
|
||||
kind: "codex",
|
||||
models,
|
||||
modelDisplayNames: catalog.modelDisplayNames,
|
||||
models: catalog.models,
|
||||
name: "Codex API",
|
||||
protocol: "openai_responses",
|
||||
sourceFile: auth.sourceFile,
|
||||
status: "available"
|
||||
};
|
||||
}
|
||||
return missingCandidate("codex", "codex-api", "Codex API", "openai_responses", models);
|
||||
return missingCandidate("codex", "codex-api", "Codex API", "openai_responses", catalog.models, catalog.modelDisplayNames);
|
||||
}
|
||||
|
||||
export function importCodexProvider(candidate: LocalAgentProviderCandidate, providerNames: string[]): LocalAgentProviderImportResult {
|
||||
|
|
@ -197,13 +204,31 @@ function codexBackendRequestTransform(): Record<string, unknown> {
|
|||
};
|
||||
}
|
||||
|
||||
function readCodexModels(): string[] {
|
||||
function readCodexModelCatalog(): LocalAgentModelCatalog {
|
||||
const modelsFile = path.join(os.homedir(), ".codex", "models_cache.json");
|
||||
const record = readJsonRecord(modelsFile);
|
||||
const models = Array.isArray(record?.models)
|
||||
? record.models.map((model) => isRecord(model) ? readString(model.slug) || readString(model.id) || readString(model.name) : readString(model))
|
||||
: [];
|
||||
return uniqueStrings([...models, ...codexDefaultModels]);
|
||||
const models: string[] = [];
|
||||
const modelDisplayNames: Record<string, string> = {};
|
||||
for (const item of Array.isArray(record?.models) ? record.models : []) {
|
||||
const model = isRecord(item)
|
||||
? readString(item.slug) || readString(item.id) || readString(item.name)
|
||||
: readString(item);
|
||||
if (!model) {
|
||||
continue;
|
||||
}
|
||||
models.push(model);
|
||||
if (isRecord(item)) {
|
||||
const displayName = readString(item.display_name) || readString(item.displayName) || readString(item.label) || readString(item.name);
|
||||
if (displayName && displayName !== model) {
|
||||
modelDisplayNames[model] = displayName;
|
||||
}
|
||||
}
|
||||
}
|
||||
const uniqueModels = uniqueStrings([...models, ...codexDefaultModels]);
|
||||
return {
|
||||
modelDisplayNames: modelDisplayNamesForModels(modelDisplayNames, uniqueModels),
|
||||
models: uniqueModels
|
||||
};
|
||||
}
|
||||
|
||||
function readCodexIdTokenClaims(idToken: string | undefined): { accountId?: string; isFedrampAccount?: boolean } {
|
||||
|
|
|
|||
|
|
@ -30,13 +30,15 @@ export function missingCandidate(
|
|||
id: string,
|
||||
name: string,
|
||||
protocol: GatewayProviderProtocol,
|
||||
models: string[]
|
||||
models: string[],
|
||||
modelDisplayNames?: Record<string, string>
|
||||
): LocalAgentProviderCandidate {
|
||||
return {
|
||||
detail: "No local login state was found for this agent.",
|
||||
id,
|
||||
importable: false,
|
||||
kind,
|
||||
modelDisplayNames: modelDisplayNamesForModels(modelDisplayNames, models),
|
||||
models,
|
||||
name,
|
||||
protocol,
|
||||
|
|
@ -50,16 +52,29 @@ export function providerPayload(
|
|||
baseUrl: string,
|
||||
account?: ProviderAccountConfig
|
||||
): ProviderDeepLinkPayload {
|
||||
const models = uniqueStrings(candidate.models).slice(0, 24);
|
||||
return {
|
||||
account,
|
||||
apiKey: localAgentProviderApiKey,
|
||||
baseUrl,
|
||||
models: uniqueStrings(candidate.models).slice(0, 24),
|
||||
modelDisplayNames: modelDisplayNamesForModels(candidate.modelDisplayNames, models),
|
||||
models,
|
||||
name,
|
||||
protocol: candidate.protocol
|
||||
};
|
||||
}
|
||||
|
||||
export function modelDisplayNamesForModels(
|
||||
value: Record<string, string> | undefined,
|
||||
models: string[]
|
||||
): Record<string, string> | undefined {
|
||||
const modelIds = new Set(models);
|
||||
const entries = Object.entries(value ?? {})
|
||||
.map(([rawModel, rawDisplayName]) => [rawModel.trim(), rawDisplayName.trim()] as const)
|
||||
.filter(([model, displayName]) => model && displayName && model !== displayName && modelIds.has(model));
|
||||
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
||||
}
|
||||
|
||||
export function bearerAuthPlugin(
|
||||
suffix: string,
|
||||
token: string,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
isLoopbackUrl,
|
||||
isRecord,
|
||||
missingCandidate,
|
||||
modelDisplayNamesForModels,
|
||||
providerInternalNamePlaceholder,
|
||||
providerPayload,
|
||||
readJsonRecord,
|
||||
|
|
@ -25,12 +26,18 @@ import {
|
|||
type ZcodeConfiguredProvider = {
|
||||
apiKey: string;
|
||||
baseUrl: string;
|
||||
modelDisplayNames?: Record<string, string>;
|
||||
models: string[];
|
||||
name: string;
|
||||
providerId: string;
|
||||
sourceFile: string;
|
||||
};
|
||||
|
||||
type LocalAgentModelCatalog = {
|
||||
modelDisplayNames?: Record<string, string>;
|
||||
models: string[];
|
||||
};
|
||||
|
||||
const zcodeDefaultModels = ["GLM-5.2", "GLM-5-Turbo"];
|
||||
const zcodeDefaultBaseUrl = "https://zcode.z.ai/api/v1/zcode-plan/anthropic";
|
||||
|
||||
|
|
@ -40,12 +47,16 @@ export function zcodeCandidate(): LocalAgentProviderCandidate {
|
|||
const models = configuredProvider?.models.length
|
||||
? configuredProvider.models
|
||||
: zcodeRuntime.models.length > 0 ? zcodeRuntime.models : zcodeDefaultModels;
|
||||
const modelDisplayNames = configuredProvider?.models.length
|
||||
? configuredProvider.modelDisplayNames
|
||||
: zcodeRuntime.modelDisplayNames;
|
||||
if (configuredProvider) {
|
||||
return {
|
||||
detail: "ZCode provider API key detected in local ZCode config. Click Import to add it as a gateway provider.",
|
||||
id: "zcode-api",
|
||||
importable: true,
|
||||
kind: "zcode",
|
||||
modelDisplayNames,
|
||||
models,
|
||||
name: "ZCode API",
|
||||
protocol: "anthropic_messages",
|
||||
|
|
@ -61,6 +72,7 @@ export function zcodeCandidate(): LocalAgentProviderCandidate {
|
|||
id: "zcode-api",
|
||||
importable: false,
|
||||
kind: "zcode",
|
||||
modelDisplayNames,
|
||||
models,
|
||||
name: "ZCode API",
|
||||
protocol: "anthropic_messages",
|
||||
|
|
@ -68,7 +80,7 @@ export function zcodeCandidate(): LocalAgentProviderCandidate {
|
|||
status: "locked"
|
||||
};
|
||||
}
|
||||
return missingCandidate("zcode", "zcode-api", "ZCode API", "anthropic_messages", models);
|
||||
return missingCandidate("zcode", "zcode-api", "ZCode API", "anthropic_messages", models, modelDisplayNames);
|
||||
}
|
||||
|
||||
export function importZcodeProvider(candidate: LocalAgentProviderCandidate, providerNames: string[]): LocalAgentProviderImportResult {
|
||||
|
|
@ -77,7 +89,11 @@ export function importZcodeProvider(candidate: LocalAgentProviderCandidate, prov
|
|||
throw new Error("ZCode provider API key was not found in ZCode config.");
|
||||
}
|
||||
const provider = providerPayload(
|
||||
{ ...candidate, models: configuredProvider.models.length > 0 ? configuredProvider.models : candidate.models },
|
||||
{
|
||||
...candidate,
|
||||
modelDisplayNames: configuredProvider.models.length > 0 ? configuredProvider.modelDisplayNames : candidate.modelDisplayNames,
|
||||
models: configuredProvider.models.length > 0 ? configuredProvider.models : candidate.models
|
||||
},
|
||||
uniqueProviderName(providerNames, "ZCode API"),
|
||||
configuredProvider.baseUrl,
|
||||
zcodeProviderAccountConfig(configuredProvider.baseUrl)
|
||||
|
|
@ -152,7 +168,7 @@ function readZcodeConfiguredProviders(sourceFile: string): ZcodeConfiguredProvid
|
|||
return [{
|
||||
apiKey,
|
||||
baseUrl,
|
||||
models: zcodeProviderModels(value),
|
||||
...zcodeProviderModelCatalog(value),
|
||||
name: readString(value.name) || providerId,
|
||||
providerId,
|
||||
sourceFile
|
||||
|
|
@ -160,7 +176,7 @@ function readZcodeConfiguredProviders(sourceFile: string): ZcodeConfiguredProvid
|
|||
});
|
||||
}
|
||||
|
||||
function readZcodeRuntime(): { baseUrl: string; models: string[] } {
|
||||
function readZcodeRuntime(): { baseUrl: string } & LocalAgentModelCatalog {
|
||||
const cache = readJsonRecord(path.join(os.homedir(), ".zcode", "v2", "bots-model-cache.v2.json"));
|
||||
const providers = Array.isArray(cache?.providers)
|
||||
? cache.providers.filter((provider): provider is Record<string, unknown> => isRecord(provider))
|
||||
|
|
@ -174,23 +190,63 @@ function readZcodeRuntime(): { baseUrl: string; models: string[] } {
|
|||
return text.includes("zcode") || text.includes("z.ai") || text.includes("bigmodel");
|
||||
});
|
||||
const baseUrl = readString(isRecord(provider?.endpoints) ? provider?.endpoints.baseURL : undefined) || zcodeDefaultBaseUrl;
|
||||
const models = Array.isArray(provider?.models)
|
||||
? provider.models.map((model) => isRecord(model) ? readString(model.id) || readString(model.name) : readString(model))
|
||||
: [];
|
||||
const catalog = zcodeProviderModelCatalog(provider ?? {});
|
||||
const models = uniqueStrings([...catalog.models, ...zcodeDefaultModels]);
|
||||
return {
|
||||
baseUrl,
|
||||
models: uniqueStrings([...models, ...zcodeDefaultModels])
|
||||
modelDisplayNames: modelDisplayNamesForModels(catalog.modelDisplayNames, models),
|
||||
models
|
||||
};
|
||||
}
|
||||
|
||||
function zcodeProviderModels(provider: Record<string, unknown>): string[] {
|
||||
function zcodeProviderModelCatalog(provider: Record<string, unknown>): LocalAgentModelCatalog {
|
||||
const models: string[] = [];
|
||||
const modelDisplayNames: Record<string, string> = {};
|
||||
if (Array.isArray(provider.models)) {
|
||||
return uniqueStrings(provider.models.map((model) => isRecord(model) ? readString(model.id) || readString(model.name) : readString(model)));
|
||||
for (const item of provider.models) {
|
||||
const model = isRecord(item)
|
||||
? readString(item.id) || readString(item.name)
|
||||
: readString(item);
|
||||
if (!model) {
|
||||
continue;
|
||||
}
|
||||
models.push(model);
|
||||
if (isRecord(item)) {
|
||||
const displayName = readString(item.displayName) || readString(item.display_name) || readString(item.label) || readString(item.name);
|
||||
if (displayName && displayName !== model) {
|
||||
modelDisplayNames[model] = displayName;
|
||||
}
|
||||
}
|
||||
}
|
||||
const uniqueModels = uniqueStrings(models);
|
||||
return {
|
||||
modelDisplayNames: modelDisplayNamesForModels(modelDisplayNames, uniqueModels),
|
||||
models: uniqueModels
|
||||
};
|
||||
}
|
||||
if (isRecord(provider.models)) {
|
||||
return uniqueStrings(Object.entries(provider.models).map(([key, value]) => isRecord(value) ? readString(value.id) || key : key));
|
||||
for (const [key, value] of Object.entries(provider.models)) {
|
||||
const model = isRecord(value) ? readString(value.id) || key : key;
|
||||
if (!model) {
|
||||
continue;
|
||||
}
|
||||
models.push(model);
|
||||
if (isRecord(value)) {
|
||||
const displayName = readString(value.displayName) || readString(value.display_name) || readString(value.label) || readString(value.name);
|
||||
if (displayName && displayName !== model) {
|
||||
modelDisplayNames[model] = displayName;
|
||||
}
|
||||
}
|
||||
}
|
||||
const uniqueModels = uniqueStrings(models);
|
||||
return {
|
||||
modelDisplayNames: modelDisplayNamesForModels(modelDisplayNames, uniqueModels),
|
||||
models: uniqueModels
|
||||
};
|
||||
}
|
||||
return [];
|
||||
return {
|
||||
models: []
|
||||
};
|
||||
}
|
||||
|
||||
function isZcodeModelProvider(providerId: string, provider: Record<string, unknown>): boolean {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import {
|
|||
isCursorProxyPluginConfig, isMacPlatform, isPlainRecord, isProfileDraftSubmittable, isProviderNameDuplicate, isProviderProbeCandidateReady,
|
||||
isTraySupportedPlatform,
|
||||
isRoutingRewriteDraftRowValid,
|
||||
LayoutGroup, mergeModelDisplayNames, mergeProviderCapabilities, mergeProviderModelLists,
|
||||
LayoutGroup, mergeModelDisplayNames, mergeProviderCapabilities, mergeProviderModelLists, modelDisplayNamesForModels,
|
||||
navigation, NavigationId, normalizeApiKeys, normalizeBotGatewaySavedConfigs, normalizeConfig, normalizeLanguagePreference, normalizeObservabilityConfig, normalizeOverviewWidgets,
|
||||
normalizeProfileItem, normalizeProfileScope, normalizeProviderBaseUrl, normalizeRouterFallbackConfig, normalizeThemePreference, normalizeTrayBalanceProgressConfig, normalizeTrayIconPreference,
|
||||
normalizeTrayWidgets, normalizeTrayWindowModules, normalizeVirtualModelDraftPatch, numberValue, OnboardingReadinessOptions, OnboardingStepId, onboardingStepOrder,
|
||||
|
|
@ -1349,6 +1349,7 @@ function App() {
|
|||
}
|
||||
|
||||
const protocolsToSave = selectedProtocols.length > 0 ? selectedProtocols : [fallbackProtocol];
|
||||
const modelDisplayNames = modelDisplayNamesForModels(providerDraft.modelDisplayNames, models);
|
||||
const selectedProtocolSet = new Set(protocolsToSave);
|
||||
const capabilityCandidates = mergeProviderCapabilities(
|
||||
presetCapabilitiesFromDraft(providerDraft),
|
||||
|
|
@ -1416,6 +1417,7 @@ function App() {
|
|||
account: accountConfig,
|
||||
credentials: credentials.length > 0 ? credentials : undefined,
|
||||
icon: providerDraft.icon.trim() || undefined,
|
||||
modelDisplayNames,
|
||||
models,
|
||||
name: providerName,
|
||||
type: protocol
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import {
|
|||
DialogTitle, Field, GatewayProviderConfig, Info, Input, KeyValueRowsControl, LoaderCircle, motion,
|
||||
normalizeProfileScope, normalizeProfileSurface, parseProfileModelValue, Pencil, Plus, PopoverContent,
|
||||
profileAgentLabel, profileAgentOptions, ProfileConfig, profileModelDisplayValue, profileModelMatchesQuery, profileModelProviderMatchesQuery,
|
||||
profileModelProviderOptions, profileOpenSurfaces, profileScopeLabel, profileScopeOptions, profileSummaryItems, profileSurfaceLabel, profileSurfaceOptions,
|
||||
profileModelOptionDisplayName, profileModelProviderOptions, profileOpenSurfaces, profileScopeLabel, profileScopeOptions, profileSummaryItems, profileSurfaceLabel, profileSurfaceOptions,
|
||||
Play, Power, RefreshCw, Search, Select, SelectControl, Terminal, Toggle, translateOptions, Trash2, useAppErrorText, useAppText, type ProfileOpenSurface, type ProfileRuntimeStatus, type ReactNode, type VirtualModelProfileConfig,
|
||||
copyTextToClipboard,
|
||||
useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, X
|
||||
|
|
@ -516,7 +516,7 @@ function ProfileModelSelector({
|
|||
filteredProviders.find((provider) => provider.name === parsedValue.provider) ??
|
||||
filteredProviders[0];
|
||||
const filteredModels = activeProvider
|
||||
? activeProvider.models.filter((model) => profileModelMatchesQuery(activeProvider.name, model, query))
|
||||
? activeProvider.models.filter((model) => profileModelMatchesQuery(activeProvider.name, model, query, profileModelOptionDisplayName(activeProvider, model)))
|
||||
: [];
|
||||
const displayValue = profileModelDisplayValue(value, parsedValue, providers, placeholder, virtualModelProfiles);
|
||||
|
||||
|
|
@ -729,6 +729,7 @@ function ProfileModelSelector({
|
|||
) : null}
|
||||
{activeProvider && filteredModels.map((model) => {
|
||||
const selected = parsedValue.provider === activeProvider.name && parsedValue.model === model;
|
||||
const displayName = profileModelOptionDisplayName(activeProvider, model);
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
|
|
@ -739,7 +740,7 @@ function ProfileModelSelector({
|
|||
onClick={() => chooseModel(activeProvider.name, model)}
|
||||
type="button"
|
||||
>
|
||||
<span className="min-w-0 flex-1 truncate font-mono">{model}</span>
|
||||
<span className="min-w-0 flex-1 truncate" title={displayName}>{displayName}</span>
|
||||
{selected ? <Check className="h-3.5 w-3.5 shrink-0" /> : null}
|
||||
</button>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {
|
|||
providerAccountSnapshotCredentialLabel, providerAccountSnapshotLabel, ProviderAccountTestPath,
|
||||
ProviderAccountTestResult, providerBaseUrl, providerCapabilitiesSummary, ProviderCredentialDraft, ProviderDeepLinkPayload, ProviderDeepLinkRequest, providerDraftSafetyIssue, providerCredentialDraftPatchFromJson, providerHttpJsonConnectorFromDraft,
|
||||
ProviderConnectivityCheckReport, providerDeepLinkDisplayIcon, providerListItemKey, providerMatchesQuery, ProviderPreset, providerPresetIconUrls, providerProbeHasSupportedProtocol,
|
||||
providerSelectableProtocolsFromProbe, providerUsageFieldPatch, ProviderUsageFieldTarget, providerUsageMethodOptions, Search, SelectControl,
|
||||
providerModelDisplayName, providerModelDisplayTitle, providerSelectableProtocolsFromProbe, providerUsageFieldPatch, ProviderUsageFieldTarget, providerUsageMethodOptions, Search, SelectControl,
|
||||
resolveProviderDeepLinkPreset, ShieldCheck, splitLines, splitModelTagInput, Switch, Textarea, translatedProviderProtocolLabel, translateOptions,
|
||||
translateProbeProtocolMessage, Trash2, uniqueProviderName, uniqueProviderProtocols, useAppErrorText, useAppText, useEffect, useMemo,
|
||||
useRef, useState, X, isPlainRecord
|
||||
|
|
@ -216,16 +216,17 @@ export function ProvidersView({ accountSnapshots, addProvider, editProvider, not
|
|||
<div className="flex flex-wrap gap-2">
|
||||
{provider.models.map((model) => {
|
||||
const modelKey = `${itemKey}:${model}`;
|
||||
const displayName = providerModelDisplayName(provider, model);
|
||||
return (
|
||||
<button
|
||||
aria-label={`${t("Double click to copy")} ${model}`}
|
||||
className="inline-flex max-w-full items-center rounded-full border border-border bg-background px-2.5 py-1 font-mono text-[11px] leading-4 text-foreground transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/30"
|
||||
aria-label={`${t("Double click to copy")} ${displayName}`}
|
||||
className="inline-flex max-w-full items-center rounded-full border border-border bg-background px-2.5 py-1 text-[11px] leading-4 text-foreground transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/30"
|
||||
key={modelKey}
|
||||
onDoubleClick={() => void copyModel(model)}
|
||||
title={t("Double click to copy")}
|
||||
title={`${providerModelDisplayTitle(provider, model)} · ${t("Double click to copy")}`}
|
||||
type="button"
|
||||
>
|
||||
<span className="min-w-0 truncate">{model}</span>
|
||||
<span className="min-w-0 truncate">{displayName}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
|
@ -308,8 +309,8 @@ export function ModelsView({ config }: { config: AppConfig }) {
|
|||
key={row.key}
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<div className="truncate font-mono text-[12px] font-semibold text-foreground" title={row.model}>
|
||||
{row.model}
|
||||
<div className="truncate text-[12px] font-semibold text-foreground" title={row.displayName ?? row.model}>
|
||||
{row.displayName ?? row.model}
|
||||
</div>
|
||||
</div>
|
||||
</AnimatedListItem>
|
||||
|
|
@ -521,7 +522,9 @@ export function ProviderDeepLinkDialog({
|
|||
<div className="flex flex-wrap gap-2">
|
||||
{modelPreview.map((model) => (
|
||||
<Badge key={model} variant="outline">
|
||||
<span className="max-w-[210px] truncate font-mono">{model}</span>
|
||||
<span className="max-w-[210px] truncate" title={provider.modelDisplayNames?.[model] ?? model}>
|
||||
{provider.modelDisplayNames?.[model] ?? model}
|
||||
</span>
|
||||
</Badge>
|
||||
))}
|
||||
{provider.models.length > modelPreview.length ? (
|
||||
|
|
@ -943,6 +946,7 @@ function LocalAgentProviderImportPanel({
|
|||
baseUrl: result.provider.baseUrl,
|
||||
credentials: [],
|
||||
icon: result.provider.icon ?? "",
|
||||
modelDisplayNames: result.provider.modelDisplayNames,
|
||||
modelSearch: "",
|
||||
modelsText: result.provider.models.join("\n"),
|
||||
name: result.provider.name?.trim() || inferProviderNameFromBaseUrl(result.provider.baseUrl),
|
||||
|
|
@ -2321,22 +2325,25 @@ function ModelTagInput({
|
|||
/>
|
||||
{models.length > 0 ? (
|
||||
<div className="flex max-h-[120px] flex-wrap gap-1.5 overflow-auto">
|
||||
{models.map((model) => (
|
||||
<Badge className="max-w-full pr-1" key={model} variant="secondary">
|
||||
<span className="min-w-0 max-w-[260px] truncate" title={model}>
|
||||
{displayNames?.[model] ?? model}
|
||||
</span>
|
||||
<button
|
||||
aria-label={`${t("Remove model")} ${model}`}
|
||||
className="inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/25"
|
||||
onClick={() => removeModel(model)}
|
||||
title={t("Remove model")}
|
||||
type="button"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
{models.map((model) => {
|
||||
const displayName = displayNames?.[model] ?? model;
|
||||
return (
|
||||
<Badge className="max-w-full pr-1" key={model} variant="secondary">
|
||||
<span className="min-w-0 max-w-[260px] truncate" title={displayName}>
|
||||
{displayName}
|
||||
</span>
|
||||
<button
|
||||
aria-label={`${t("Remove model")} ${displayName}`}
|
||||
className="inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/25"
|
||||
onClick={() => removeModel(model)}
|
||||
title={t("Remove model")}
|
||||
type="button"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</Badge>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
|
|
@ -2401,7 +2408,7 @@ function ModelMultiSelect({
|
|||
checked && "border-primary bg-accent"
|
||||
)}
|
||||
key={model}
|
||||
title={displayName ? `${displayName} (${model})` : model}
|
||||
title={displayName ?? model}
|
||||
>
|
||||
<Checkbox checked={checked} onCheckedChange={() => toggleModel(model)} />
|
||||
<span className="min-w-0 flex-1 truncate">{displayName ?? model}</span>
|
||||
|
|
|
|||
|
|
@ -415,6 +415,7 @@ export function normalizeProfileClientModel(value: string | undefined): string {
|
|||
}
|
||||
|
||||
export type ProfileModelProviderOption = {
|
||||
modelDisplayNames?: Record<string, string>;
|
||||
models: string[];
|
||||
name: string;
|
||||
};
|
||||
|
|
@ -433,6 +434,7 @@ export function profileModelProviderOptions(
|
|||
const providerOptions = providers
|
||||
.filter((provider) => provider.name?.trim() && Array.isArray(provider.models))
|
||||
.map((provider) => ({
|
||||
modelDisplayNames: profileModelDisplayNamesForModels(provider.modelDisplayNames, provider.models),
|
||||
models: uniqueStrings(provider.models.filter(Boolean)),
|
||||
name: provider.name.trim()
|
||||
}))
|
||||
|
|
@ -485,10 +487,11 @@ export function profileModelDisplayValue(
|
|||
}
|
||||
const normalized = normalizeProfileClientModel(value);
|
||||
if (parsedValue.provider && parsedValue.model) {
|
||||
return `${parsedValue.provider}/${parsedValue.model}`;
|
||||
const provider = profileModelProviderOptions(providers, virtualModelProfiles).find((item) => item.name === parsedValue.provider);
|
||||
return `${parsedValue.provider}/${profileModelOptionDisplayName(provider, parsedValue.model)}`;
|
||||
}
|
||||
const provider = profileModelProviderOptions(providers, virtualModelProfiles).find((item) => item.models.includes(normalized));
|
||||
return provider ? `${provider.name}/${normalized}` : normalized;
|
||||
return provider ? `${provider.name}/${profileModelOptionDisplayName(provider, normalized)}` : normalized;
|
||||
}
|
||||
|
||||
export function profileModelProviderMatchesQuery(provider: ProfileModelProviderOption, query: string): boolean {
|
||||
|
|
@ -498,16 +501,36 @@ export function profileModelProviderMatchesQuery(provider: ProfileModelProviderO
|
|||
}
|
||||
return (
|
||||
provider.name.toLowerCase().includes(normalizedQuery) ||
|
||||
provider.models.some((model) => model.toLowerCase().includes(normalizedQuery))
|
||||
provider.models.some((model) =>
|
||||
model.toLowerCase().includes(normalizedQuery) ||
|
||||
profileModelOptionDisplayName(provider, model).toLowerCase().includes(normalizedQuery)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function profileModelMatchesQuery(providerName: string, model: string, query: string): boolean {
|
||||
export function profileModelMatchesQuery(providerName: string, model: string, query: string, displayName?: string): boolean {
|
||||
const normalizedQuery = query.trim().toLowerCase();
|
||||
if (!normalizedQuery) {
|
||||
return true;
|
||||
}
|
||||
return providerName.toLowerCase().includes(normalizedQuery) || model.toLowerCase().includes(normalizedQuery);
|
||||
return providerName.toLowerCase().includes(normalizedQuery) ||
|
||||
model.toLowerCase().includes(normalizedQuery) ||
|
||||
(displayName ?? "").toLowerCase().includes(normalizedQuery);
|
||||
}
|
||||
|
||||
export function profileModelOptionDisplayName(provider: ProfileModelProviderOption | undefined, model: string): string {
|
||||
return provider?.modelDisplayNames?.[model]?.trim() || model;
|
||||
}
|
||||
|
||||
function profileModelDisplayNamesForModels(
|
||||
value: Record<string, string> | undefined,
|
||||
models: string[]
|
||||
): Record<string, string> | undefined {
|
||||
const modelIds = new Set(models);
|
||||
const entries = Object.entries(value ?? {})
|
||||
.map(([rawModel, rawDisplayName]) => [rawModel.trim(), rawDisplayName.trim()] as const)
|
||||
.filter(([model, displayName]) => model && displayName && model !== displayName && modelIds.has(model));
|
||||
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
||||
}
|
||||
|
||||
export type BotGatewayAuthInputType = "text" | "password";
|
||||
|
|
|
|||
|
|
@ -390,12 +390,23 @@ export const localAgentProviderIconUrls: Record<LocalAgentProviderKind, string>
|
|||
};
|
||||
|
||||
export function createModelCatalogItems(config: AppConfig): ModelCatalogItem[] {
|
||||
const providerModels = config.Providers.flatMap((provider) => mergeProviderModelLists(provider.models));
|
||||
const providerModels: string[] = [];
|
||||
const displayNames: Record<string, string> = {};
|
||||
for (const provider of config.Providers) {
|
||||
for (const model of mergeProviderModelLists(provider.models)) {
|
||||
providerModels.push(model);
|
||||
const displayName = provider.modelDisplayNames?.[model]?.trim();
|
||||
if (displayName && !displayNames[model]) {
|
||||
displayNames[model] = displayName;
|
||||
}
|
||||
}
|
||||
}
|
||||
const virtualModels = (config.virtualModelProfiles ?? [])
|
||||
.filter(virtualModelIsCatalogVisible)
|
||||
.flatMap(virtualModelCatalogNames);
|
||||
|
||||
return uniqueStrings([...providerModels, ...virtualModels]).map((model, index) => ({
|
||||
...(displayNames[model] ? { displayName: displayNames[model] } : {}),
|
||||
key: `model:${index}:${model}`,
|
||||
model
|
||||
}));
|
||||
|
|
@ -451,7 +462,7 @@ export function modelCatalogItemMatchesQuery(row: ModelCatalogItem, query: strin
|
|||
return true;
|
||||
}
|
||||
|
||||
return row.model.toLowerCase().includes(query);
|
||||
return row.model.toLowerCase().includes(query) || (row.displayName ?? "").toLowerCase().includes(query);
|
||||
}
|
||||
|
||||
export function createRouteModelOptions(providers: GatewayProviderConfig[]): Array<{ label: string; value: string }> {
|
||||
|
|
@ -462,7 +473,7 @@ export function createRouteModelOptions(providers: GatewayProviderConfig[]): Arr
|
|||
return provider.models
|
||||
.filter(Boolean)
|
||||
.map((model) => ({
|
||||
label: `${provider.name}, ${model}`,
|
||||
label: `${provider.name}, ${providerModelDisplayName(provider, model)}`,
|
||||
value: `${provider.name},${model}`
|
||||
}));
|
||||
});
|
||||
|
|
@ -867,7 +878,10 @@ export function createProviderDraftFromDeepLinkPayload(
|
|||
baseUrl,
|
||||
credentials: [],
|
||||
icon: payload.icon?.trim() || "",
|
||||
modelDisplayNames: providerPresetModelDisplayNames(preset),
|
||||
modelDisplayNames: modelDisplayNamesForModels(
|
||||
mergeModelDisplayNames(providerPresetModelDisplayNames(preset), payload.modelDisplayNames),
|
||||
models
|
||||
),
|
||||
modelSearch: "",
|
||||
modelsText: models.join("\n"),
|
||||
name: uniqueProviderName(providers, baseName),
|
||||
|
|
@ -926,6 +940,7 @@ export function createProviderConfigFromDeepLink(
|
|||
api_key: apiKey,
|
||||
capabilities: capabilities.length > 0 ? capabilities : undefined,
|
||||
icon: payload.icon?.trim() || undefined,
|
||||
modelDisplayNames: modelDisplayNamesForModels(mergeModelDisplayNames(payload.modelDisplayNames, probe?.modelDisplayNames), models),
|
||||
models,
|
||||
name,
|
||||
type: protocol
|
||||
|
|
@ -973,7 +988,10 @@ export function createProviderDraftFromProvider(provider: GatewayProviderConfig)
|
|||
baseUrl,
|
||||
credentials: (provider.credentials ?? []).map(providerCredentialDraftFromConfig),
|
||||
icon: provider.icon ?? "",
|
||||
modelDisplayNames: providerPresetModelDisplayNames(preset),
|
||||
modelDisplayNames: modelDisplayNamesForModels(
|
||||
mergeModelDisplayNames(providerPresetModelDisplayNames(preset), provider.modelDisplayNames),
|
||||
provider.models
|
||||
),
|
||||
modelSearch: "",
|
||||
modelsText: provider.models.join("\n"),
|
||||
name: provider.name,
|
||||
|
|
@ -1519,10 +1537,12 @@ export function createProviderInstallLinkFromDraft(draft: AddProviderDraft, prob
|
|||
return accountKeySafetyIssue.message;
|
||||
}
|
||||
|
||||
const modelDisplayNames = modelDisplayNamesForModels(draft.modelDisplayNames, models);
|
||||
const payload: ProviderDeepLinkPayload = {
|
||||
...(account ? { account } : {}),
|
||||
baseUrl,
|
||||
...(draft.icon.trim() ? { icon: draft.icon.trim() } : {}),
|
||||
...(modelDisplayNames ? { modelDisplayNames } : {}),
|
||||
models,
|
||||
name: providerName,
|
||||
protocol
|
||||
|
|
@ -1870,6 +1890,25 @@ export function mergeModelDisplayNames(
|
|||
return Object.keys(merged).length > 0 ? merged : undefined;
|
||||
}
|
||||
|
||||
export function modelDisplayNamesForModels(
|
||||
value: Record<string, string> | undefined,
|
||||
models: string[]
|
||||
): Record<string, string> | undefined {
|
||||
const modelIds = new Set(models);
|
||||
const entries = Object.entries(value ?? {})
|
||||
.map(([rawModel, rawDisplayName]) => [rawModel.trim(), rawDisplayName.trim()] as const)
|
||||
.filter(([model, displayName]) => model && displayName && model !== displayName && modelIds.has(model));
|
||||
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
||||
}
|
||||
|
||||
export function providerModelDisplayName(provider: GatewayProviderConfig, model: string): string {
|
||||
return provider.modelDisplayNames?.[model]?.trim() || model;
|
||||
}
|
||||
|
||||
export function providerModelDisplayTitle(provider: GatewayProviderConfig, model: string): string {
|
||||
return providerModelDisplayName(provider, model);
|
||||
}
|
||||
|
||||
export function pickRecommendedProviderModels(models: string[], protocol?: GatewayProviderProtocol): string[] {
|
||||
const candidates = mergeProviderModelLists(models);
|
||||
if (candidates.length === 0) {
|
||||
|
|
@ -1965,7 +2004,8 @@ export function providerMatchesQuery(provider: GatewayProviderConfig, query: str
|
|||
providerBaseUrl(provider),
|
||||
providerCapabilitiesSummary(provider),
|
||||
...(provider.capabilities ?? []).map((capability) => capability.baseUrl),
|
||||
...provider.models
|
||||
...provider.models,
|
||||
...provider.models.map((model) => providerModelDisplayName(provider, model))
|
||||
]
|
||||
.filter(Boolean)
|
||||
.some((value) => value.toLowerCase().includes(query));
|
||||
|
|
|
|||
|
|
@ -694,6 +694,7 @@ export type ExtensionListItem = {
|
|||
};
|
||||
|
||||
export type ModelCatalogItem = {
|
||||
displayName?: string;
|
||||
key: string;
|
||||
model: string;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ export type GatewayProviderConfig = {
|
|||
extraHeaders?: unknown;
|
||||
icon?: string;
|
||||
id?: string;
|
||||
modelDisplayNames?: Record<string, string>;
|
||||
models: string[];
|
||||
name: string;
|
||||
provider?: string;
|
||||
|
|
@ -221,6 +222,7 @@ export type ProviderDeepLinkPayload = {
|
|||
apiKey?: string;
|
||||
baseUrl: string;
|
||||
icon?: string;
|
||||
modelDisplayNames?: Record<string, string>;
|
||||
models: string[];
|
||||
name?: string;
|
||||
protocol?: GatewayProviderProtocol;
|
||||
|
|
@ -250,6 +252,7 @@ export type LocalAgentProviderCandidate = {
|
|||
id: string;
|
||||
importable: boolean;
|
||||
kind: LocalAgentProviderKind;
|
||||
modelDisplayNames?: Record<string, string>;
|
||||
models: string[];
|
||||
name: string;
|
||||
protocol: GatewayProviderProtocol;
|
||||
|
|
|
|||
|
|
@ -147,6 +147,7 @@ export function parseProviderDeepLinkPayload(rawUrl: string): ProviderDeepLinkPa
|
|||
firstStringParam(params, ["protocol"]) ?? firstPayloadString(payload, ["protocol"])
|
||||
);
|
||||
const models = readDeepLinkModels(params, payload);
|
||||
const modelDisplayNames = readDeepLinkModelDisplayNames(params, payload, models);
|
||||
const account = readDeepLinkAccount(params, payload);
|
||||
const source = boundedString(
|
||||
firstStringParam(params, ["source"]) ??
|
||||
|
|
@ -159,6 +160,7 @@ export function parseProviderDeepLinkPayload(rawUrl: string): ProviderDeepLinkPa
|
|||
...(apiKey ? { apiKey } : {}),
|
||||
baseUrl,
|
||||
...(icon ? { icon } : {}),
|
||||
...(modelDisplayNames ? { modelDisplayNames } : {}),
|
||||
models,
|
||||
...(name ? { name } : {}),
|
||||
...(protocol ? { protocol } : {}),
|
||||
|
|
@ -216,6 +218,7 @@ function parseProviderPayloadFields(
|
|||
firstStringParam(params, ["protocol"]) ?? firstPayloadString(payload, ["protocol"])
|
||||
);
|
||||
const models = readDeepLinkModels(params, payload);
|
||||
const modelDisplayNames = readDeepLinkModelDisplayNames(params, payload, models);
|
||||
const account = readDeepLinkAccount(params, payload);
|
||||
const source = boundedString(
|
||||
firstStringParam(params, ["source"]) ??
|
||||
|
|
@ -230,6 +233,7 @@ function parseProviderPayloadFields(
|
|||
...(apiKey ? { apiKey } : {}),
|
||||
baseUrl,
|
||||
...(icon ? { icon } : {}),
|
||||
...(modelDisplayNames ? { modelDisplayNames } : {}),
|
||||
models,
|
||||
...(name ? { name } : {}),
|
||||
...(protocol ? { protocol } : {}),
|
||||
|
|
@ -515,11 +519,61 @@ function payloadModels(payload: Record<string, unknown> | undefined): string[] {
|
|||
}
|
||||
const value = payload.models;
|
||||
if (Array.isArray(value)) {
|
||||
return value.filter((item): item is string => typeof item === "string");
|
||||
return value
|
||||
.map((item) => typeof item === "string" ? item : readPayloadModelId(item))
|
||||
.filter((item): item is string => Boolean(item));
|
||||
}
|
||||
return typeof value === "string" ? [value] : [];
|
||||
}
|
||||
|
||||
function readDeepLinkModelDisplayNames(
|
||||
params: URLSearchParams,
|
||||
payload: Record<string, unknown> | undefined,
|
||||
models: string[]
|
||||
): Record<string, string> | undefined {
|
||||
const modelIds = new Set(models);
|
||||
const displayNames: Record<string, string> = {};
|
||||
const addDisplayName = (rawModel: unknown, rawDisplayName: unknown) => {
|
||||
const model = typeof rawModel === "string" ? rawModel.trim() : "";
|
||||
const displayName = typeof rawDisplayName === "string" ? rawDisplayName.trim() : "";
|
||||
if (!model || !displayName || model === displayName || !modelIds.has(model)) {
|
||||
return;
|
||||
}
|
||||
if (displayName.length > maxModelLength) {
|
||||
throw new Error("Model display name is too long.");
|
||||
}
|
||||
displayNames[model] = displayName;
|
||||
};
|
||||
|
||||
const explicit = parseJsonValueParam(params, payload, ["modelDisplayNames", "model_display_names"]);
|
||||
if (isRecord(explicit)) {
|
||||
for (const [model, displayName] of Object.entries(explicit)) {
|
||||
addDisplayName(model, displayName);
|
||||
}
|
||||
}
|
||||
|
||||
const payloadModelList = Array.isArray(payload?.models) ? payload.models : [];
|
||||
for (const item of payloadModelList) {
|
||||
if (!isRecord(item)) {
|
||||
continue;
|
||||
}
|
||||
addDisplayName(readPayloadModelId(item), readPayloadModelDisplayName(item));
|
||||
}
|
||||
|
||||
return Object.keys(displayNames).length > 0 ? displayNames : undefined;
|
||||
}
|
||||
|
||||
function readPayloadModelId(value: unknown): string | undefined {
|
||||
if (!isRecord(value)) {
|
||||
return undefined;
|
||||
}
|
||||
return firstPayloadString(value, ["id", "slug", "model", "name"]);
|
||||
}
|
||||
|
||||
function readPayloadModelDisplayName(value: Record<string, unknown>): string | undefined {
|
||||
return firstPayloadString(value, ["display_name", "displayName", "label", "name"]);
|
||||
}
|
||||
|
||||
function splitModelValue(value: string): string[] {
|
||||
return value
|
||||
.split(/[\n,]+/)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue