@nextclaw/ui 0.9.6 → 0.9.8

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 (69) hide show
  1. package/CHANGELOG.md +16 -2
  2. package/dist/assets/ChannelsList-CIMYaIji.js +1 -0
  3. package/dist/assets/{ChatPage-DM1ewbWf.js → ChatPage-B5UpeEIp.js} +2 -2
  4. package/dist/assets/{DocBrowser-BLv77lJ0.js → DocBrowser-BJ610SPa.js} +1 -1
  5. package/dist/assets/{LogoBadge-D7j1al-w.js → LogoBadge-BKq1GKWP.js} +1 -1
  6. package/dist/assets/MarketplacePage-Bs3sLsgx.js +49 -0
  7. package/dist/assets/{McpMarketplacePage-DpMjaD3m.js → McpMarketplacePage-BWTguHCs.js} +2 -2
  8. package/dist/assets/ModelConfig-B-oTP-Bc.js +1 -0
  9. package/dist/assets/ProvidersList-r7bD0-R0.js +1 -0
  10. package/dist/assets/RemoteAccessPage-D7On6waK.js +1 -0
  11. package/dist/assets/{RuntimeConfig-BbX4yFKy.js → RuntimeConfig-C11xVxH9.js} +1 -1
  12. package/dist/assets/{SearchConfig-BmmmeyJd.js → SearchConfig-BVZdCxiM.js} +1 -1
  13. package/dist/assets/{SecretsConfig-CWG8J01H.js → SecretsConfig-DuEDdC3X.js} +2 -2
  14. package/dist/assets/SessionsConfig-Y-Blf_-K.js +2 -0
  15. package/dist/assets/{chat-message-CGXiVhyN.js → chat-message-B6VCCEXF.js} +1 -1
  16. package/dist/assets/index-DfEAJJsA.css +1 -0
  17. package/dist/assets/index-DvA7S11O.js +8 -0
  18. package/dist/assets/{label-CCSffS1D.js → label-DzwitL78.js} +1 -1
  19. package/dist/assets/{page-layout-ud8wZ8gX.js → page-layout-DEq5N_8L.js} +1 -1
  20. package/dist/assets/popover-CY54V8F6.js +1 -0
  21. package/dist/assets/provider-models-BOeNnjk9.js +1 -0
  22. package/dist/assets/{security-config-DJJUCMov.js → security-config-CgbYP57d.js} +1 -1
  23. package/dist/assets/skeleton-zjQZMWu9.js +1 -0
  24. package/dist/assets/{status-dot-Fz9-eKsl.js → status-dot-CU_P0tvO.js} +1 -1
  25. package/dist/assets/{switch-B-_SrMSL.js → switch-PvjTvlcs.js} +1 -1
  26. package/dist/assets/{tabs-custom-6Tm1ZHfS.js → tabs-custom-Bke5J9ny.js} +1 -1
  27. package/dist/assets/useConfirmDialog-8tzzp_oW.js +1 -0
  28. package/dist/assets/vendor-CmQZsDAE.js +436 -0
  29. package/dist/index.html +3 -3
  30. package/package.json +4 -4
  31. package/src/App.tsx +36 -39
  32. package/src/account/components/account-panel.tsx +93 -0
  33. package/src/account/managers/account.manager.ts +179 -0
  34. package/src/account/stores/account.store.ts +68 -0
  35. package/src/api/types.ts +2 -0
  36. package/src/app-query-client.ts +10 -0
  37. package/src/components/config/ProviderForm.tsx +91 -641
  38. package/src/components/config/ProvidersList.tsx +10 -5
  39. package/src/components/config/provider-advanced-settings-section.tsx +92 -0
  40. package/src/components/config/provider-auth-section.tsx +113 -0
  41. package/src/components/config/provider-enabled-field.tsx +20 -0
  42. package/src/components/config/provider-form-support.ts +344 -0
  43. package/src/components/config/provider-models-section.tsx +198 -0
  44. package/src/components/config/provider-pill-selector.tsx +39 -0
  45. package/src/components/config/provider-status-badge.tsx +21 -0
  46. package/src/components/layout/Sidebar.tsx +26 -0
  47. package/src/components/remote/RemoteAccessPage.tsx +162 -442
  48. package/src/hooks/useRemoteAccess.ts +7 -6
  49. package/src/lib/i18n.remote.ts +108 -4
  50. package/src/lib/provider-models.ts +2 -2
  51. package/src/presenter/app-presenter-context.tsx +20 -0
  52. package/src/presenter/app.presenter.ts +12 -0
  53. package/src/remote/managers/remote-access.manager.ts +196 -0
  54. package/src/remote/remote-access.query.ts +78 -0
  55. package/src/remote/stores/remote-access.store.ts +44 -0
  56. package/dist/assets/ChannelsList-Byfj2R01.js +0 -1
  57. package/dist/assets/MarketplacePage-DuskLKYh.js +0 -49
  58. package/dist/assets/ModelConfig-ubaecweS.js +0 -1
  59. package/dist/assets/ProvidersList-w8MJH2LI.js +0 -1
  60. package/dist/assets/RemoteAccessPage-D79_5Kbn.js +0 -1
  61. package/dist/assets/SessionsConfig-D-vg_Lgv.js +0 -2
  62. package/dist/assets/index-COrhpAdh.css +0 -1
  63. package/dist/assets/index-CeRbsQ90.js +0 -8
  64. package/dist/assets/index-Ct7FQpxN.js +0 -1
  65. package/dist/assets/popover-Bfoe6YBX.js +0 -1
  66. package/dist/assets/provider-models-D3B_xWXx.js +0 -1
  67. package/dist/assets/skeleton-IOOTmHzP.js +0 -1
  68. package/dist/assets/useConfirmDialog-BeOW2bOI.js +0 -5
  69. package/dist/assets/vendor-CwsIoNvJ.js +0 -442
@@ -39,7 +39,10 @@ export function ProvidersList() {
39
39
  const uiHints = schema?.uiHints;
40
40
  const providers = meta?.providers ?? [];
41
41
  const providersConfig = config?.providers ?? {};
42
- const configuredCount = providers.filter((provider) => providersConfig[provider.name]?.apiKeySet).length;
42
+ const configuredCount = providers.filter((provider) => {
43
+ const current = providersConfig[provider.name];
44
+ return current?.enabled !== false && current?.apiKeySet;
45
+ }).length;
43
46
 
44
47
  const tabs = [
45
48
  { id: 'installed', label: t('providersTabConfigured'), count: configuredCount },
@@ -53,7 +56,8 @@ export function ProvidersList() {
53
56
  return baseProviders
54
57
  .filter((provider) => {
55
58
  if (activeTab === 'installed') {
56
- return Boolean(baseConfig[provider.name]?.apiKeySet);
59
+ const current = baseConfig[provider.name];
60
+ return Boolean(current?.enabled !== false && current?.apiKeySet);
57
61
  }
58
62
  return true;
59
63
  })
@@ -131,7 +135,8 @@ export function ProvidersList() {
131
135
  <div className="min-h-0 flex-1 space-y-2 overflow-y-auto p-3">
132
136
  {filteredProviders.map((provider) => {
133
137
  const providerConfig = config.providers[provider.name];
134
- const isReady = Boolean(providerConfig?.apiKeySet);
138
+ const isEnabled = providerConfig?.enabled !== false;
139
+ const isReady = Boolean(isEnabled && providerConfig?.apiKeySet);
135
140
  const isActive = selectedName === provider.name;
136
141
  const providerLabel = providerConfig?.displayName?.trim() || provider.displayName || provider.name;
137
142
  const providerHint = hintForPath(`providers.${provider.name}`, uiHints);
@@ -169,8 +174,8 @@ export function ProvidersList() {
169
174
  </div>
170
175
  </div>
171
176
  <StatusDot
172
- status={isReady ? 'ready' : 'setup'}
173
- label={isReady ? t('statusReady') : t('statusSetup')}
177
+ status={isEnabled ? (isReady ? 'ready' : 'setup') : 'inactive'}
178
+ label={isEnabled ? (isReady ? t('statusReady') : t('statusSetup')) : t('disabled')}
174
179
  className="min-w-[56px] justify-center"
175
180
  />
176
181
  </div>
@@ -0,0 +1,92 @@
1
+ import { KeyValueEditor } from '@/components/common/KeyValueEditor';
2
+ import { Label } from '@/components/ui/label';
3
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
4
+ import { t } from '@/lib/i18n';
5
+ import { ChevronDown, Settings2 } from 'lucide-react';
6
+ import { ProviderPillSelector } from './provider-pill-selector';
7
+
8
+ type WireApiType = 'auto' | 'chat' | 'responses';
9
+
10
+ type ProviderAdvancedSettingsSectionProps = {
11
+ showAdvanced: boolean;
12
+ onShowAdvancedChange: (value: boolean) => void;
13
+ supportsWireApi: boolean;
14
+ wireApiLabel: string;
15
+ wireApi: WireApiType;
16
+ onWireApiChange: (value: WireApiType) => void;
17
+ shouldUseWireApiPills: boolean;
18
+ wireApiSelectOptions: Array<{ value: string; label: string }>;
19
+ extraHeadersLabel: string;
20
+ extraHeaders: Record<string, string> | null;
21
+ onExtraHeadersChange: (value: Record<string, string> | null) => void;
22
+ };
23
+
24
+ export function ProviderAdvancedSettingsSection(props: ProviderAdvancedSettingsSectionProps) {
25
+ const {
26
+ showAdvanced,
27
+ onShowAdvancedChange,
28
+ supportsWireApi,
29
+ wireApiLabel,
30
+ wireApi,
31
+ onWireApiChange,
32
+ shouldUseWireApiPills,
33
+ wireApiSelectOptions,
34
+ extraHeadersLabel,
35
+ extraHeaders,
36
+ onExtraHeadersChange
37
+ } = props;
38
+
39
+ return (
40
+ <div className="border-t border-gray-100 pt-4">
41
+ <button
42
+ type="button"
43
+ onClick={() => onShowAdvancedChange(!showAdvanced)}
44
+ className="flex w-full items-center justify-between text-sm text-gray-600 transition-colors hover:text-gray-900"
45
+ >
46
+ <span className="flex items-center gap-1.5">
47
+ <Settings2 className="h-3.5 w-3.5" />
48
+ {t('providerAdvancedSettings')}
49
+ </span>
50
+ <ChevronDown className={`h-4 w-4 transition-transform ${showAdvanced ? 'rotate-180' : ''}`} />
51
+ </button>
52
+
53
+ {showAdvanced ? (
54
+ <div className="mt-4 space-y-5">
55
+ {supportsWireApi ? (
56
+ <div className="space-y-2">
57
+ <Label htmlFor="wireApi" className="text-sm font-medium text-gray-900">
58
+ {wireApiLabel}
59
+ </Label>
60
+ {shouldUseWireApiPills ? (
61
+ <ProviderPillSelector
62
+ value={wireApi}
63
+ onChange={(value) => onWireApiChange(value as WireApiType)}
64
+ options={wireApiSelectOptions}
65
+ />
66
+ ) : (
67
+ <Select value={wireApi} onValueChange={(value) => onWireApiChange(value as WireApiType)}>
68
+ <SelectTrigger className="rounded-xl">
69
+ <SelectValue />
70
+ </SelectTrigger>
71
+ <SelectContent>
72
+ {wireApiSelectOptions.map((option) => (
73
+ <SelectItem key={option.value} value={option.value}>
74
+ {option.label}
75
+ </SelectItem>
76
+ ))}
77
+ </SelectContent>
78
+ </Select>
79
+ )}
80
+ </div>
81
+ ) : null}
82
+
83
+ <div className="space-y-2">
84
+ <Label className="text-sm font-medium text-gray-900">{extraHeadersLabel}</Label>
85
+ <KeyValueEditor value={extraHeaders} onChange={onExtraHeadersChange} />
86
+ <p className="text-xs text-gray-500">{t('providerExtraHeadersHelpShort')}</p>
87
+ </div>
88
+ </div>
89
+ ) : null}
90
+ </div>
91
+ );
92
+ }
@@ -0,0 +1,113 @@
1
+ import { Button } from '@/components/ui/button';
2
+ import { Label } from '@/components/ui/label';
3
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
4
+ import { t } from '@/lib/i18n';
5
+ import type { ProviderSpecView } from '@/api/types';
6
+ import { ProviderPillSelector } from './provider-pill-selector';
7
+
8
+ type ProviderAuthSectionProps = {
9
+ providerAuth: ProviderSpecView['auth'];
10
+ providerAuthNote: string;
11
+ providerAuthMethodOptions: Array<{ value: string; label: string }>;
12
+ providerAuthMethodsCount: number;
13
+ selectedAuthMethodHint: string;
14
+ shouldUseAuthMethodPills: boolean;
15
+ resolvedAuthMethodId: string;
16
+ onAuthMethodChange: (value: string) => void;
17
+ onStartProviderAuth: () => void;
18
+ onImportProviderAuthFromCli: () => void;
19
+ startPending: boolean;
20
+ importPending: boolean;
21
+ authSessionId: string | null;
22
+ authStatusMessage: string;
23
+ };
24
+
25
+ export function ProviderAuthSection(props: ProviderAuthSectionProps) {
26
+ const {
27
+ providerAuth,
28
+ providerAuthNote,
29
+ providerAuthMethodOptions,
30
+ providerAuthMethodsCount,
31
+ selectedAuthMethodHint,
32
+ shouldUseAuthMethodPills,
33
+ resolvedAuthMethodId,
34
+ onAuthMethodChange,
35
+ onStartProviderAuth,
36
+ onImportProviderAuthFromCli,
37
+ startPending,
38
+ importPending,
39
+ authSessionId,
40
+ authStatusMessage
41
+ } = props;
42
+
43
+ if (providerAuth?.kind !== 'device_code') {
44
+ return null;
45
+ }
46
+
47
+ return (
48
+ <div className="space-y-2 rounded-xl border border-primary/20 bg-primary-50/50 p-3">
49
+ <Label className="text-sm font-medium text-gray-900">
50
+ {providerAuth.displayName || t('providerAuthSectionTitle')}
51
+ </Label>
52
+ {providerAuthNote ? <p className="text-xs text-gray-600">{providerAuthNote}</p> : null}
53
+ {providerAuthMethodsCount > 1 ? (
54
+ <div className="space-y-2">
55
+ <Label className="text-xs font-medium text-gray-700">{t('providerAuthMethodLabel')}</Label>
56
+ {shouldUseAuthMethodPills ? (
57
+ <ProviderPillSelector
58
+ value={resolvedAuthMethodId}
59
+ onChange={onAuthMethodChange}
60
+ options={providerAuthMethodOptions}
61
+ />
62
+ ) : (
63
+ <Select value={resolvedAuthMethodId} onValueChange={onAuthMethodChange}>
64
+ <SelectTrigger className="h-8 rounded-lg bg-white">
65
+ <SelectValue placeholder={t('providerAuthMethodPlaceholder')} />
66
+ </SelectTrigger>
67
+ <SelectContent>
68
+ {providerAuthMethodOptions.map((method) => (
69
+ <SelectItem key={method.value} value={method.value}>
70
+ {method.label}
71
+ </SelectItem>
72
+ ))}
73
+ </SelectContent>
74
+ </Select>
75
+ )}
76
+ {selectedAuthMethodHint ? <p className="text-xs text-gray-500">{selectedAuthMethodHint}</p> : null}
77
+ </div>
78
+ ) : null}
79
+ <div className="flex flex-wrap items-center gap-2">
80
+ <Button
81
+ type="button"
82
+ variant="outline"
83
+ size="sm"
84
+ onClick={onStartProviderAuth}
85
+ disabled={startPending || Boolean(authSessionId)}
86
+ >
87
+ {startPending
88
+ ? t('providerAuthStarting')
89
+ : authSessionId
90
+ ? t('providerAuthAuthorizing')
91
+ : t('providerAuthAuthorizeInBrowser')}
92
+ </Button>
93
+ {providerAuth.supportsCliImport ? (
94
+ <Button
95
+ type="button"
96
+ variant="outline"
97
+ size="sm"
98
+ onClick={onImportProviderAuthFromCli}
99
+ disabled={importPending}
100
+ >
101
+ {importPending ? t('providerAuthImporting') : t('providerAuthImportFromCli')}
102
+ </Button>
103
+ ) : null}
104
+ {authSessionId ? (
105
+ <span className="text-xs text-gray-500">
106
+ {t('providerAuthSessionLabel')}: {authSessionId.slice(0, 8)}…
107
+ </span>
108
+ ) : null}
109
+ </div>
110
+ {authStatusMessage ? <p className="text-xs text-gray-600">{authStatusMessage}</p> : null}
111
+ </div>
112
+ );
113
+ }
@@ -0,0 +1,20 @@
1
+ import { Label } from '@/components/ui/label';
2
+ import { Switch } from '@/components/ui/switch';
3
+ import { t } from '@/lib/i18n';
4
+
5
+ type ProviderEnabledFieldProps = {
6
+ enabled: boolean;
7
+ onChange: (enabled: boolean) => void;
8
+ };
9
+
10
+ export function ProviderEnabledField(props: ProviderEnabledFieldProps) {
11
+ return (
12
+ <div className="flex items-center justify-between rounded-xl border border-gray-200 bg-gray-50 px-4 py-3">
13
+ <Label className="text-sm font-medium text-gray-900">{t('enabled')}</Label>
14
+ <div className="flex items-center gap-3">
15
+ <span className="text-xs text-gray-500">{props.enabled ? t('enabled') : t('disabled')}</span>
16
+ <Switch checked={props.enabled} onCheckedChange={props.onChange} />
17
+ </div>
18
+ </div>
19
+ );
20
+ }
@@ -0,0 +1,344 @@
1
+ import { getLanguage, t } from '@/lib/i18n';
2
+ import type { ProviderConfigUpdate, ProviderConfigView, ThinkingLevel } from '@/api/types';
3
+
4
+ type WireApiType = 'auto' | 'chat' | 'responses';
5
+ type ModelThinkingConfig = Record<string, { supported: ThinkingLevel[]; default?: ThinkingLevel | null }>;
6
+ type ProviderAuthMethodOption = {
7
+ id: string;
8
+ };
9
+
10
+ const EMPTY_PROVIDER_CONFIG: ProviderConfigView = {
11
+ enabled: true,
12
+ displayName: '',
13
+ apiKeySet: false,
14
+ apiKeyMasked: undefined,
15
+ apiBase: null,
16
+ extraHeaders: null,
17
+ wireApi: null,
18
+ models: [],
19
+ modelThinking: {}
20
+ };
21
+
22
+ const THINKING_LEVELS: ThinkingLevel[] = ['off', 'minimal', 'low', 'medium', 'high', 'adaptive', 'xhigh'];
23
+ const THINKING_LEVEL_SET = new Set<string>(THINKING_LEVELS);
24
+
25
+ function normalizeHeaders(input: Record<string, string> | null | undefined): Record<string, string> | null {
26
+ if (!input) {
27
+ return null;
28
+ }
29
+ const entries = Object.entries(input)
30
+ .map(([key, value]) => [key.trim(), value] as const)
31
+ .filter(([key]) => key.length > 0);
32
+ if (entries.length === 0) {
33
+ return null;
34
+ }
35
+ return Object.fromEntries(entries);
36
+ }
37
+
38
+ function headersEqual(
39
+ left: Record<string, string> | null | undefined,
40
+ right: Record<string, string> | null | undefined
41
+ ): boolean {
42
+ const a = normalizeHeaders(left);
43
+ const b = normalizeHeaders(right);
44
+ if (a === null && b === null) {
45
+ return true;
46
+ }
47
+ if (!a || !b) {
48
+ return false;
49
+ }
50
+ const aEntries = Object.entries(a).sort(([ak], [bk]) => ak.localeCompare(bk));
51
+ const bEntries = Object.entries(b).sort(([ak], [bk]) => ak.localeCompare(bk));
52
+ if (aEntries.length !== bEntries.length) {
53
+ return false;
54
+ }
55
+ return aEntries.every(([key, value], index) => key === bEntries[index][0] && value === bEntries[index][1]);
56
+ }
57
+
58
+ function normalizeModelList(input: string[] | null | undefined): string[] {
59
+ if (!input || input.length === 0) {
60
+ return [];
61
+ }
62
+ const deduped = new Set<string>();
63
+ for (const item of input) {
64
+ const trimmed = item.trim();
65
+ if (trimmed) {
66
+ deduped.add(trimmed);
67
+ }
68
+ }
69
+ return [...deduped];
70
+ }
71
+
72
+ function stripProviderPrefix(model: string, prefix: string): string {
73
+ const trimmed = model.trim();
74
+ if (!trimmed || !prefix.trim()) {
75
+ return trimmed;
76
+ }
77
+ const fullPrefix = `${prefix.trim()}/`;
78
+ if (trimmed.startsWith(fullPrefix)) {
79
+ return trimmed.slice(fullPrefix.length);
80
+ }
81
+ return trimmed;
82
+ }
83
+
84
+ function toProviderLocalModelId(model: string, aliases: string[]): string {
85
+ let normalized = model.trim();
86
+ if (!normalized) {
87
+ return '';
88
+ }
89
+ for (const alias of aliases) {
90
+ const cleanAlias = alias.trim();
91
+ if (!cleanAlias) {
92
+ continue;
93
+ }
94
+ normalized = stripProviderPrefix(normalized, cleanAlias);
95
+ }
96
+ return normalized.trim();
97
+ }
98
+
99
+ function modelListsEqual(left: string[], right: string[]): boolean {
100
+ if (left.length !== right.length) {
101
+ return false;
102
+ }
103
+ return left.every((item, index) => item === right[index]);
104
+ }
105
+
106
+ function mergeModelLists(base: string[], extra: string[]): string[] {
107
+ const merged = [...base];
108
+ for (const item of extra) {
109
+ if (!merged.includes(item)) {
110
+ merged.push(item);
111
+ }
112
+ }
113
+ return merged;
114
+ }
115
+
116
+ function resolveEditableModels(defaultModels: string[], savedModels: string[]): string[] {
117
+ if (savedModels.length === 0) {
118
+ return defaultModels;
119
+ }
120
+ const looksLikeLegacyCustomList = savedModels.every((model) => !defaultModels.includes(model));
121
+ if (looksLikeLegacyCustomList) {
122
+ return mergeModelLists(defaultModels, savedModels);
123
+ }
124
+ return savedModels;
125
+ }
126
+
127
+ function serializeModelsForSave(models: string[], defaultModels: string[]): string[] {
128
+ if (modelListsEqual(models, defaultModels)) {
129
+ return [];
130
+ }
131
+ return models;
132
+ }
133
+
134
+ function applyEnabledPatch(payload: ProviderConfigUpdate, enabled: boolean, currentEnabled: boolean): void {
135
+ if (enabled !== currentEnabled) {
136
+ payload.enabled = enabled;
137
+ }
138
+ }
139
+
140
+ function parseThinkingLevel(value: unknown): ThinkingLevel | null {
141
+ if (typeof value !== 'string') {
142
+ return null;
143
+ }
144
+ const normalized = value.trim().toLowerCase();
145
+ if (!normalized) {
146
+ return null;
147
+ }
148
+ return THINKING_LEVEL_SET.has(normalized) ? (normalized as ThinkingLevel) : null;
149
+ }
150
+
151
+ function normalizeThinkingLevels(values: unknown): ThinkingLevel[] {
152
+ if (!Array.isArray(values)) {
153
+ return [];
154
+ }
155
+ const deduped: ThinkingLevel[] = [];
156
+ for (const value of values) {
157
+ const level = parseThinkingLevel(value);
158
+ if (!level || deduped.includes(level)) {
159
+ continue;
160
+ }
161
+ deduped.push(level);
162
+ }
163
+ return deduped;
164
+ }
165
+
166
+ function normalizeModelThinkingConfig(
167
+ input: ProviderConfigView['modelThinking'],
168
+ aliases: string[]
169
+ ): ModelThinkingConfig {
170
+ if (!input) {
171
+ return {};
172
+ }
173
+ const normalized: ModelThinkingConfig = {};
174
+ for (const [rawModel, rawValue] of Object.entries(input)) {
175
+ const model = toProviderLocalModelId(rawModel, aliases);
176
+ if (!model) {
177
+ continue;
178
+ }
179
+ const supported = normalizeThinkingLevels(rawValue?.supported);
180
+ if (supported.length === 0) {
181
+ continue;
182
+ }
183
+ const defaultLevel = parseThinkingLevel(rawValue?.default);
184
+ normalized[model] =
185
+ defaultLevel && supported.includes(defaultLevel)
186
+ ? { supported, default: defaultLevel }
187
+ : { supported };
188
+ }
189
+ return normalized;
190
+ }
191
+
192
+ function normalizeModelThinkingForModels(modelThinking: ModelThinkingConfig, models: string[]): ModelThinkingConfig {
193
+ const modelSet = new Set(models.map((item) => item.trim()).filter(Boolean));
194
+ const normalized: ModelThinkingConfig = {};
195
+ for (const [model, entry] of Object.entries(modelThinking)) {
196
+ if (!modelSet.has(model)) {
197
+ continue;
198
+ }
199
+ const supported = normalizeThinkingLevels(entry.supported);
200
+ if (supported.length === 0) {
201
+ continue;
202
+ }
203
+ const defaultLevel = parseThinkingLevel(entry.default);
204
+ normalized[model] =
205
+ defaultLevel && supported.includes(defaultLevel)
206
+ ? { supported, default: defaultLevel }
207
+ : { supported };
208
+ }
209
+ return normalized;
210
+ }
211
+
212
+ function modelThinkingEqual(left: ModelThinkingConfig, right: ModelThinkingConfig): boolean {
213
+ const leftKeys = Object.keys(left).sort();
214
+ const rightKeys = Object.keys(right).sort();
215
+ if (leftKeys.length !== rightKeys.length) {
216
+ return false;
217
+ }
218
+ for (let index = 0; index < leftKeys.length; index += 1) {
219
+ const key = leftKeys[index];
220
+ if (key !== rightKeys[index]) {
221
+ return false;
222
+ }
223
+ const leftEntry = left[key];
224
+ const rightEntry = right[key];
225
+ if (!leftEntry || !rightEntry) {
226
+ return false;
227
+ }
228
+ const leftSupported = [...leftEntry.supported].sort();
229
+ const rightSupported = [...rightEntry.supported].sort();
230
+ if (!modelListsEqual(leftSupported, rightSupported)) {
231
+ return false;
232
+ }
233
+ if ((leftEntry.default ?? null) !== (rightEntry.default ?? null)) {
234
+ return false;
235
+ }
236
+ }
237
+ return true;
238
+ }
239
+
240
+ function formatThinkingLevelLabel(level: ThinkingLevel): string {
241
+ if (level === 'off') {
242
+ return t('chatThinkingLevelOff');
243
+ }
244
+ if (level === 'minimal') {
245
+ return t('chatThinkingLevelMinimal');
246
+ }
247
+ if (level === 'low') {
248
+ return t('chatThinkingLevelLow');
249
+ }
250
+ if (level === 'medium') {
251
+ return t('chatThinkingLevelMedium');
252
+ }
253
+ if (level === 'high') {
254
+ return t('chatThinkingLevelHigh');
255
+ }
256
+ if (level === 'adaptive') {
257
+ return t('chatThinkingLevelAdaptive');
258
+ }
259
+ return t('chatThinkingLevelXhigh');
260
+ }
261
+
262
+ function resolvePreferredAuthMethodId(params: {
263
+ providerName?: string;
264
+ methods: ProviderAuthMethodOption[];
265
+ defaultMethodId?: string;
266
+ language: ReturnType<typeof getLanguage>;
267
+ }): string {
268
+ const { providerName, methods, defaultMethodId, language } = params;
269
+ if (methods.length === 0) {
270
+ return '';
271
+ }
272
+
273
+ const methodIdMap = new Map<string, string>();
274
+ for (const method of methods) {
275
+ const methodId = method.id.trim();
276
+ if (methodId) {
277
+ methodIdMap.set(methodId.toLowerCase(), methodId);
278
+ }
279
+ }
280
+
281
+ const pick = (...candidates: string[]): string | undefined => {
282
+ for (const candidate of candidates) {
283
+ const resolved = methodIdMap.get(candidate.toLowerCase());
284
+ if (resolved) {
285
+ return resolved;
286
+ }
287
+ }
288
+ return undefined;
289
+ };
290
+
291
+ const normalizedDefault = defaultMethodId?.trim();
292
+ if (providerName === 'minimax-portal') {
293
+ if (language === 'zh') {
294
+ return pick('cn', 'china-mainland') ?? pick(normalizedDefault ?? '') ?? methods[0]?.id ?? '';
295
+ }
296
+ if (language === 'en') {
297
+ return pick('global', 'intl', 'international') ?? pick(normalizedDefault ?? '') ?? methods[0]?.id ?? '';
298
+ }
299
+ }
300
+
301
+ if (normalizedDefault) {
302
+ const matchedDefault = pick(normalizedDefault);
303
+ if (matchedDefault) {
304
+ return matchedDefault;
305
+ }
306
+ }
307
+
308
+ if (language === 'zh') {
309
+ return pick('cn') ?? methods[0]?.id ?? '';
310
+ }
311
+ if (language === 'en') {
312
+ return pick('global') ?? methods[0]?.id ?? '';
313
+ }
314
+
315
+ return methods[0]?.id ?? '';
316
+ }
317
+
318
+ function shouldUsePillSelector(params: {
319
+ required: boolean;
320
+ hasDefault: boolean;
321
+ optionCount: number;
322
+ }): boolean {
323
+ return params.required && params.hasDefault && params.optionCount > 1 && params.optionCount <= 3;
324
+ }
325
+
326
+ export type { ModelThinkingConfig, ProviderAuthMethodOption, WireApiType };
327
+ export {
328
+ applyEnabledPatch,
329
+ EMPTY_PROVIDER_CONFIG,
330
+ formatThinkingLevelLabel,
331
+ headersEqual,
332
+ modelListsEqual,
333
+ modelThinkingEqual,
334
+ normalizeHeaders,
335
+ normalizeModelList,
336
+ normalizeModelThinkingConfig,
337
+ normalizeModelThinkingForModels,
338
+ resolveEditableModels,
339
+ resolvePreferredAuthMethodId,
340
+ serializeModelsForSave,
341
+ shouldUsePillSelector,
342
+ THINKING_LEVELS,
343
+ toProviderLocalModelId
344
+ };