@nextclaw/ui 0.6.12 → 0.6.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/assets/{ChannelsList-DBDjwf-X.js → ChannelsList-DiSnpiW0.js} +1 -1
- package/dist/assets/ChatPage-DsaIrNHN.js +36 -0
- package/dist/assets/{DocBrowser-ZOplDEMS.js → DocBrowser-CnfcptGM.js} +1 -1
- package/dist/assets/{LogoBadge-2LMzEMwe.js → LogoBadge-BxZJ9BJT.js} +1 -1
- package/dist/assets/{MarketplacePage-D4JHYcB5.js → MarketplacePage-BI_J_DBQ.js} +2 -2
- package/dist/assets/ModelConfig-DfL8F4tN.js +1 -0
- package/dist/assets/ProvidersList-DpT_oFHZ.js +1 -0
- package/dist/assets/{RuntimeConfig-4sb3mpkd.js → RuntimeConfig-BNYR_Iag.js} +1 -1
- package/dist/assets/{SearchConfig-B4u_MxRG.js → SearchConfig-TDBl7Fjh.js} +1 -1
- package/dist/assets/{SecretsConfig-BQXblZvb.js → SecretsConfig-9OABNssV.js} +2 -2
- package/dist/assets/SessionsConfig-BRwntUDz.js +2 -0
- package/dist/assets/{card-BekAnCgX.js → card-BYnT3Mxo.js} +1 -1
- package/dist/assets/index-BCfS4UY1.css +1 -0
- package/dist/assets/index-BnUxgevr.js +8 -0
- package/dist/assets/index-CkqvHQAt.js +1 -0
- package/dist/assets/{input-MMn_Na9q.js → input-oaepEtqu.js} +1 -1
- package/dist/assets/{label-Dg2ydpN0.js → label-BIjHWZUm.js} +1 -1
- package/dist/assets/{page-layout-7K0rcz0I.js → page-layout-B6JXiSQB.js} +1 -1
- package/dist/assets/popover-LJQgv5l1.js +1 -0
- package/dist/assets/provider-models-D3B_xWXx.js +1 -0
- package/dist/assets/{session-run-status-CAdjSqeb.js → session-run-status-BZEH0QZp.js} +1 -1
- package/dist/assets/{switch-DnDMlDVu.js → switch-CnGQpdTp.js} +1 -1
- package/dist/assets/{tabs-custom-khLM8lWj.js → tabs-custom-CpSv7pDl.js} +1 -1
- package/dist/assets/useConfirmDialog-pqAlPdQZ.js +5 -0
- package/dist/assets/{vendor-d7E8OgNx.js → vendor-BKtTvQYU.js} +69 -64
- package/dist/index.html +3 -3
- package/package.json +1 -1
- package/src/api/types.ts +4 -0
- package/src/components/chat/ChatPage.tsx +16 -0
- package/src/components/chat/ChatSidebar.tsx +10 -1
- package/src/components/chat/chat-input/ChatInputBottomToolbar.tsx +12 -0
- package/src/components/chat/chat-input/components/ChatInputSlashPanelSection.tsx +3 -3
- package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputThinkingSelector.tsx +74 -0
- package/src/components/chat/chat-input/useChatInputBarController.ts +11 -2
- package/src/components/chat/chat-input.types.ts +8 -0
- package/src/components/chat/chat-page-data.ts +40 -3
- package/src/components/chat/chat-stream/transport.ts +3 -0
- package/src/components/chat/chat-stream/types.ts +3 -1
- package/src/components/chat/managers/chat-input.manager.ts +51 -0
- package/src/components/chat/stores/chat-input.store.ts +5 -1
- package/src/components/common/SearchableModelInput.tsx +22 -5
- package/src/components/config/ModelConfig.tsx +13 -12
- package/src/components/config/ProviderForm.tsx +292 -19
- package/src/lib/i18n.ts +15 -0
- package/src/lib/provider-models.ts +91 -3
- package/dist/assets/ChatPage-C18sGGk1.js +0 -36
- package/dist/assets/ModelConfig-DZVvdLFq.js +0 -1
- package/dist/assets/ProvidersList-Dum31480.js +0 -1
- package/dist/assets/SessionsConfig-Jk29xjQU.js +0 -2
- package/dist/assets/index-BXwjfCEO.css +0 -1
- package/dist/assets/index-Dl6t70wA.js +0 -8
- package/dist/assets/provider-models-y4mUDcGF.js +0 -1
- package/dist/assets/useConfirmDialog-BYA1XnVU.js +0 -5
|
@@ -33,33 +33,26 @@ export function ModelConfig() {
|
|
|
33
33
|
const workspaceHint = hintForPath('agents.defaults.workspace', uiHints);
|
|
34
34
|
|
|
35
35
|
const providerCatalog = useMemo(
|
|
36
|
-
() => buildProviderModelCatalog({ meta, config }),
|
|
36
|
+
() => buildProviderModelCatalog({ meta, config, onlyConfigured: true }),
|
|
37
37
|
[config, meta]
|
|
38
38
|
);
|
|
39
39
|
|
|
40
40
|
const providerMap = useMemo(() => new Map(providerCatalog.map((provider) => [provider.name, provider])), [providerCatalog]);
|
|
41
|
-
const selectedProvider = providerMap.get(providerName)
|
|
41
|
+
const selectedProvider = providerMap.get(providerName);
|
|
42
42
|
const selectedProviderName = selectedProvider?.name ?? '';
|
|
43
43
|
const selectedProviderAliases = useMemo(() => selectedProvider?.aliases ?? [], [selectedProvider]);
|
|
44
44
|
const selectedProviderModels = useMemo(() => selectedProvider?.models ?? [], [selectedProvider]);
|
|
45
45
|
|
|
46
|
-
useEffect(() => {
|
|
47
|
-
if (providerName || providerCatalog.length === 0) {
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
setProviderName(providerCatalog[0].name);
|
|
51
|
-
}, [providerName, providerCatalog]);
|
|
52
|
-
|
|
53
46
|
useEffect(() => {
|
|
54
47
|
if (!config?.agents?.defaults) {
|
|
55
48
|
return;
|
|
56
49
|
}
|
|
57
50
|
const currentModel = (config.agents.defaults.model || '').trim();
|
|
58
51
|
const matchedProvider = findProviderByModel(currentModel, providerCatalog);
|
|
59
|
-
const effectiveProvider = matchedProvider ??
|
|
52
|
+
const effectiveProvider = matchedProvider ?? '';
|
|
60
53
|
const aliases = providerMap.get(effectiveProvider)?.aliases ?? [];
|
|
61
54
|
setProviderName(effectiveProvider);
|
|
62
|
-
setModelId(toProviderLocalModel(currentModel, aliases));
|
|
55
|
+
setModelId(effectiveProvider ? toProviderLocalModel(currentModel, aliases) : '');
|
|
63
56
|
setWorkspace(config.agents.defaults.workspace || '');
|
|
64
57
|
}, [config, providerCatalog, providerMap]);
|
|
65
58
|
|
|
@@ -75,11 +68,14 @@ export function ModelConfig() {
|
|
|
75
68
|
}, [selectedProviderModels]);
|
|
76
69
|
|
|
77
70
|
const composedModel = useMemo(() => {
|
|
71
|
+
if (!selectedProvider) {
|
|
72
|
+
return '';
|
|
73
|
+
}
|
|
78
74
|
const normalizedModelId = toProviderLocalModel(modelId, selectedProviderAliases);
|
|
79
75
|
if (!normalizedModelId) {
|
|
80
76
|
return '';
|
|
81
77
|
}
|
|
82
|
-
return composeProviderModel(selectedProvider
|
|
78
|
+
return composeProviderModel(selectedProvider.prefix, normalizedModelId);
|
|
83
79
|
}, [modelId, selectedProvider, selectedProviderAliases]);
|
|
84
80
|
|
|
85
81
|
const modelHelpText = t('modelIdentifierHelp') || modelHint?.help || '';
|
|
@@ -90,6 +86,10 @@ export function ModelConfig() {
|
|
|
90
86
|
};
|
|
91
87
|
|
|
92
88
|
const handleModelChange = (nextModelId: string) => {
|
|
89
|
+
if (!selectedProvider) {
|
|
90
|
+
setModelId('');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
93
|
setModelId(toProviderLocalModel(nextModelId, selectedProviderAliases));
|
|
94
94
|
};
|
|
95
95
|
|
|
@@ -165,6 +165,7 @@ export function ModelConfig() {
|
|
|
165
165
|
value={modelId}
|
|
166
166
|
onChange={handleModelChange}
|
|
167
167
|
options={modelOptions}
|
|
168
|
+
disabled={!selectedProviderName}
|
|
168
169
|
placeholder={modelHint?.placeholder ?? 'gpt-5.1'}
|
|
169
170
|
className="sm:flex-1"
|
|
170
171
|
inputClassName="h-10 rounded-xl"
|
|
@@ -15,17 +15,19 @@ import { Button } from '@/components/ui/button';
|
|
|
15
15
|
import { Input } from '@/components/ui/input';
|
|
16
16
|
import { Label } from '@/components/ui/label';
|
|
17
17
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
18
|
+
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
|
18
19
|
import { MaskedInput } from '@/components/common/MaskedInput';
|
|
19
20
|
import { KeyValueEditor } from '@/components/common/KeyValueEditor';
|
|
20
21
|
import { StatusDot } from '@/components/ui/status-dot';
|
|
21
22
|
import { getLanguage, t } from '@/lib/i18n';
|
|
22
23
|
import { hintForPath } from '@/lib/config-hints';
|
|
23
|
-
import type { ProviderConfigView, ProviderConfigUpdate, ProviderConnectionTestRequest } from '@/api/types';
|
|
24
|
+
import type { ProviderConfigView, ProviderConfigUpdate, ProviderConnectionTestRequest, ThinkingLevel } from '@/api/types';
|
|
24
25
|
import { CircleDotDashed, Plus, X, Trash2, ChevronDown, Settings2 } from 'lucide-react';
|
|
25
26
|
import { toast } from 'sonner';
|
|
26
27
|
import { CONFIG_DETAIL_CARD_CLASS, CONFIG_EMPTY_DETAIL_CARD_CLASS } from './config-layout';
|
|
27
28
|
|
|
28
29
|
type WireApiType = 'auto' | 'chat' | 'responses';
|
|
30
|
+
type ModelThinkingConfig = Record<string, { supported: ThinkingLevel[]; default?: ThinkingLevel | null }>;
|
|
29
31
|
|
|
30
32
|
type ProviderFormProps = {
|
|
31
33
|
providerName?: string;
|
|
@@ -48,8 +50,11 @@ const EMPTY_PROVIDER_CONFIG: ProviderConfigView = {
|
|
|
48
50
|
apiBase: null,
|
|
49
51
|
extraHeaders: null,
|
|
50
52
|
wireApi: null,
|
|
51
|
-
models: []
|
|
53
|
+
models: [],
|
|
54
|
+
modelThinking: {}
|
|
52
55
|
};
|
|
56
|
+
const THINKING_LEVELS: ThinkingLevel[] = ['off', 'minimal', 'low', 'medium', 'high', 'adaptive', 'xhigh'];
|
|
57
|
+
const THINKING_LEVEL_SET = new Set<string>(THINKING_LEVELS);
|
|
53
58
|
|
|
54
59
|
function normalizeHeaders(input: Record<string, string> | null | undefined): Record<string, string> | null {
|
|
55
60
|
if (!input) {
|
|
@@ -160,6 +165,128 @@ function serializeModelsForSave(models: string[], defaultModels: string[]): stri
|
|
|
160
165
|
return models;
|
|
161
166
|
}
|
|
162
167
|
|
|
168
|
+
function parseThinkingLevel(value: unknown): ThinkingLevel | null {
|
|
169
|
+
if (typeof value !== 'string') {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
const normalized = value.trim().toLowerCase();
|
|
173
|
+
if (!normalized) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
return THINKING_LEVEL_SET.has(normalized) ? (normalized as ThinkingLevel) : null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function normalizeThinkingLevels(values: unknown): ThinkingLevel[] {
|
|
180
|
+
if (!Array.isArray(values)) {
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
const deduped: ThinkingLevel[] = [];
|
|
184
|
+
for (const value of values) {
|
|
185
|
+
const level = parseThinkingLevel(value);
|
|
186
|
+
if (!level || deduped.includes(level)) {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
deduped.push(level);
|
|
190
|
+
}
|
|
191
|
+
return deduped;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function normalizeModelThinkingConfig(
|
|
195
|
+
input: ProviderConfigView['modelThinking'],
|
|
196
|
+
aliases: string[]
|
|
197
|
+
): ModelThinkingConfig {
|
|
198
|
+
if (!input) {
|
|
199
|
+
return {};
|
|
200
|
+
}
|
|
201
|
+
const normalized: ModelThinkingConfig = {};
|
|
202
|
+
for (const [rawModel, rawValue] of Object.entries(input)) {
|
|
203
|
+
const model = toProviderLocalModelId(rawModel, aliases);
|
|
204
|
+
if (!model) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const supported = normalizeThinkingLevels(rawValue?.supported);
|
|
208
|
+
if (supported.length === 0) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const defaultLevel = parseThinkingLevel(rawValue?.default);
|
|
212
|
+
normalized[model] =
|
|
213
|
+
defaultLevel && supported.includes(defaultLevel)
|
|
214
|
+
? { supported, default: defaultLevel }
|
|
215
|
+
: { supported };
|
|
216
|
+
}
|
|
217
|
+
return normalized;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function normalizeModelThinkingForModels(modelThinking: ModelThinkingConfig, models: string[]): ModelThinkingConfig {
|
|
221
|
+
const modelSet = new Set(models.map((item) => item.trim()).filter(Boolean));
|
|
222
|
+
const normalized: ModelThinkingConfig = {};
|
|
223
|
+
for (const [model, entry] of Object.entries(modelThinking)) {
|
|
224
|
+
if (!modelSet.has(model)) {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const supported = normalizeThinkingLevels(entry.supported);
|
|
228
|
+
if (supported.length === 0) {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
const defaultLevel = parseThinkingLevel(entry.default);
|
|
232
|
+
normalized[model] =
|
|
233
|
+
defaultLevel && supported.includes(defaultLevel)
|
|
234
|
+
? { supported, default: defaultLevel }
|
|
235
|
+
: { supported };
|
|
236
|
+
}
|
|
237
|
+
return normalized;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function modelThinkingEqual(left: ModelThinkingConfig, right: ModelThinkingConfig): boolean {
|
|
241
|
+
const leftKeys = Object.keys(left).sort();
|
|
242
|
+
const rightKeys = Object.keys(right).sort();
|
|
243
|
+
if (leftKeys.length !== rightKeys.length) {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
for (let index = 0; index < leftKeys.length; index += 1) {
|
|
247
|
+
const key = leftKeys[index];
|
|
248
|
+
if (key !== rightKeys[index]) {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
const leftEntry = left[key];
|
|
252
|
+
const rightEntry = right[key];
|
|
253
|
+
if (!leftEntry || !rightEntry) {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
const leftSupported = [...leftEntry.supported].sort();
|
|
257
|
+
const rightSupported = [...rightEntry.supported].sort();
|
|
258
|
+
if (!modelListsEqual(leftSupported, rightSupported)) {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
if ((leftEntry.default ?? null) !== (rightEntry.default ?? null)) {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function formatThinkingLevelLabel(level: ThinkingLevel): string {
|
|
269
|
+
if (level === 'off') {
|
|
270
|
+
return t('chatThinkingLevelOff');
|
|
271
|
+
}
|
|
272
|
+
if (level === 'minimal') {
|
|
273
|
+
return t('chatThinkingLevelMinimal');
|
|
274
|
+
}
|
|
275
|
+
if (level === 'low') {
|
|
276
|
+
return t('chatThinkingLevelLow');
|
|
277
|
+
}
|
|
278
|
+
if (level === 'medium') {
|
|
279
|
+
return t('chatThinkingLevelMedium');
|
|
280
|
+
}
|
|
281
|
+
if (level === 'high') {
|
|
282
|
+
return t('chatThinkingLevelHigh');
|
|
283
|
+
}
|
|
284
|
+
if (level === 'adaptive') {
|
|
285
|
+
return t('chatThinkingLevelAdaptive');
|
|
286
|
+
}
|
|
287
|
+
return t('chatThinkingLevelXhigh');
|
|
288
|
+
}
|
|
289
|
+
|
|
163
290
|
function resolvePreferredAuthMethodId(params: {
|
|
164
291
|
providerName?: string;
|
|
165
292
|
methods: ProviderAuthMethodOption[];
|
|
@@ -272,6 +399,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
272
399
|
const [extraHeaders, setExtraHeaders] = useState<Record<string, string> | null>(null);
|
|
273
400
|
const [wireApi, setWireApi] = useState<WireApiType>('auto');
|
|
274
401
|
const [models, setModels] = useState<string[]>([]);
|
|
402
|
+
const [modelThinking, setModelThinking] = useState<ModelThinkingConfig>({});
|
|
275
403
|
const [modelDraft, setModelDraft] = useState('');
|
|
276
404
|
const [providerDisplayName, setProviderDisplayName] = useState('');
|
|
277
405
|
const [showAdvanced, setShowAdvanced] = useState(false);
|
|
@@ -323,6 +451,14 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
323
451
|
() => resolveEditableModels(defaultModels, currentModels),
|
|
324
452
|
[defaultModels, currentModels]
|
|
325
453
|
);
|
|
454
|
+
const currentModelThinking = useMemo(
|
|
455
|
+
() =>
|
|
456
|
+
normalizeModelThinkingForModels(
|
|
457
|
+
normalizeModelThinkingConfig(resolvedProviderConfig.modelThinking, providerModelAliases),
|
|
458
|
+
currentEditableModels
|
|
459
|
+
),
|
|
460
|
+
[currentEditableModels, providerModelAliases, resolvedProviderConfig.modelThinking]
|
|
461
|
+
);
|
|
326
462
|
const language = getLanguage();
|
|
327
463
|
const apiBaseHelpText =
|
|
328
464
|
providerSpec?.apiBaseHelp?.[language] ||
|
|
@@ -443,6 +579,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
443
579
|
setExtraHeaders(null);
|
|
444
580
|
setWireApi('auto');
|
|
445
581
|
setModels([]);
|
|
582
|
+
setModelThinking({});
|
|
446
583
|
setModelDraft('');
|
|
447
584
|
setProviderDisplayName('');
|
|
448
585
|
setAuthSessionId(null);
|
|
@@ -457,6 +594,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
457
594
|
setExtraHeaders(resolvedProviderConfig.extraHeaders || null);
|
|
458
595
|
setWireApi(currentWireApi);
|
|
459
596
|
setModels(currentEditableModels);
|
|
597
|
+
setModelThinking(currentModelThinking);
|
|
460
598
|
setModelDraft('');
|
|
461
599
|
setProviderDisplayName(effectiveDisplayName);
|
|
462
600
|
setAuthSessionId(null);
|
|
@@ -469,6 +607,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
469
607
|
resolvedProviderConfig.extraHeaders,
|
|
470
608
|
currentWireApi,
|
|
471
609
|
currentEditableModels,
|
|
610
|
+
currentModelThinking,
|
|
472
611
|
effectiveDisplayName,
|
|
473
612
|
preferredAuthMethodId,
|
|
474
613
|
clearAuthPollTimer
|
|
@@ -476,6 +615,10 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
476
615
|
|
|
477
616
|
useEffect(() => () => clearAuthPollTimer(), [clearAuthPollTimer]);
|
|
478
617
|
|
|
618
|
+
useEffect(() => {
|
|
619
|
+
setModelThinking((prev) => normalizeModelThinkingForModels(prev, models));
|
|
620
|
+
}, [models]);
|
|
621
|
+
|
|
479
622
|
const hasChanges = useMemo(() => {
|
|
480
623
|
if (!providerName) {
|
|
481
624
|
return false;
|
|
@@ -485,11 +628,20 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
485
628
|
const headersChanged = !headersEqual(extraHeaders, currentHeaders);
|
|
486
629
|
const wireApiChanged = providerSpec?.supportsWireApi ? wireApi !== currentWireApi : false;
|
|
487
630
|
const modelsChanged = !modelListsEqual(models, currentEditableModels);
|
|
631
|
+
const modelThinkingChanged = !modelThinkingEqual(modelThinking, currentModelThinking);
|
|
488
632
|
const displayNameChanged = isCustomProvider
|
|
489
633
|
? providerDisplayName.trim() !== effectiveDisplayName
|
|
490
634
|
: false;
|
|
491
635
|
|
|
492
|
-
return
|
|
636
|
+
return (
|
|
637
|
+
apiKeyChanged ||
|
|
638
|
+
apiBaseChanged ||
|
|
639
|
+
headersChanged ||
|
|
640
|
+
wireApiChanged ||
|
|
641
|
+
modelsChanged ||
|
|
642
|
+
modelThinkingChanged ||
|
|
643
|
+
displayNameChanged
|
|
644
|
+
);
|
|
493
645
|
}, [
|
|
494
646
|
providerName,
|
|
495
647
|
isCustomProvider,
|
|
@@ -504,7 +656,9 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
504
656
|
wireApi,
|
|
505
657
|
currentWireApi,
|
|
506
658
|
models,
|
|
507
|
-
currentEditableModels
|
|
659
|
+
currentEditableModels,
|
|
660
|
+
modelThinking,
|
|
661
|
+
currentModelThinking
|
|
508
662
|
]);
|
|
509
663
|
|
|
510
664
|
const handleAddModel = () => {
|
|
@@ -520,6 +674,45 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
520
674
|
setModelDraft('');
|
|
521
675
|
};
|
|
522
676
|
|
|
677
|
+
const toggleModelThinkingLevel = (modelName: string, level: ThinkingLevel) => {
|
|
678
|
+
setModelThinking((prev) => {
|
|
679
|
+
const currentEntry = prev[modelName];
|
|
680
|
+
const currentLevels = currentEntry?.supported ?? [];
|
|
681
|
+
const nextLevels = currentLevels.includes(level)
|
|
682
|
+
? currentLevels.filter((item) => item !== level)
|
|
683
|
+
: THINKING_LEVELS.filter((item) => item === level || currentLevels.includes(item));
|
|
684
|
+
if (nextLevels.length === 0) {
|
|
685
|
+
const next = { ...prev };
|
|
686
|
+
delete next[modelName];
|
|
687
|
+
return next;
|
|
688
|
+
}
|
|
689
|
+
const nextDefault =
|
|
690
|
+
currentEntry?.default && nextLevels.includes(currentEntry.default) ? currentEntry.default : undefined;
|
|
691
|
+
return {
|
|
692
|
+
...prev,
|
|
693
|
+
[modelName]: nextDefault ? { supported: nextLevels, default: nextDefault } : { supported: nextLevels }
|
|
694
|
+
};
|
|
695
|
+
});
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
const setModelThinkingDefault = (modelName: string, level: ThinkingLevel | null) => {
|
|
699
|
+
setModelThinking((prev) => {
|
|
700
|
+
const currentEntry = prev[modelName];
|
|
701
|
+
if (!currentEntry || currentEntry.supported.length === 0) {
|
|
702
|
+
return prev;
|
|
703
|
+
}
|
|
704
|
+
if (level && !currentEntry.supported.includes(level)) {
|
|
705
|
+
return prev;
|
|
706
|
+
}
|
|
707
|
+
return {
|
|
708
|
+
...prev,
|
|
709
|
+
[modelName]: level
|
|
710
|
+
? { supported: currentEntry.supported, default: level }
|
|
711
|
+
: { supported: currentEntry.supported }
|
|
712
|
+
};
|
|
713
|
+
});
|
|
714
|
+
};
|
|
715
|
+
|
|
523
716
|
const handleSubmit = (e: React.FormEvent) => {
|
|
524
717
|
e.preventDefault();
|
|
525
718
|
|
|
@@ -555,6 +748,9 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
555
748
|
if (!modelListsEqual(models, currentEditableModels)) {
|
|
556
749
|
payload.models = serializeModelsForSave(models, defaultModels);
|
|
557
750
|
}
|
|
751
|
+
if (!modelThinkingEqual(modelThinking, currentModelThinking)) {
|
|
752
|
+
payload.modelThinking = normalizeModelThinkingForModels(modelThinking, models);
|
|
753
|
+
}
|
|
558
754
|
|
|
559
755
|
updateProvider.mutate({ provider: providerName, data: payload });
|
|
560
756
|
};
|
|
@@ -876,22 +1072,99 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
876
1072
|
</div>
|
|
877
1073
|
) : (
|
|
878
1074
|
<div className="flex flex-wrap gap-2">
|
|
879
|
-
{models.map((modelName) =>
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
<
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
onClick={() => setModels((prev) => prev.filter((name) => name !== modelName))}
|
|
888
|
-
className="inline-flex h-5 w-5 items-center justify-center rounded-full text-gray-400 transition-opacity hover:bg-gray-100 hover:text-gray-600 opacity-100 md:opacity-0 md:group-hover:opacity-100 md:group-focus-within:opacity-100"
|
|
889
|
-
aria-label={t('remove')}
|
|
1075
|
+
{models.map((modelName) => {
|
|
1076
|
+
const thinkingEntry = modelThinking[modelName];
|
|
1077
|
+
const supportedLevels = thinkingEntry?.supported ?? [];
|
|
1078
|
+
const defaultThinkingLevel = thinkingEntry?.default ?? null;
|
|
1079
|
+
return (
|
|
1080
|
+
<div
|
|
1081
|
+
key={modelName}
|
|
1082
|
+
className="group inline-flex max-w-full items-center gap-1 rounded-full border border-gray-200 bg-white px-3 py-1.5"
|
|
890
1083
|
>
|
|
891
|
-
<
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
1084
|
+
<span className="max-w-[140px] truncate text-sm text-gray-800 sm:max-w-[220px]">{modelName}</span>
|
|
1085
|
+
<Popover>
|
|
1086
|
+
<PopoverTrigger asChild>
|
|
1087
|
+
<button
|
|
1088
|
+
type="button"
|
|
1089
|
+
className="inline-flex h-5 w-5 items-center justify-center rounded-full text-gray-400 transition-opacity hover:bg-gray-100 hover:text-gray-600 opacity-100 md:opacity-0 md:group-hover:opacity-100 md:group-focus-within:opacity-100"
|
|
1090
|
+
aria-label={t('providerModelThinkingTitle')}
|
|
1091
|
+
title={t('providerModelThinkingTitle')}
|
|
1092
|
+
>
|
|
1093
|
+
<Settings2 className="h-3 w-3" />
|
|
1094
|
+
</button>
|
|
1095
|
+
</PopoverTrigger>
|
|
1096
|
+
<PopoverContent className="w-80 space-y-3">
|
|
1097
|
+
<div className="space-y-1">
|
|
1098
|
+
<p className="text-xs font-semibold text-gray-800">{t('providerModelThinkingTitle')}</p>
|
|
1099
|
+
<p className="text-xs text-gray-500">{t('providerModelThinkingHint')}</p>
|
|
1100
|
+
</div>
|
|
1101
|
+
<div className="flex flex-wrap gap-1.5">
|
|
1102
|
+
{THINKING_LEVELS.map((level) => {
|
|
1103
|
+
const selected = supportedLevels.includes(level);
|
|
1104
|
+
return (
|
|
1105
|
+
<button
|
|
1106
|
+
key={level}
|
|
1107
|
+
type="button"
|
|
1108
|
+
onClick={() => toggleModelThinkingLevel(modelName, level)}
|
|
1109
|
+
className={`rounded-full border px-2.5 py-1 text-xs font-medium transition-colors ${
|
|
1110
|
+
selected
|
|
1111
|
+
? 'border-primary bg-primary text-white'
|
|
1112
|
+
: 'border-gray-200 bg-white text-gray-600 hover:border-primary/40 hover:text-primary'
|
|
1113
|
+
}`}
|
|
1114
|
+
>
|
|
1115
|
+
{formatThinkingLevelLabel(level)}
|
|
1116
|
+
</button>
|
|
1117
|
+
);
|
|
1118
|
+
})}
|
|
1119
|
+
</div>
|
|
1120
|
+
<div className="space-y-1.5">
|
|
1121
|
+
<Label className="text-xs font-medium text-gray-700">{t('providerModelThinkingDefault')}</Label>
|
|
1122
|
+
<Select
|
|
1123
|
+
value={defaultThinkingLevel ?? '__none__'}
|
|
1124
|
+
onValueChange={(value) =>
|
|
1125
|
+
setModelThinkingDefault(
|
|
1126
|
+
modelName,
|
|
1127
|
+
value === '__none__' ? null : (value as ThinkingLevel)
|
|
1128
|
+
)
|
|
1129
|
+
}
|
|
1130
|
+
disabled={supportedLevels.length === 0}
|
|
1131
|
+
>
|
|
1132
|
+
<SelectTrigger className="h-8 rounded-lg bg-white text-xs">
|
|
1133
|
+
<SelectValue />
|
|
1134
|
+
</SelectTrigger>
|
|
1135
|
+
<SelectContent>
|
|
1136
|
+
<SelectItem value="__none__">{t('providerModelThinkingDefaultNone')}</SelectItem>
|
|
1137
|
+
{supportedLevels.map((level) => (
|
|
1138
|
+
<SelectItem key={level} value={level}>
|
|
1139
|
+
{formatThinkingLevelLabel(level)}
|
|
1140
|
+
</SelectItem>
|
|
1141
|
+
))}
|
|
1142
|
+
</SelectContent>
|
|
1143
|
+
</Select>
|
|
1144
|
+
{supportedLevels.length === 0 ? (
|
|
1145
|
+
<p className="text-xs text-gray-500">{t('providerModelThinkingNoSupported')}</p>
|
|
1146
|
+
) : null}
|
|
1147
|
+
</div>
|
|
1148
|
+
</PopoverContent>
|
|
1149
|
+
</Popover>
|
|
1150
|
+
<button
|
|
1151
|
+
type="button"
|
|
1152
|
+
onClick={() => {
|
|
1153
|
+
setModels((prev) => prev.filter((name) => name !== modelName));
|
|
1154
|
+
setModelThinking((prev) => {
|
|
1155
|
+
const next = { ...prev };
|
|
1156
|
+
delete next[modelName];
|
|
1157
|
+
return next;
|
|
1158
|
+
});
|
|
1159
|
+
}}
|
|
1160
|
+
className="inline-flex h-5 w-5 items-center justify-center rounded-full text-gray-400 transition-opacity hover:bg-gray-100 hover:text-gray-600 opacity-100 md:opacity-0 md:group-hover:opacity-100 md:group-focus-within:opacity-100"
|
|
1161
|
+
aria-label={t('remove')}
|
|
1162
|
+
>
|
|
1163
|
+
<X className="h-3 w-3" />
|
|
1164
|
+
</button>
|
|
1165
|
+
</div>
|
|
1166
|
+
);
|
|
1167
|
+
})}
|
|
895
1168
|
</div>
|
|
896
1169
|
)}
|
|
897
1170
|
</div>
|
package/src/lib/i18n.ts
CHANGED
|
@@ -272,6 +272,14 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
|
|
|
272
272
|
},
|
|
273
273
|
providerModelsEmptyShort: { zh: '暂无可用模型', en: 'No models available' },
|
|
274
274
|
providerAddFirstModel: { zh: '添加第一个模型', en: 'Add first model' },
|
|
275
|
+
providerModelThinkingTitle: { zh: '思考档位能力', en: 'Thinking Capability' },
|
|
276
|
+
providerModelThinkingHint: {
|
|
277
|
+
zh: '为该模型声明可切换的思考档位,聊天会话将按这里的能力展示下拉。',
|
|
278
|
+
en: 'Declare supported thinking levels for this model. Chat sessions will show the selector accordingly.'
|
|
279
|
+
},
|
|
280
|
+
providerModelThinkingDefault: { zh: '默认思考档位', en: 'Default Thinking Level' },
|
|
281
|
+
providerModelThinkingDefaultNone: { zh: '无默认(回落 off)', en: 'No default (fallback off)' },
|
|
282
|
+
providerModelThinkingNoSupported: { zh: '请先至少选择一个支持档位。', en: 'Select at least one supported level first.' },
|
|
275
283
|
providerDisplayNameHelpShort: { zh: '便于区分多个自定义提供商', en: 'Helps distinguish multiple custom providers' },
|
|
276
284
|
providerApiBaseHelpShort: { zh: '一般只需填写域名,系统自动补全路径', en: 'Usually just the domain; path auto-appended' },
|
|
277
285
|
providerExtraHeadersHelpShort: { zh: '可选,用于自定义鉴权等场景', en: 'Optional, for custom auth etc.' },
|
|
@@ -528,6 +536,13 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
|
|
|
528
536
|
chatSelectAgent: { zh: '选择 Agent', en: 'Select Agent' },
|
|
529
537
|
chatModelLabel: { zh: '对话模型', en: 'Chat Model' },
|
|
530
538
|
chatSelectModel: { zh: '选择模型', en: 'Select model' },
|
|
539
|
+
chatThinkingLevelOff: { zh: '思考关闭', en: 'Thinking Off' },
|
|
540
|
+
chatThinkingLevelMinimal: { zh: '思考 Minimal', en: 'Thinking Minimal' },
|
|
541
|
+
chatThinkingLevelLow: { zh: '思考 Low', en: 'Thinking Low' },
|
|
542
|
+
chatThinkingLevelMedium: { zh: '思考 Medium', en: 'Thinking Medium' },
|
|
543
|
+
chatThinkingLevelHigh: { zh: '思考 High', en: 'Thinking High' },
|
|
544
|
+
chatThinkingLevelAdaptive: { zh: '思考 Adaptive', en: 'Thinking Adaptive' },
|
|
545
|
+
chatThinkingLevelXhigh: { zh: '思考 XHigh', en: 'Thinking XHigh' },
|
|
531
546
|
chatSessionTypeLabel: { zh: '会话类型', en: 'Session Type' },
|
|
532
547
|
chatSessionTypeNative: { zh: '原生', en: 'Native' },
|
|
533
548
|
chatSessionTypeCodex: { zh: 'Codex', en: 'Codex' },
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import type { ConfigMetaView, ConfigView, ProviderConfigView } from '@/api/types';
|
|
1
|
+
import type { ConfigMetaView, ConfigView, ProviderConfigView, ThinkingLevel } from '@/api/types';
|
|
2
|
+
|
|
3
|
+
const THINKING_LEVELS: ThinkingLevel[] = ['off', 'minimal', 'low', 'medium', 'high', 'adaptive', 'xhigh'];
|
|
4
|
+
const THINKING_LEVEL_SET = new Set<string>(THINKING_LEVELS);
|
|
5
|
+
|
|
6
|
+
export type ModelThinkingCapability = {
|
|
7
|
+
supported: ThinkingLevel[];
|
|
8
|
+
default?: ThinkingLevel | null;
|
|
9
|
+
};
|
|
2
10
|
|
|
3
11
|
export type ProviderModelCatalogItem = {
|
|
4
12
|
name: string;
|
|
@@ -6,6 +14,7 @@ export type ProviderModelCatalogItem = {
|
|
|
6
14
|
prefix: string;
|
|
7
15
|
aliases: string[];
|
|
8
16
|
models: string[];
|
|
17
|
+
modelThinking: Record<string, ModelThinkingCapability>;
|
|
9
18
|
configured: boolean;
|
|
10
19
|
};
|
|
11
20
|
|
|
@@ -59,9 +68,73 @@ export function composeProviderModel(prefix: string, localModel: string): string
|
|
|
59
68
|
return `${normalizedPrefix}/${normalizedModel}`;
|
|
60
69
|
}
|
|
61
70
|
|
|
71
|
+
function parseThinkingLevel(value: unknown): ThinkingLevel | null {
|
|
72
|
+
if (typeof value !== 'string') {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const normalized = value.trim().toLowerCase();
|
|
76
|
+
if (!normalized) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
return THINKING_LEVEL_SET.has(normalized) ? (normalized as ThinkingLevel) : null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizeThinkingLevels(values: unknown): ThinkingLevel[] {
|
|
83
|
+
if (!Array.isArray(values)) {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
const deduped: ThinkingLevel[] = [];
|
|
87
|
+
for (const value of values) {
|
|
88
|
+
const level = parseThinkingLevel(value);
|
|
89
|
+
if (!level || deduped.includes(level)) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
deduped.push(level);
|
|
93
|
+
}
|
|
94
|
+
return deduped;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function normalizeModelThinkingMap(
|
|
98
|
+
input: ProviderConfigView['modelThinking'],
|
|
99
|
+
aliases: string[]
|
|
100
|
+
): Record<string, ModelThinkingCapability> {
|
|
101
|
+
if (!input) {
|
|
102
|
+
return {};
|
|
103
|
+
}
|
|
104
|
+
const normalized: Record<string, ModelThinkingCapability> = {};
|
|
105
|
+
for (const [rawModel, rawValue] of Object.entries(input)) {
|
|
106
|
+
const localModel = toProviderLocalModel(rawModel, aliases);
|
|
107
|
+
if (!localModel) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const supported = normalizeThinkingLevels(rawValue?.supported);
|
|
111
|
+
if (supported.length === 0) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const defaultLevel = parseThinkingLevel(rawValue?.default);
|
|
115
|
+
normalized[localModel] =
|
|
116
|
+
defaultLevel && supported.includes(defaultLevel)
|
|
117
|
+
? { supported, default: defaultLevel }
|
|
118
|
+
: { supported };
|
|
119
|
+
}
|
|
120
|
+
return normalized;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function resolveModelThinkingCapability(
|
|
124
|
+
map: Record<string, ModelThinkingCapability>,
|
|
125
|
+
model: string,
|
|
126
|
+
aliases: string[]
|
|
127
|
+
): ModelThinkingCapability | null {
|
|
128
|
+
const localModel = toProviderLocalModel(model, aliases);
|
|
129
|
+
if (!localModel) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
return map[localModel] ?? null;
|
|
133
|
+
}
|
|
134
|
+
|
|
62
135
|
export function findProviderByModel(
|
|
63
136
|
model: string,
|
|
64
|
-
providerCatalog: Array<{ name: string; aliases: string[] }>
|
|
137
|
+
providerCatalog: Array<{ name: string; aliases: string[]; models?: string[] }>
|
|
65
138
|
): string | null {
|
|
66
139
|
const trimmed = model.trim();
|
|
67
140
|
if (!trimmed) {
|
|
@@ -81,7 +154,20 @@ export function findProviderByModel(
|
|
|
81
154
|
}
|
|
82
155
|
}
|
|
83
156
|
}
|
|
84
|
-
|
|
157
|
+
if (bestMatch) {
|
|
158
|
+
return bestMatch.name;
|
|
159
|
+
}
|
|
160
|
+
for (const provider of providerCatalog) {
|
|
161
|
+
const normalizedModel = toProviderLocalModel(trimmed, provider.aliases);
|
|
162
|
+
if (!normalizedModel) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const models = normalizeStringList(provider.models ?? []);
|
|
166
|
+
if (models.some((modelId) => modelId === normalizedModel)) {
|
|
167
|
+
return provider.name;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
85
171
|
}
|
|
86
172
|
|
|
87
173
|
function isProviderConfigured(provider: ProviderConfigView | undefined): boolean {
|
|
@@ -108,6 +194,7 @@ export function buildProviderModelCatalog(params: {
|
|
|
108
194
|
(providerConfig?.models ?? []).map((model) => toProviderLocalModel(model, aliases))
|
|
109
195
|
);
|
|
110
196
|
const models = normalizeStringList([...defaultModels, ...customModels]);
|
|
197
|
+
const modelThinking = normalizeModelThinkingMap(providerConfig?.modelThinking, aliases);
|
|
111
198
|
const configDisplayName = providerConfig?.displayName?.trim();
|
|
112
199
|
const configured = isProviderConfigured(providerConfig);
|
|
113
200
|
|
|
@@ -117,6 +204,7 @@ export function buildProviderModelCatalog(params: {
|
|
|
117
204
|
prefix,
|
|
118
205
|
aliases,
|
|
119
206
|
models,
|
|
207
|
+
modelThinking,
|
|
120
208
|
configured
|
|
121
209
|
} satisfies ProviderModelCatalogItem;
|
|
122
210
|
});
|