mirror of
https://github.com/ValueCell-ai/ClawX.git
synced 2026-08-14 00:48:10 +00:00
chore:show provider or custom name in chat model picker labels (#1115)
This commit is contained in:
@@ -48,6 +48,27 @@ export function formatModelRefLabel(modelRef: string | null | undefined): string
|
||||
return parsed?.modelId || (modelRef || '').trim() || 'Model';
|
||||
}
|
||||
|
||||
export function formatProviderDisplayName(
|
||||
account: ProviderAccount,
|
||||
vendorMap: Map<string, ProviderVendorInfo>,
|
||||
): string {
|
||||
if (account.vendorId === 'custom' || account.vendorId === 'ollama') {
|
||||
return account.label.trim() || account.vendorId;
|
||||
}
|
||||
|
||||
const vendor = vendorMap.get(account.vendorId);
|
||||
return vendor?.name || account.label.trim() || account.vendorId;
|
||||
}
|
||||
|
||||
export function formatConfiguredModelLabel(
|
||||
modelId: string,
|
||||
account: ProviderAccount,
|
||||
vendorMap: Map<string, ProviderVendorInfo>,
|
||||
): string {
|
||||
const providerName = formatProviderDisplayName(account, vendorMap);
|
||||
return `${modelId} (${providerName})`;
|
||||
}
|
||||
|
||||
export function toModelOptionTestId(label: string): string {
|
||||
return label.replace(/[^a-zA-Z0-9_-]+/g, '-');
|
||||
}
|
||||
@@ -108,10 +129,13 @@ export function buildRuntimeProviderOptions(
|
||||
export function buildConfiguredModelOptions(
|
||||
providerAccounts: ProviderAccount[],
|
||||
providerStatuses: ProviderWithKeyInfo[],
|
||||
providerVendors: ProviderVendorInfo[],
|
||||
providerDefaultAccountId: string | null,
|
||||
): ConfiguredModelOption[] {
|
||||
const safeAccounts = Array.isArray(providerAccounts) ? providerAccounts : [];
|
||||
const safeStatuses = Array.isArray(providerStatuses) ? providerStatuses : [];
|
||||
const safeVendors = Array.isArray(providerVendors) ? providerVendors : [];
|
||||
const vendorMap = new Map<string, ProviderVendorInfo>(safeVendors.map((vendor) => [vendor.id, vendor]));
|
||||
const statusById = new Map<string, ProviderWithKeyInfo>(safeStatuses.map((status) => [status.id, status]));
|
||||
const entries = safeAccounts
|
||||
.filter((account) => account.enabled && account.model?.trim() && hasConfiguredProviderCredentials(account, statusById))
|
||||
@@ -132,7 +156,7 @@ export function buildConfiguredModelOptions(
|
||||
if (deduped.has(modelRef)) continue;
|
||||
deduped.set(modelRef, {
|
||||
modelRef,
|
||||
label: modelId,
|
||||
label: formatConfiguredModelLabel(modelId, account, vendorMap),
|
||||
runtimeProviderKey,
|
||||
accountId: account.id,
|
||||
});
|
||||
|
||||
@@ -218,6 +218,7 @@ export function ChatInput({ onSend, onStop, disabled = false, sending = false }:
|
||||
const providerAccounts = useProviderStore((s) => s.accounts);
|
||||
const providerStatuses = useProviderStore((s) => s.statuses);
|
||||
const providerDefaultAccountId = useProviderStore((s) => s.defaultAccountId);
|
||||
const providerVendors = useProviderStore((s) => s.vendors);
|
||||
const refreshProviderSnapshot = useProviderStore((s) => s.refreshProviderSnapshot);
|
||||
const currentAgentId = useChatStore((s) => s.currentAgentId);
|
||||
const currentAgent = useMemo(
|
||||
@@ -229,11 +230,19 @@ export function ChatInput({ onSend, onStop, disabled = false, sending = false }:
|
||||
[currentAgent, currentAgentId],
|
||||
);
|
||||
const modelOptions = useMemo(
|
||||
() => buildConfiguredModelOptions(providerAccounts, providerStatuses, providerDefaultAccountId),
|
||||
[providerAccounts, providerDefaultAccountId, providerStatuses],
|
||||
() => buildConfiguredModelOptions(
|
||||
providerAccounts,
|
||||
providerStatuses,
|
||||
providerVendors,
|
||||
providerDefaultAccountId,
|
||||
),
|
||||
[providerAccounts, providerDefaultAccountId, providerStatuses, providerVendors],
|
||||
);
|
||||
const effectiveModelRef = optimisticModelRef || currentAgent?.modelRef || defaultModelRef || modelOptions[0]?.modelRef || null;
|
||||
const currentModelLabel = formatModelRefLabel(effectiveModelRef);
|
||||
const currentModelLabel = useMemo(() => {
|
||||
const matchedOption = modelOptions.find((option) => option.modelRef === effectiveModelRef);
|
||||
return matchedOption?.label || formatModelRefLabel(effectiveModelRef);
|
||||
}, [effectiveModelRef, modelOptions]);
|
||||
const mentionableAgents = useMemo(
|
||||
() => (agents ?? []).filter((agent) => agent.id !== currentAgentId),
|
||||
[agents, currentAgentId],
|
||||
|
||||
@@ -162,12 +162,12 @@ test.describe('ClawX chat model picker', () => {
|
||||
win?.webContents.send('gateway:status-changed', { state: 'running', port: 18789, pid: 12345, gatewayReady: true });
|
||||
});
|
||||
|
||||
await expect(page.getByTestId('chat-model-picker-button')).toContainText('model-alpha');
|
||||
await expect(page.getByTestId('chat-model-picker-button')).toContainText('model-alpha (Alpha)');
|
||||
await page.getByTestId('chat-model-picker-button').click();
|
||||
await expect(page.getByTestId('chat-model-picker-menu')).toBeVisible();
|
||||
await expect(page.getByTestId('chat-model-picker-menu')).toContainText('provider/model-beta');
|
||||
await page.getByTestId('chat-model-picker-menu').getByRole('button', { name: 'provider/model-beta' }).click();
|
||||
await expect(page.getByTestId('chat-model-picker-button')).toContainText('provider/model-beta');
|
||||
await expect(page.getByTestId('chat-model-picker-menu')).toContainText('provider/model-beta (Beta)');
|
||||
await page.getByTestId('chat-model-picker-menu').getByRole('button', { name: 'provider/model-beta (Beta)' }).click();
|
||||
await expect(page.getByTestId('chat-model-picker-button')).toContainText('provider/model-beta (Beta)');
|
||||
|
||||
const requests = await app.evaluate(() => (
|
||||
(globalThis as typeof globalThis & { __chatModelPickerRequests?: Array<{ path: string; method: string; body: unknown }> }).__chatModelPickerRequests ?? []
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
buildConfiguredModelOptions,
|
||||
formatConfiguredModelLabel,
|
||||
formatModelRefLabel,
|
||||
formatProviderDisplayName,
|
||||
resolveRuntimeProviderKey,
|
||||
} from '../../src/lib/model-options';
|
||||
import type { ProviderAccount, ProviderWithKeyInfo } from '../../src/lib/providers';
|
||||
import type { ProviderAccount, ProviderVendorInfo, ProviderWithKeyInfo } from '../../src/lib/providers';
|
||||
|
||||
const now = '2026-04-28T00:00:00.000Z';
|
||||
|
||||
@@ -36,12 +38,34 @@ function status(id: string, hasKey = true): ProviderWithKeyInfo {
|
||||
} as ProviderWithKeyInfo;
|
||||
}
|
||||
|
||||
const vendors: ProviderVendorInfo[] = [
|
||||
{
|
||||
id: 'openai',
|
||||
name: 'OpenAI',
|
||||
icon: '💚',
|
||||
placeholder: 'sk-proj-...',
|
||||
model: 'GPT',
|
||||
requiresApiKey: true,
|
||||
category: 'official',
|
||||
supportedAuthModes: ['api_key', 'oauth_browser'],
|
||||
defaultAuthMode: 'api_key',
|
||||
supportsMultipleAccounts: true,
|
||||
},
|
||||
];
|
||||
|
||||
describe('model option helpers', () => {
|
||||
it('formats model refs using only the text after the provider prefix', () => {
|
||||
expect(formatModelRefLabel('openrouter/openai/gpt-5.5')).toBe('openai/gpt-5.5');
|
||||
expect(formatModelRefLabel('custom-alpha1234/model-alpha')).toBe('model-alpha');
|
||||
});
|
||||
|
||||
it('formats provider display names using custom labels or vendor names', () => {
|
||||
const vendorMap = new Map(vendors.map((vendor) => [vendor.id, vendor]));
|
||||
expect(formatProviderDisplayName(account({ vendorId: 'custom', label: 'Alpha' }), vendorMap)).toBe('Alpha');
|
||||
expect(formatProviderDisplayName(account({ vendorId: 'openai', label: 'OpenAI' }), vendorMap)).toBe('OpenAI');
|
||||
expect(formatConfiguredModelLabel('gpt-5.5', account({ vendorId: 'openai', label: 'OpenAI' }), vendorMap)).toBe('gpt-5.5 (OpenAI)');
|
||||
});
|
||||
|
||||
it('builds one configured custom model option per account', () => {
|
||||
const options = buildConfiguredModelOptions(
|
||||
[
|
||||
@@ -49,19 +73,20 @@ describe('model option helpers', () => {
|
||||
account({ id: 'beta5678', label: 'Beta', model: 'provider/model-beta', updatedAt: '2026-04-02T00:00:00.000Z' }),
|
||||
],
|
||||
[status('alpha1234'), status('beta5678')],
|
||||
vendors,
|
||||
'alpha1234',
|
||||
);
|
||||
|
||||
expect(options).toEqual([
|
||||
{
|
||||
modelRef: 'custom-alpha123/model-alpha',
|
||||
label: 'model-alpha',
|
||||
label: 'model-alpha (Alpha)',
|
||||
runtimeProviderKey: 'custom-alpha123',
|
||||
accountId: 'alpha1234',
|
||||
},
|
||||
{
|
||||
modelRef: 'custom-beta5678/provider/model-beta',
|
||||
label: 'provider/model-beta',
|
||||
label: 'provider/model-beta (Beta)',
|
||||
runtimeProviderKey: 'custom-beta5678',
|
||||
accountId: 'beta5678',
|
||||
},
|
||||
@@ -76,12 +101,13 @@ describe('model option helpers', () => {
|
||||
account({ id: 'delta3456', label: 'Delta', model: 'model-delta' }),
|
||||
],
|
||||
[status('gamma9012'), status('delta3456', false)],
|
||||
vendors,
|
||||
null,
|
||||
);
|
||||
|
||||
expect(options).toHaveLength(1);
|
||||
expect(options[0].modelRef).toBe('custom-gamma901/model-gamma');
|
||||
expect(options[0].label).toBe('model-gamma');
|
||||
expect(options[0].label).toBe('model-gamma (Alpha)');
|
||||
});
|
||||
|
||||
it('treats malformed provider snapshots as empty options', () => {
|
||||
@@ -89,6 +115,7 @@ describe('model option helpers', () => {
|
||||
buildConfiguredModelOptions(
|
||||
{} as ProviderAccount[],
|
||||
{} as ProviderWithKeyInfo[],
|
||||
{} as ProviderVendorInfo[],
|
||||
null,
|
||||
),
|
||||
).toEqual([]);
|
||||
|
||||
Reference in New Issue
Block a user