Skip to content

Commit 02ced6d

Browse files
committed
feat(chat): centralize LLM model configs and add provider model presets
1 parent d3fa5b2 commit 02ced6d

5 files changed

Lines changed: 406 additions & 119 deletions

File tree

apps/webuiapps/src/components/ChatPanel/index.module.scss

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,38 @@
356356
cursor: pointer;
357357
}
358358

359+
.modelSelectorWrapper {
360+
display: flex;
361+
align-items: center;
362+
gap: 4px;
363+
364+
.select {
365+
flex: 1;
366+
}
367+
368+
.fieldInput {
369+
flex: 1;
370+
}
371+
}
372+
373+
.manualToggleBtn {
374+
padding: 6px 8px;
375+
border: 1px solid rgba(255, 255, 255, 0.1);
376+
border-radius: 6px;
377+
background: transparent;
378+
cursor: pointer;
379+
display: flex;
380+
align-items: center;
381+
justify-content: center;
382+
color: rgba(255, 255, 255, 0.6);
383+
transition: all 0.2s;
384+
385+
&:hover {
386+
background: #282a2a;
387+
color: rgba(255, 255, 255, 0.9);
388+
}
389+
}
390+
359391
.settingsActions {
360392
display: flex;
361393
gap: 8px;

apps/webuiapps/src/components/ChatPanel/index.tsx

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import {
77
Maximize2,
88
ChevronDown,
99
ChevronRight,
10+
Pencil,
11+
List,
1012
} from 'lucide-react';
1113
import {
1214
chat,
@@ -18,6 +20,7 @@ import {
1820
type LLMProvider,
1921
type ChatMessage,
2022
} from '@/lib/llmClient';
23+
import { PROVIDER_MODELS } from '@/lib/llmModels';
2124
import {
2225
loadImageGenConfig,
2326
loadImageGenConfigSync,
@@ -1124,6 +1127,10 @@ const SettingsModal: React.FC<{
11241127
const [baseUrl, setBaseUrl] = useState(config?.baseUrl || getDefaultConfig('minimax').baseUrl);
11251128
const [model, setModel] = useState(config?.model || getDefaultConfig('minimax').model);
11261129
const [customHeaders, setCustomHeaders] = useState(config?.customHeaders || '');
1130+
const [manualModelMode, setManualModelMode] = useState(false);
1131+
1132+
const isPresetModel = PROVIDER_MODELS[provider]?.includes(model) ?? false;
1133+
const showDropdown = !manualModelMode && isPresetModel;
11271134

11281135
// Image gen settings
11291136
const [igProvider, setIgProvider] = useState<ImageGenProvider>(
@@ -1143,6 +1150,12 @@ const SettingsModal: React.FC<{
11431150
const defaults = getDefaultConfig(p);
11441151
setBaseUrl(defaults.baseUrl);
11451152
setModel(defaults.model);
1153+
setManualModelMode(false);
1154+
};
1155+
1156+
const handleModelChange = (newModel: string) => {
1157+
setModel(newModel);
1158+
setManualModelMode(false);
11461159
};
11471160

11481161
const handleIgProviderChange = (p: ImageGenProvider) => {
@@ -1168,6 +1181,8 @@ const SettingsModal: React.FC<{
11681181
<option value="anthropic">Anthropic</option>
11691182
<option value="deepseek">DeepSeek</option>
11701183
<option value="minimax">MiniMax</option>
1184+
<option value="z.ai">Z.ai</option>
1185+
<option value="kimi">Kimi</option>
11711186
</select>
11721187
</div>
11731188

@@ -1193,11 +1208,50 @@ const SettingsModal: React.FC<{
11931208

11941209
<div className={styles.field}>
11951210
<label className={styles.label}>Model</label>
1196-
<input
1197-
className={styles.fieldInput}
1198-
value={model}
1199-
onChange={(e) => setModel(e.target.value)}
1200-
/>
1211+
<div className={styles.modelSelectorWrapper}>
1212+
{showDropdown ? (
1213+
<>
1214+
<select
1215+
className={styles.select}
1216+
value={model}
1217+
onChange={(e) => handleModelChange(e.target.value)}
1218+
>
1219+
{PROVIDER_MODELS[provider]?.map((m) => (
1220+
<option key={m} value={m}>
1221+
{m}
1222+
</option>
1223+
))}
1224+
</select>
1225+
<button
1226+
type="button"
1227+
onClick={() => setManualModelMode(true)}
1228+
className={styles.manualToggleBtn}
1229+
title="Enter custom model name"
1230+
>
1231+
<Pencil size={14} />
1232+
</button>
1233+
</>
1234+
) : (
1235+
<>
1236+
<input
1237+
className={styles.fieldInput}
1238+
value={model}
1239+
onChange={(e) => setModel(e.target.value)}
1240+
placeholder="e.g. gpt-4-turbo"
1241+
/>
1242+
{isPresetModel && (
1243+
<button
1244+
type="button"
1245+
onClick={() => setManualModelMode(false)}
1246+
className={styles.manualToggleBtn}
1247+
title="Back to model list"
1248+
>
1249+
<List size={14} />
1250+
</button>
1251+
)}
1252+
</>
1253+
)}
1254+
</div>
12011255
</div>
12021256

12031257
<div className={styles.field}>

apps/webuiapps/src/lib/__tests__/llmClient.test.ts

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ import {
1414
loadConfigSync,
1515
saveConfig,
1616
chat,
17-
type LLMConfig,
1817
type ChatMessage,
1918
type ToolDef,
2019
} from '../llmClient';
20+
import type { LLMConfig } from '../llmModels';
2121

2222
// ─── Constants ────────────────────────────────────────────────────────────────
2323

@@ -98,37 +98,50 @@ describe('getDefaultConfig()', () => {
9898
it('returns correct defaults for openai', () => {
9999
const cfg = getDefaultConfig('openai');
100100
expect(cfg.provider).toBe('openai');
101-
expect(cfg.baseUrl).toBe('https://api.openai.com');
102-
expect(cfg.model).toBe('gpt-5.3-chat-latest');
101+
expect(cfg.baseUrl).toBe('https://api.openai.com/v1');
102+
expect(cfg.model).toBe('gpt-5.4');
103103
expect('apiKey' in cfg).toBe(false);
104104
});
105105

106106
it('returns correct defaults for anthropic', () => {
107107
const cfg = getDefaultConfig('anthropic');
108108
expect(cfg.provider).toBe('anthropic');
109-
expect(cfg.baseUrl).toBe('https://api.anthropic.com');
110-
expect(cfg.model).toBe('claude-opus-4-6');
109+
expect(cfg.baseUrl).toBe('https://api.anthropic.com/v1');
110+
expect(cfg.model).toBe('claude-sonnet-4-6');
111111
});
112112

113113
it('returns correct defaults for deepseek', () => {
114114
const cfg = getDefaultConfig('deepseek');
115115
expect(cfg.provider).toBe('deepseek');
116-
expect(cfg.baseUrl).toBe('https://api.deepseek.com');
116+
expect(cfg.baseUrl).toBe('https://api.deepseek.com/v1');
117117
expect(cfg.model).toBe('deepseek-chat');
118118
});
119119

120120
it('returns correct defaults for minimax', () => {
121121
const cfg = getDefaultConfig('minimax');
122122
expect(cfg.provider).toBe('minimax');
123-
expect(cfg.baseUrl).toBe('https://api.minimax.io/anthropic');
123+
expect(cfg.baseUrl).toBe('https://api.minimax.io/anthropic/v1');
124124
expect(cfg.model).toBe('MiniMax-M2.5');
125125
});
126126

127-
it('returns the same stable reference for the same provider', () => {
128-
// getDefaultConfig returns a direct reference to the internal constant (by design)
127+
it('returns correct defaults for z.ai', () => {
128+
const cfg = getDefaultConfig('z.ai');
129+
expect(cfg.provider).toBe('z.ai');
130+
expect(cfg.baseUrl).toBe('https://api.z.ai/api/coding/paas/v4');
131+
expect(cfg.model).toBe('glm-5');
132+
});
133+
134+
it('returns correct defaults for kimi', () => {
135+
const cfg = getDefaultConfig('kimi');
136+
expect(cfg.provider).toBe('kimi');
137+
expect(cfg.baseUrl).toBe('https://api.moonshot.cn/v1');
138+
expect(cfg.model).toBe('kimi-k2-5');
139+
});
140+
141+
it('returns consistent values for the same provider', () => {
129142
const a = getDefaultConfig('openai');
130143
const b = getDefaultConfig('openai');
131-
expect(a).toBe(b);
144+
expect(a).toStrictEqual(b);
132145
});
133146
});
134147

0 commit comments

Comments
 (0)