@nextclaw/ui 0.6.12 → 0.6.13

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.
Files changed (53) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/assets/{ChannelsList-DBDjwf-X.js → ChannelsList-CXLzowHj.js} +1 -1
  3. package/dist/assets/ChatPage-CvtonrzM.js +36 -0
  4. package/dist/assets/{DocBrowser-ZOplDEMS.js → DocBrowser-4NK6-Q_u.js} +1 -1
  5. package/dist/assets/{LogoBadge-2LMzEMwe.js → LogoBadge-NI7KQCLa.js} +1 -1
  6. package/dist/assets/{MarketplacePage-D4JHYcB5.js → MarketplacePage-n7y-pif2.js} +2 -2
  7. package/dist/assets/ModelConfig-DztCs0mA.js +1 -0
  8. package/dist/assets/ProvidersList-hSzfE0pG.js +1 -0
  9. package/dist/assets/{RuntimeConfig-4sb3mpkd.js → RuntimeConfig-CKFGVus7.js} +1 -1
  10. package/dist/assets/{SearchConfig-B4u_MxRG.js → SearchConfig-Cxs1744q.js} +1 -1
  11. package/dist/assets/{SecretsConfig-BQXblZvb.js → SecretsConfig-C90UckNB.js} +2 -2
  12. package/dist/assets/SessionsConfig-CRor418P.js +2 -0
  13. package/dist/assets/{card-BekAnCgX.js → card-BQiPUGaa.js} +1 -1
  14. package/dist/assets/index-BCfS4UY1.css +1 -0
  15. package/dist/assets/index-CB5eJOGS.js +8 -0
  16. package/dist/assets/index-CkqvHQAt.js +1 -0
  17. package/dist/assets/{input-MMn_Na9q.js → input-DmFFMdAk.js} +1 -1
  18. package/dist/assets/{label-Dg2ydpN0.js → label-BHvlZoIz.js} +1 -1
  19. package/dist/assets/{page-layout-7K0rcz0I.js → page-layout-COPE9JyG.js} +1 -1
  20. package/dist/assets/popover-gcypYeec.js +1 -0
  21. package/dist/assets/provider-models-D3B_xWXx.js +1 -0
  22. package/dist/assets/{session-run-status-CAdjSqeb.js → session-run-status-BgNvd_-a.js} +1 -1
  23. package/dist/assets/{switch-DnDMlDVu.js → switch-BampMwqT.js} +1 -1
  24. package/dist/assets/{tabs-custom-khLM8lWj.js → tabs-custom-DSQeYaKd.js} +1 -1
  25. package/dist/assets/useConfirmDialog-DZUn23Li.js +5 -0
  26. package/dist/assets/{vendor-d7E8OgNx.js → vendor-BKtTvQYU.js} +69 -64
  27. package/dist/index.html +3 -3
  28. package/package.json +1 -1
  29. package/src/api/types.ts +4 -0
  30. package/src/components/chat/ChatPage.tsx +16 -0
  31. package/src/components/chat/chat-input/ChatInputBottomToolbar.tsx +12 -0
  32. package/src/components/chat/chat-input/components/ChatInputSlashPanelSection.tsx +3 -3
  33. package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputThinkingSelector.tsx +74 -0
  34. package/src/components/chat/chat-input/useChatInputBarController.ts +11 -2
  35. package/src/components/chat/chat-input.types.ts +8 -0
  36. package/src/components/chat/chat-page-data.ts +40 -3
  37. package/src/components/chat/chat-stream/transport.ts +3 -0
  38. package/src/components/chat/chat-stream/types.ts +3 -1
  39. package/src/components/chat/managers/chat-input.manager.ts +51 -0
  40. package/src/components/chat/stores/chat-input.store.ts +5 -1
  41. package/src/components/common/SearchableModelInput.tsx +22 -5
  42. package/src/components/config/ModelConfig.tsx +13 -12
  43. package/src/components/config/ProviderForm.tsx +292 -19
  44. package/src/lib/i18n.ts +15 -0
  45. package/src/lib/provider-models.ts +91 -3
  46. package/dist/assets/ChatPage-C18sGGk1.js +0 -36
  47. package/dist/assets/ModelConfig-DZVvdLFq.js +0 -1
  48. package/dist/assets/ProvidersList-Dum31480.js +0 -1
  49. package/dist/assets/SessionsConfig-Jk29xjQU.js +0 -2
  50. package/dist/assets/index-BXwjfCEO.css +0 -1
  51. package/dist/assets/index-Dl6t70wA.js +0 -8
  52. package/dist/assets/provider-models-y4mUDcGF.js +0 -1
  53. package/dist/assets/useConfirmDialog-BYA1XnVU.js +0 -5
@@ -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 apiKeyChanged || apiBaseChanged || headersChanged || wireApiChanged || modelsChanged || displayNameChanged;
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
- <div
881
- key={modelName}
882
- className="group inline-flex max-w-full items-center gap-1 rounded-full border border-gray-200 bg-white px-3 py-1.5"
883
- >
884
- <span className="max-w-[180px] truncate text-sm text-gray-800 sm:max-w-[240px]">{modelName}</span>
885
- <button
886
- type="button"
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
- <X className="h-3 w-3" />
892
- </button>
893
- </div>
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
- return bestMatch?.name ?? null;
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
  });