fix: improve fallback model names
Shows readable model names when provider data is still loading Centralizes model display fallback logic Covers fallback formatting with tests
This commit is contained in:
@@ -25,6 +25,7 @@ import { filterVisibleParts, normalizeParts } from './message/partUtils';
|
|||||||
import { normalizeUserDisplayParts } from './message/normalizeUserDisplayParts';
|
import { normalizeUserDisplayParts } from './message/normalizeUserDisplayParts';
|
||||||
import { flattenAssistantTextParts } from '@/lib/messages/messageText';
|
import { flattenAssistantTextParts } from '@/lib/messages/messageText';
|
||||||
import { isLikelyProviderAuthFailure, PROVIDER_AUTH_FAILURE_MESSAGE } from '@/lib/messages/providerAuthError';
|
import { isLikelyProviderAuthFailure, PROVIDER_AUTH_FAILURE_MESSAGE } from '@/lib/messages/providerAuthError';
|
||||||
|
import { getProviderModelDisplayName } from '@/lib/modelDisplay';
|
||||||
import { lazyWithChunkRecovery } from '@/lib/chunkLoadRecovery';
|
import { lazyWithChunkRecovery } from '@/lib/chunkLoadRecovery';
|
||||||
import type { TurnGroupingContext } from './lib/turns/types';
|
import type { TurnGroupingContext } from './lib/turns/types';
|
||||||
import { copyTextToClipboard } from '@/lib/clipboard';
|
import { copyTextToClipboard } from '@/lib/clipboard';
|
||||||
@@ -168,7 +169,7 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
|
|||||||
streamPerfCount('ui.chat_message.render.streaming');
|
streamPerfCount('ui.chat_message.render.streaming');
|
||||||
}
|
}
|
||||||
|
|
||||||
const providers = useConfigStore.getState().providers;
|
const providers = useConfigStore((state) => state.providers);
|
||||||
const { showReasoningTraces, stickyUserHeader, chatRenderMode, showExpandedBashTools, showExpandedEditTools } = useUIStore(
|
const { showReasoningTraces, stickyUserHeader, chatRenderMode, showExpandedBashTools, showExpandedEditTools } = useUIStore(
|
||||||
useShallow((state) => ({
|
useShallow((state) => ({
|
||||||
showReasoningTraces: state.showReasoningTraces,
|
showReasoningTraces: state.showReasoningTraces,
|
||||||
@@ -363,17 +364,10 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
|
|||||||
const modelName = React.useMemo(() => {
|
const modelName = React.useMemo(() => {
|
||||||
if (isUser) return undefined;
|
if (isUser) return undefined;
|
||||||
|
|
||||||
if (providerID && modelID && providers.length > 0) {
|
const provider = providerID && providers.length > 0
|
||||||
const provider = providers.find((p) => p.id === providerID);
|
? providers.find((p) => p.id === providerID)
|
||||||
if (provider?.models && Array.isArray(provider.models)) {
|
: undefined;
|
||||||
const model = provider.models.find((m: Record<string, unknown>) => (m as Record<string, unknown>).id === modelID);
|
return getProviderModelDisplayName(provider, modelID) || undefined;
|
||||||
const modelObj = model as Record<string, unknown> | undefined;
|
|
||||||
const name = modelObj?.name;
|
|
||||||
return typeof name === 'string' ? name : undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}, [isUser, providerID, modelID, providers]);
|
}, [isUser, providerID, modelID, providers]);
|
||||||
|
|
||||||
const modelHasVariants = React.useMemo(() => {
|
const modelHasVariants = React.useMemo(() => {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React from 'react';
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useConfigStore } from '@/stores/useConfigStore';
|
import { useConfigStore } from '@/stores/useConfigStore';
|
||||||
import { getModelDisplayName } from './mobileControlsUtils';
|
import { getModelDisplayName } from './mobileControlsUtils';
|
||||||
|
import { useI18n } from '@/lib/i18n';
|
||||||
|
|
||||||
interface MobileModelButtonProps {
|
interface MobileModelButtonProps {
|
||||||
onOpenModel: () => void;
|
onOpenModel: () => void;
|
||||||
@@ -9,10 +10,11 @@ interface MobileModelButtonProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const MobileModelButton: React.FC<MobileModelButtonProps> = ({ onOpenModel, className }) => {
|
export const MobileModelButton: React.FC<MobileModelButtonProps> = ({ onOpenModel, className }) => {
|
||||||
|
const { t } = useI18n();
|
||||||
const currentModelId = useConfigStore((state) => state.currentModelId);
|
const currentModelId = useConfigStore((state) => state.currentModelId);
|
||||||
const getCurrentProvider = useConfigStore((state) => state.getCurrentProvider);
|
const getCurrentProvider = useConfigStore((state) => state.getCurrentProvider);
|
||||||
const currentProvider = getCurrentProvider();
|
const currentProvider = getCurrentProvider();
|
||||||
const modelLabel = getModelDisplayName(currentProvider, currentModelId);
|
const modelLabel = getModelDisplayName(currentProvider, currentModelId, t('chat.modelControls.selectModel'));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { isDesktopShell } from '@/lib/desktop';
|
|||||||
import { getAgentColor } from '@/lib/agentColors';
|
import { getAgentColor } from '@/lib/agentColors';
|
||||||
import { useDeviceInfo } from '@/lib/device';
|
import { useDeviceInfo } from '@/lib/device';
|
||||||
import { mergeModelMetadataWithLiveModel } from '@/lib/modelMetadata';
|
import { mergeModelMetadataWithLiveModel } from '@/lib/modelMetadata';
|
||||||
|
import { getModelDisplayName as getSharedModelDisplayName } from '@/lib/modelDisplay';
|
||||||
import { getEditModeColors } from '@/lib/permissions/editModeColors';
|
import { getEditModeColors } from '@/lib/permissions/editModeColors';
|
||||||
import { cn, fuzzyMatch } from '@/lib/utils';
|
import { cn, fuzzyMatch } from '@/lib/utils';
|
||||||
import { useContextStore } from '@/stores/contextStore';
|
import { useContextStore } from '@/stores/contextStore';
|
||||||
@@ -1258,12 +1259,8 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getModelDisplayName = (model: ProviderModel | undefined) => {
|
const getModelDisplayName = (model: ProviderModel | undefined, fallbackModelId?: string) => {
|
||||||
const name = (typeof model?.name === 'string' ? model.name : (typeof model?.id === 'string' ? model.id : ''));
|
return getSharedModelDisplayName(model, fallbackModelId, { maxLength: 40 });
|
||||||
if (name.length > 40) {
|
|
||||||
return name.substring(0, 37) + '...';
|
|
||||||
}
|
|
||||||
return name;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getProviderDisplayName = () => {
|
const getProviderDisplayName = () => {
|
||||||
@@ -1272,10 +1269,9 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getCurrentModelDisplayName = () => {
|
const getCurrentModelDisplayName = () => {
|
||||||
if (!currentProviderId || !currentModelId) return 'Not selected';
|
if (!currentModelId) return t('chat.modelControls.selectModel');
|
||||||
if (models.length === 0) return 'Not selected';
|
|
||||||
const currentModel = models.find((m: ProviderModel) => m.id === currentModelId);
|
const currentModel = models.find((m: ProviderModel) => m.id === currentModelId);
|
||||||
return getModelDisplayName(currentModel);
|
return getModelDisplayName(currentModel, currentModelId) || t('chat.modelControls.selectModel');
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentModelDisplayName = getCurrentModelDisplayName();
|
const currentModelDisplayName = getCurrentModelDisplayName();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useConfigStore } from '@/stores/useConfigStore';
|
|||||||
import { useSessionUIStore } from '@/sync/session-ui-store';
|
import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||||
import { useContextStore } from '@/stores/contextStore';
|
import { useContextStore } from '@/stores/contextStore';
|
||||||
import { formatEffortLabel, getAgentDisplayName, getModelDisplayName } from './mobileControlsUtils';
|
import { formatEffortLabel, getAgentDisplayName, getModelDisplayName } from './mobileControlsUtils';
|
||||||
|
import { useI18n } from '@/lib/i18n';
|
||||||
|
|
||||||
const STATUS_CHIP_STYLE = {
|
const STATUS_CHIP_STYLE = {
|
||||||
height: '28px',
|
height: '28px',
|
||||||
@@ -17,6 +18,7 @@ interface StatusChipProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const StatusChip: React.FC<StatusChipProps> = ({ onClick, className }) => {
|
export const StatusChip: React.FC<StatusChipProps> = ({ onClick, className }) => {
|
||||||
|
const { t } = useI18n();
|
||||||
const currentModelId = useConfigStore((state) => state.currentModelId);
|
const currentModelId = useConfigStore((state) => state.currentModelId);
|
||||||
const currentVariant = useConfigStore((state) => state.currentVariant);
|
const currentVariant = useConfigStore((state) => state.currentVariant);
|
||||||
const currentAgentName = useConfigStore((state) => state.currentAgentName);
|
const currentAgentName = useConfigStore((state) => state.currentAgentName);
|
||||||
@@ -32,7 +34,7 @@ export const StatusChip: React.FC<StatusChipProps> = ({ onClick, className }) =>
|
|||||||
const uiAgentName = currentSessionId ? (sessionAgentName || currentAgentName) : currentAgentName;
|
const uiAgentName = currentSessionId ? (sessionAgentName || currentAgentName) : currentAgentName;
|
||||||
const agentLabel = getAgentDisplayName(agents, uiAgentName);
|
const agentLabel = getAgentDisplayName(agents, uiAgentName);
|
||||||
const currentProvider = getCurrentProvider();
|
const currentProvider = getCurrentProvider();
|
||||||
const modelLabel = getModelDisplayName(currentProvider, currentModelId);
|
const modelLabel = getModelDisplayName(currentProvider, currentModelId, t('chat.modelControls.selectModel'));
|
||||||
const hasEffort = getCurrentModelVariants().length > 0;
|
const hasEffort = getCurrentModelVariants().length > 0;
|
||||||
const effortLabel = hasEffort ? formatEffortLabel(currentVariant) : null;
|
const effortLabel = hasEffort ? formatEffortLabel(currentVariant) : null;
|
||||||
const fullLabel = [agentLabel, modelLabel, effortLabel].filter(Boolean).join(' · ');
|
const fullLabel = [agentLabel, modelLabel, effortLabel].filter(Boolean).join(' · ');
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { Agent } from '@opencode-ai/sdk/v2';
|
import type { Agent } from '@opencode-ai/sdk/v2';
|
||||||
|
import { getProviderModelDisplayName, type DisplayProvider } from '@/lib/modelDisplay';
|
||||||
|
|
||||||
export type MobileControlsPanel = 'model' | 'agent' | 'variant' | null;
|
export type MobileControlsPanel = 'model' | 'agent' | 'variant' | null;
|
||||||
|
|
||||||
@@ -36,24 +37,12 @@ export const getAgentDisplayName = (agents: Agent[], agentName?: string) => {
|
|||||||
return fallbackAgent ? capitalizeLabel(fallbackAgent.name) : 'Select agent';
|
return fallbackAgent ? capitalizeLabel(fallbackAgent.name) : 'Select agent';
|
||||||
};
|
};
|
||||||
|
|
||||||
type ProviderModel = { id?: string; name?: string };
|
|
||||||
|
|
||||||
export const getModelDisplayName = (
|
export const getModelDisplayName = (
|
||||||
provider: { models?: ProviderModel[] } | undefined,
|
provider: DisplayProvider,
|
||||||
modelId: string | undefined,
|
modelId: string | undefined,
|
||||||
|
fallbackLabel = '',
|
||||||
) => {
|
) => {
|
||||||
if (!provider || !modelId) {
|
return getProviderModelDisplayName(provider, modelId, { fallbackLabel });
|
||||||
return 'Not selected';
|
|
||||||
}
|
|
||||||
const models = Array.isArray(provider.models) ? provider.models : [];
|
|
||||||
const model = models.find((entry) => entry.id === modelId);
|
|
||||||
if (typeof model?.name === 'string' && model.name.trim().length > 0) {
|
|
||||||
return model.name;
|
|
||||||
}
|
|
||||||
if (typeof model?.id === 'string' && model.id.trim().length > 0) {
|
|
||||||
return model.id;
|
|
||||||
}
|
|
||||||
return modelId;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const formatEffortLabel = (variant?: string) => {
|
export const formatEffortLabel = (variant?: string) => {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { ProviderLogo } from '@/components/ui/ProviderLogo';
|
|||||||
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
import { mergeModelMetadataWithLiveModel } from '@/lib/modelMetadata';
|
import { mergeModelMetadataWithLiveModel } from '@/lib/modelMetadata';
|
||||||
|
import { getModelDisplayName as getSharedModelDisplayName } from '@/lib/modelDisplay';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import type { ModelMetadata } from '@/types';
|
import type { ModelMetadata } from '@/types';
|
||||||
|
|
||||||
@@ -58,10 +59,7 @@ const CURRENCY_FORMATTER = new Intl.NumberFormat('en-US', {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const getModelDisplayName = (model: Record<string, unknown>) => {
|
const getModelDisplayName = (model: Record<string, unknown>) => {
|
||||||
const name = model?.name || model?.id || '';
|
return getSharedModelDisplayName(model, undefined, { maxLength: 40 });
|
||||||
const nameStr = String(name);
|
|
||||||
if (nameStr.length > 40) return `${nameStr.substring(0, 37)}...`;
|
|
||||||
return nameStr;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatModelContextTokens = (value?: number | null) => {
|
const formatModelContextTokens = (value?: number | null) => {
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { describe, expect, test } from 'bun:test';
|
||||||
|
|
||||||
|
import { getModelDisplayName, getProviderModelDisplayName, humanizeModelId } from './modelDisplay';
|
||||||
|
|
||||||
|
describe('modelDisplay', () => {
|
||||||
|
test('prefers model name over ids', () => {
|
||||||
|
expect(getModelDisplayName({ id: 'claude-sonnet-4-5', name: 'Claude Sonnet 4.5' })).toBe('Claude Sonnet 4.5');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('falls back to a human-readable model id when name is missing', () => {
|
||||||
|
expect(getModelDisplayName({ id: 'claude-sonnet-4-5' })).toBe('Claude Sonnet 4.5');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('falls back to a human-readable explicit model id when provider data is unavailable', () => {
|
||||||
|
expect(getProviderModelDisplayName(undefined, 'claude-sonnet-4-5')).toBe('Claude Sonnet 4.5');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('uses fallback label only when no model id is available', () => {
|
||||||
|
expect(getProviderModelDisplayName(undefined, undefined, { fallbackLabel: 'Select model' })).toBe('Select model');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('supports provider model records and truncation', () => {
|
||||||
|
const provider = {
|
||||||
|
models: {
|
||||||
|
'very-long-model-id': { id: 'very-long-model-id', name: 'Very Long Model Name' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(getProviderModelDisplayName(provider, 'very-long-model-id', { maxLength: 9 })).toBe('Very L...');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('humanizes provider-prefixed model ids using common model catalog patterns', () => {
|
||||||
|
expect(humanizeModelId('anthropic/claude-opus-4-7-fast')).toBe('Claude Opus 4.7 Fast');
|
||||||
|
expect(humanizeModelId('google/gemini-3.1-flash-lite-preview')).toBe('Gemini 3.1 Flash Lite Preview');
|
||||||
|
expect(humanizeModelId('meta-llama/llama-3.2-3b-instruct:free')).toBe('Llama 3.2 3B Instruct (free)');
|
||||||
|
expect(humanizeModelId('openai/gpt-4o-mini-2024-07-18')).toBe('GPT-4o Mini (2024-07-18)');
|
||||||
|
expect(humanizeModelId('openai/gpt-5.4-mini-fast')).toBe('GPT-5.4 Mini Fast');
|
||||||
|
expect(humanizeModelId('qwen/qwen3-coder:free')).toBe('Qwen3 Coder (free)');
|
||||||
|
expect(humanizeModelId('xai/grok-4-1-fast-non-reasoning')).toBe('Grok 4.1 Fast (Non-Reasoning)');
|
||||||
|
expect(humanizeModelId('z-ai/glm-4.5-air')).toBe('GLM-4.5 Air');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('humanizes alias and custom model ids without provider data', () => {
|
||||||
|
expect(humanizeModelId('~openai/gpt-mini-latest')).toBe('GPT Mini Latest');
|
||||||
|
expect(humanizeModelId('my-custom_provider/myAwesomeModel-v2-fast')).toBe('My Awesome Model V2 Fast');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
export type DisplayModel = Record<string, unknown> & {
|
||||||
|
id?: unknown;
|
||||||
|
name?: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DisplayProvider = {
|
||||||
|
models?: DisplayModel[] | Record<string, DisplayModel | undefined>;
|
||||||
|
} | null | undefined;
|
||||||
|
|
||||||
|
type ModelDisplayOptions = {
|
||||||
|
fallbackLabel?: string;
|
||||||
|
maxLength?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeString = (value: unknown): string => {
|
||||||
|
return typeof value === 'string' ? value.trim() : '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const truncate = (value: string, maxLength?: number): string => {
|
||||||
|
if (!maxLength || maxLength <= 0 || value.length <= maxLength) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxLength <= 3) {
|
||||||
|
return value.slice(0, maxLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${value.slice(0, maxLength - 3)}...`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TOKEN_LABELS: Record<string, string> = {
|
||||||
|
ai: 'AI',
|
||||||
|
api: 'API',
|
||||||
|
ernie: 'ERNIE',
|
||||||
|
glm: 'GLM',
|
||||||
|
gpt: 'GPT',
|
||||||
|
it: 'IT',
|
||||||
|
lfm: 'LFM',
|
||||||
|
lm: 'LM',
|
||||||
|
oss: 'OSS',
|
||||||
|
pdf: 'PDF',
|
||||||
|
rp: 'RP',
|
||||||
|
slerp: 'SLERP',
|
||||||
|
tars: 'TARS',
|
||||||
|
ui: 'UI',
|
||||||
|
vl: 'VL',
|
||||||
|
vlm: 'VLM',
|
||||||
|
aion: 'Aion',
|
||||||
|
anthropic: 'Anthropic',
|
||||||
|
chatgpt: 'ChatGPT',
|
||||||
|
claude: 'Claude',
|
||||||
|
codex: 'Codex',
|
||||||
|
codestral: 'Codestral',
|
||||||
|
command: 'Command',
|
||||||
|
deepseek: 'DeepSeek',
|
||||||
|
gemini: 'Gemini',
|
||||||
|
gemma: 'Gemma',
|
||||||
|
grok: 'Grok',
|
||||||
|
hunyuan: 'Hunyuan',
|
||||||
|
kimi: 'Kimi',
|
||||||
|
llama: 'Llama',
|
||||||
|
minimax: 'MiniMax',
|
||||||
|
mistral: 'Mistral',
|
||||||
|
mixtral: 'Mixtral',
|
||||||
|
nemotron: 'Nemotron',
|
||||||
|
openai: 'OpenAI',
|
||||||
|
qwen: 'Qwen',
|
||||||
|
rekaai: 'RekaAI',
|
||||||
|
};
|
||||||
|
|
||||||
|
const titleCaseToken = (token: string): string => {
|
||||||
|
if (!token) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return token[0].toUpperCase() + token.slice(1).toLowerCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
const stripProviderPrefix = (value: string): string => {
|
||||||
|
const withoutAliasMarker = value.startsWith('~') ? value.slice(1) : value;
|
||||||
|
const slashIndex = withoutAliasMarker.lastIndexOf('/');
|
||||||
|
return slashIndex >= 0 ? withoutAliasMarker.slice(slashIndex + 1) : withoutAliasMarker;
|
||||||
|
};
|
||||||
|
|
||||||
|
const splitColonSuffix = (value: string): { base: string; suffix: string } => {
|
||||||
|
const colonIndex = value.lastIndexOf(':');
|
||||||
|
if (colonIndex <= 0 || colonIndex >= value.length - 1) {
|
||||||
|
return { base: value, suffix: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
base: value.slice(0, colonIndex),
|
||||||
|
suffix: value.slice(colonIndex + 1),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const tokenizeModelId = (value: string): string[] => {
|
||||||
|
const rawTokens = value
|
||||||
|
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
||||||
|
.split(/[-_\s]+/)
|
||||||
|
.map((token) => token.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const tokens: string[] = [];
|
||||||
|
for (let index = 0; index < rawTokens.length; index += 1) {
|
||||||
|
const current = rawTokens[index];
|
||||||
|
const next = rawTokens[index + 1];
|
||||||
|
const nextNext = rawTokens[index + 2];
|
||||||
|
|
||||||
|
if (/^(19|20)\d{2}$/.test(current) && /^\d{2}$/.test(next ?? '') && /^\d{2}$/.test(nextNext ?? '')) {
|
||||||
|
tokens.push(`${current}-${next}-${nextNext}`);
|
||||||
|
index += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^\d$/.test(current) && /^\d$/.test(next ?? '')) {
|
||||||
|
tokens.push(`${current}.${next}`);
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens.push(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokens;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatModelToken = (token: string): string => {
|
||||||
|
const lower = token.toLowerCase();
|
||||||
|
const mapped = TOKEN_LABELS[lower];
|
||||||
|
if (mapped) {
|
||||||
|
return mapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
const qwenMatch = lower.match(/^qwen(\d(?:\.\d+)?)$/);
|
||||||
|
if (qwenMatch) {
|
||||||
|
return `Qwen${qwenMatch[1]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^o\d/.test(lower)) {
|
||||||
|
return lower;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^v\d/.test(lower)) {
|
||||||
|
return `V${token.slice(1).toUpperCase()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^\d+[on]$/.test(lower)) {
|
||||||
|
return lower;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^[a-z]\d+[a-z]?$/.test(lower)) {
|
||||||
|
return lower.toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^\d+(?:x\d+)?[a-z]+$/.test(lower)) {
|
||||||
|
return lower.replace(/[a-z]+$/i, (unit) => unit.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^\d+(?:\.\d+)?[a-z]$/.test(lower)) {
|
||||||
|
return lower.replace(/[a-z]$/i, (unit) => unit.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^[a-z]+\d+(?:\.\d+)?[a-z]*$/.test(lower)) {
|
||||||
|
return titleCaseToken(lower).replace(/([a-z])(\d)/i, '$1$2');
|
||||||
|
}
|
||||||
|
|
||||||
|
return titleCaseToken(token);
|
||||||
|
};
|
||||||
|
|
||||||
|
const combineModelTokens = (tokens: string[]): string[] => {
|
||||||
|
const result = [...tokens];
|
||||||
|
|
||||||
|
if (result[0] === 'GPT' && result[1] && /^(?:\d|\d+[a-z]|OSS$)/i.test(result[1])) {
|
||||||
|
result.splice(0, 2, `GPT-${result[1]}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result[0] === 'GLM' && result[1] && /^\d/.test(result[1])) {
|
||||||
|
result.splice(0, 2, `GLM-${result[1]}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result[0] === 'LFM' && result[1] && /^\d/.test(result[1])) {
|
||||||
|
result.splice(0, 2, `LFM${result[1]}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result[0] === 'Qwen' && result[1] && /^\d/.test(result[1])) {
|
||||||
|
result.splice(0, 2, `Qwen${result[1]}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatSuffix = (suffix: string): string => {
|
||||||
|
const normalized = normalizeString(suffix).toLowerCase();
|
||||||
|
if (!normalized) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized === 'free' || normalized === 'thinking') {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
return humanizeModelId(normalized);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const humanizeModelId = (modelId: string | null | undefined): string => {
|
||||||
|
const normalized = normalizeString(modelId);
|
||||||
|
if (!normalized) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const { base, suffix } = splitColonSuffix(stripProviderPrefix(normalized));
|
||||||
|
const formattedTokens = combineModelTokens(tokenizeModelId(base).map(formatModelToken));
|
||||||
|
|
||||||
|
if (formattedTokens.length >= 2) {
|
||||||
|
const last = formattedTokens[formattedTokens.length - 1];
|
||||||
|
const previous = formattedTokens[formattedTokens.length - 2];
|
||||||
|
if (last === 'Reasoning' && previous === 'Non') {
|
||||||
|
formattedTokens.splice(formattedTokens.length - 2, 2, '(Non-Reasoning)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastToken = formattedTokens[formattedTokens.length - 1];
|
||||||
|
if (/^(19|20)\d{2}-\d{2}-\d{2}$/.test(lastToken ?? '')) {
|
||||||
|
formattedTokens[formattedTokens.length - 1] = `(${lastToken})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayName = formattedTokens.join(' ').replace(/\s+\(/g, ' (').trim();
|
||||||
|
const displaySuffix = formatSuffix(suffix);
|
||||||
|
return displaySuffix ? `${displayName} (${displaySuffix})` : displayName;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getModelDisplayName = (
|
||||||
|
model: DisplayModel | null | undefined,
|
||||||
|
fallbackModelId?: string | null,
|
||||||
|
options: ModelDisplayOptions = {},
|
||||||
|
): string => {
|
||||||
|
const name = normalizeString(model?.name);
|
||||||
|
if (name) {
|
||||||
|
return truncate(name, options.maxLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
const modelId = normalizeString(model?.id) || normalizeString(fallbackModelId);
|
||||||
|
if (modelId) {
|
||||||
|
return truncate(humanizeModelId(modelId), options.maxLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
return options.fallbackLabel ?? '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProviderModel = (provider: DisplayProvider, modelId: string): DisplayModel | undefined => {
|
||||||
|
const models = provider?.models;
|
||||||
|
if (!models || !modelId) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(models)) {
|
||||||
|
return models.find((model) => normalizeString(model.id) === modelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return models[modelId];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getProviderModelDisplayName = (
|
||||||
|
provider: DisplayProvider,
|
||||||
|
modelId: string | null | undefined,
|
||||||
|
options: ModelDisplayOptions = {},
|
||||||
|
): string => {
|
||||||
|
const normalizedModelId = normalizeString(modelId);
|
||||||
|
const model = getProviderModel(provider, normalizedModelId);
|
||||||
|
return getModelDisplayName(model, normalizedModelId, options);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user