bingocode 1.0.15 → 1.0.17
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/package.json +1 -1
- package/src/cli/ProviderPanel.tsx +53 -54
- package/src/commands/resume/resume.tsx +16 -0
- package/src/components/IdeOnboardingDialog.tsx +1 -1
- package/src/components/LogoV2/WelcomeV2.tsx +3 -3
- package/src/server/config/providerPresets.ts +31 -19
- package/src/server/config/providers.yaml +207 -145
- package/src/server/services/providerService.ts +23 -9
- package/src/server/types/provider.ts +3 -3
- package/src/utils/managedEnv.ts +3 -3
- package/src/utils/proxy.ts +27 -0
package/package.json
CHANGED
|
@@ -19,6 +19,7 @@ type Provider = {
|
|
|
19
19
|
baseUrl?: string;
|
|
20
20
|
notes?: string;
|
|
21
21
|
isCurrent?: boolean;
|
|
22
|
+
models?: { main: string; haiku: string; sonnet: string; opus: string };
|
|
22
23
|
};
|
|
23
24
|
|
|
24
25
|
type Preset = {
|
|
@@ -30,7 +31,6 @@ type Preset = {
|
|
|
30
31
|
apiFormat?: string;
|
|
31
32
|
needsApiKey?: boolean;
|
|
32
33
|
websiteUrl?: string;
|
|
33
|
-
defaultModels?: { main: string; haiku: string; sonnet: string; opus: string };
|
|
34
34
|
fields?: ProviderField[];
|
|
35
35
|
};
|
|
36
36
|
|
|
@@ -38,7 +38,6 @@ type Stage =
|
|
|
38
38
|
| 'list'
|
|
39
39
|
| 'add_select_preset'
|
|
40
40
|
| 'add_input_fields'
|
|
41
|
-
| 'switch'
|
|
42
41
|
| 'test_select'
|
|
43
42
|
| 'delete_select'
|
|
44
43
|
| 'delete_confirm'
|
|
@@ -87,7 +86,7 @@ export const ProviderPanel: React.FC<{
|
|
|
87
86
|
const [slotTable, setSlotTable] = useState<Record<string, SlotEntry>>({});
|
|
88
87
|
const [slotProviderModels, setSlotProviderModels] = useState<Record<string, string[]>>({});
|
|
89
88
|
const [currentSlotName, setCurrentSlotName] = useState<string>('main');
|
|
90
|
-
const [slotLoadingMsg, setSlotLoadingMsg] = useState('');
|
|
89
|
+
const [slotLoadingMsg, setSlotLoadingMsg] = useState<string>('');
|
|
91
90
|
|
|
92
91
|
const base = apiUrl.replace(/\/+$/, '');
|
|
93
92
|
|
|
@@ -104,8 +103,9 @@ export const ProviderPanel: React.FC<{
|
|
|
104
103
|
return { list, currentId: cur };
|
|
105
104
|
};
|
|
106
105
|
|
|
107
|
-
const loadProviders = useCallback(async () => {
|
|
108
|
-
setLoading(true);
|
|
106
|
+
const loadProviders = useCallback(async (opts?: { keepError?: boolean }) => {
|
|
107
|
+
setLoading(true);
|
|
108
|
+
if (!opts?.keepError) setErr(null);
|
|
109
109
|
try {
|
|
110
110
|
const res = await axios.get(`${base}/api/providers`);
|
|
111
111
|
const { list, currentId } = parseListResp(res.data);
|
|
@@ -171,18 +171,6 @@ export const ProviderPanel: React.FC<{
|
|
|
171
171
|
);
|
|
172
172
|
|
|
173
173
|
// Actions
|
|
174
|
-
const doSetCurrent = async (id: string) => {
|
|
175
|
-
setErr(null); setOpMsg(null);
|
|
176
|
-
try {
|
|
177
|
-
await axios.post(`${base}/api/providers/current`, { id });
|
|
178
|
-
setOpMsg(`已切换当前 Provider -> ${id}`);
|
|
179
|
-
await loadProviders();
|
|
180
|
-
setStage('list');
|
|
181
|
-
} catch (e: any) {
|
|
182
|
-
setErr(e?.response?.data?.message || e?.message || '切换失败');
|
|
183
|
-
}
|
|
184
|
-
};
|
|
185
|
-
|
|
186
174
|
const doCreate = async (
|
|
187
175
|
presetId: string,
|
|
188
176
|
name: string,
|
|
@@ -193,17 +181,16 @@ export const ProviderPanel: React.FC<{
|
|
|
193
181
|
setStage('creating');
|
|
194
182
|
setErr(null); setOpMsg(null);
|
|
195
183
|
try {
|
|
196
|
-
// 从预设补全 baseUrl
|
|
184
|
+
// 从预设补全 baseUrl(前端填的 baseUrl 优先);models 全部留空,后续通过槽位配置动态选择
|
|
197
185
|
const preset = presets.find(p => p.id === presetId);
|
|
198
186
|
const resolvedBaseUrl = baseUrl || preset?.baseUrl || '';
|
|
199
|
-
const resolvedModels = preset?.defaultModels || { main: '', haiku: '', sonnet: '', opus: '' };
|
|
200
187
|
|
|
201
188
|
const body: Record<string, unknown> = {
|
|
202
189
|
presetId,
|
|
203
190
|
name,
|
|
204
191
|
apiKey,
|
|
205
192
|
baseUrl: resolvedBaseUrl,
|
|
206
|
-
models:
|
|
193
|
+
models: { main: '', haiku: '', sonnet: '', opus: '' },
|
|
207
194
|
...(preset?.apiFormat && { apiFormat: preset.apiFormat }),
|
|
208
195
|
};
|
|
209
196
|
if (extra && Object.keys(extra).length > 0) body.extra = extra;
|
|
@@ -224,13 +211,21 @@ export const ProviderPanel: React.FC<{
|
|
|
224
211
|
setErr(null); setOpMsg('测试中...');
|
|
225
212
|
try {
|
|
226
213
|
const res = await axios.post(`${base}/api/providers/${encodeURIComponent(id)}/test`);
|
|
227
|
-
const
|
|
228
|
-
|
|
214
|
+
const result = res?.data?.result;
|
|
215
|
+
const conn = result?.connectivity;
|
|
216
|
+
if (conn?.success) {
|
|
217
|
+
setOpMsg(`连通性正常 -> ${id} (${conn.latencyMs}ms)`);
|
|
218
|
+
setErr(null);
|
|
219
|
+
} else {
|
|
220
|
+
setErr(`连通性异常: ${conn?.error || '未知错误'}`);
|
|
221
|
+
setOpMsg(null);
|
|
222
|
+
}
|
|
229
223
|
} catch (e: any) {
|
|
230
224
|
setErr(e?.response?.data?.message || e?.message || `测试失败 -> ${id}`);
|
|
225
|
+
setOpMsg(null);
|
|
231
226
|
} finally {
|
|
232
|
-
setStage('list');
|
|
233
|
-
await loadProviders();
|
|
227
|
+
if (stage !== 'list') setStage('list');
|
|
228
|
+
await loadProviders({ keepError: true });
|
|
234
229
|
}
|
|
235
230
|
};
|
|
236
231
|
|
|
@@ -290,8 +285,6 @@ export const ProviderPanel: React.FC<{
|
|
|
290
285
|
</Text>
|
|
291
286
|
)}
|
|
292
287
|
{loading && <Text color="yellow">加载中...</Text>}
|
|
293
|
-
{err && <Text color="red">{err}</Text>}
|
|
294
|
-
{opMsg && <Text color="green">{opMsg}</Text>}
|
|
295
288
|
|
|
296
289
|
<Box marginTop={1} flexDirection="column">
|
|
297
290
|
<SelectInput
|
|
@@ -299,7 +292,6 @@ export const ProviderPanel: React.FC<{
|
|
|
299
292
|
{ label: '新增 Provider', value: 'add' },
|
|
300
293
|
{ label: '编辑 Provider(名称/API Key)', value: 'edit' },
|
|
301
294
|
{ label: '配置槽位(main/haiku/sonnet/opus)', value: 'slots' },
|
|
302
|
-
{ label: '切换当前 Provider', value: 'switch' },
|
|
303
295
|
{ label: '连通性测试', value: 'test' },
|
|
304
296
|
{ label: '删除 Provider', value: 'delete' },
|
|
305
297
|
{ label: '刷新', value: 'refresh' },
|
|
@@ -326,9 +318,6 @@ export const ProviderPanel: React.FC<{
|
|
|
326
318
|
.catch(() => {});
|
|
327
319
|
setStage('slot_config');
|
|
328
320
|
break;
|
|
329
|
-
case 'switch':
|
|
330
|
-
setStage('switch');
|
|
331
|
-
break;
|
|
332
321
|
case 'test':
|
|
333
322
|
setStage('test_select');
|
|
334
323
|
break;
|
|
@@ -344,6 +333,16 @@ export const ProviderPanel: React.FC<{
|
|
|
344
333
|
}
|
|
345
334
|
}}
|
|
346
335
|
/>
|
|
336
|
+
{err && (
|
|
337
|
+
<Box marginTop={1}>
|
|
338
|
+
<Text color="red">{err}</Text>
|
|
339
|
+
</Box>
|
|
340
|
+
)}
|
|
341
|
+
{opMsg && (
|
|
342
|
+
<Box marginTop={1}>
|
|
343
|
+
<Text color="green">{opMsg}</Text>
|
|
344
|
+
</Box>
|
|
345
|
+
)}
|
|
347
346
|
<Text dimColor>提示:ESC 返回。↑↓/回车 选择操作</Text>
|
|
348
347
|
</Box>
|
|
349
348
|
</Box>
|
|
@@ -458,24 +457,6 @@ export const ProviderPanel: React.FC<{
|
|
|
458
457
|
return <Text color="yellow">创建中...</Text>;
|
|
459
458
|
}
|
|
460
459
|
|
|
461
|
-
if (stage === 'switch') {
|
|
462
|
-
const items = providers.map(p => ({
|
|
463
|
-
label: `${p.name || p.id}${(currentId === p.id || p.isCurrent) ? ' ← 当前' : ''}`,
|
|
464
|
-
value: p.id
|
|
465
|
-
}));
|
|
466
|
-
return (
|
|
467
|
-
<Box flexDirection="column">
|
|
468
|
-
<Text>选择要设为当前的 Provider:</Text>
|
|
469
|
-
<SelectInput
|
|
470
|
-
items={items}
|
|
471
|
-
onSelect={it => doSetCurrent(it.value as string)}
|
|
472
|
-
/>
|
|
473
|
-
{err && <Text color="red">{err}</Text>}
|
|
474
|
-
<Text dimColor>ESC 返回</Text>
|
|
475
|
-
</Box>
|
|
476
|
-
);
|
|
477
|
-
}
|
|
478
|
-
|
|
479
460
|
if (stage === 'test_select') {
|
|
480
461
|
const items = providers.map(p => ({
|
|
481
462
|
label: `${p.name || p.id}`,
|
|
@@ -641,21 +622,34 @@ export const ProviderPanel: React.FC<{
|
|
|
641
622
|
items={[...items, { label: '← 返回主菜单', value: 'back' }]}
|
|
642
623
|
onSelect={it => {
|
|
643
624
|
if (it.value === 'back') { setStage('list'); setErr(null); return; }
|
|
644
|
-
|
|
625
|
+
const slotName = it.value as string;
|
|
626
|
+
setCurrentSlotName(slotName);
|
|
645
627
|
setErr(null);
|
|
646
|
-
setSlotLoadingMsg(
|
|
628
|
+
setSlotLoadingMsg(`正在拉取模型列表...`);
|
|
647
629
|
setStage('slot_loading');
|
|
630
|
+
// Fetch model lists from each provider's API endpoint
|
|
648
631
|
Promise.all(
|
|
649
632
|
providers.map(p =>
|
|
650
633
|
axios.get(`${base}/api/providers/${encodeURIComponent(p.id)}/models`)
|
|
651
|
-
.then(r =>
|
|
634
|
+
.then(r => {
|
|
635
|
+
const data = r.data;
|
|
636
|
+
const models = Array.isArray(data) ? data : (data?.models || []);
|
|
637
|
+
return { id: p.id, models: (models as string[]) || [] };
|
|
638
|
+
})
|
|
652
639
|
.catch(() => ({ id: p.id, models: [] as string[] }))
|
|
653
640
|
)
|
|
654
641
|
).then(results => {
|
|
655
642
|
const map: Record<string, string[]> = {};
|
|
656
643
|
results.forEach(r => { map[r.id] = r.models; });
|
|
657
644
|
setSlotProviderModels(map);
|
|
658
|
-
|
|
645
|
+
// Check if any models available
|
|
646
|
+
const hasAny = results.some(r => r.models.length > 0);
|
|
647
|
+
if (!hasAny) {
|
|
648
|
+
setErr('所有 Provider 均未返回可用模型,请检查 API Key 和网络连接');
|
|
649
|
+
setStage('slot_config');
|
|
650
|
+
} else {
|
|
651
|
+
setStage('slot_select_model');
|
|
652
|
+
}
|
|
659
653
|
});
|
|
660
654
|
}}
|
|
661
655
|
/>
|
|
@@ -665,7 +659,12 @@ export const ProviderPanel: React.FC<{
|
|
|
665
659
|
}
|
|
666
660
|
|
|
667
661
|
if (stage === 'slot_loading') {
|
|
668
|
-
return
|
|
662
|
+
return (
|
|
663
|
+
<Box flexDirection="column">
|
|
664
|
+
<Text color="yellow">{slotLoadingMsg || '正在拉取模型列表...'}</Text>
|
|
665
|
+
<Text dimColor>ESC 取消</Text>
|
|
666
|
+
</Box>
|
|
667
|
+
);
|
|
669
668
|
}
|
|
670
669
|
|
|
671
670
|
if (stage === 'slot_select_model') {
|
|
@@ -680,7 +679,7 @@ export const ProviderPanel: React.FC<{
|
|
|
680
679
|
if (items.length === 0) {
|
|
681
680
|
return (
|
|
682
681
|
<Box flexDirection="column">
|
|
683
|
-
<Text color="red">没有可用的模型(所有 Provider
|
|
682
|
+
<Text color="red">没有可用的模型(所有 Provider 均未返回模型,请检查 API Key 和网络)</Text>
|
|
684
683
|
<SelectInput
|
|
685
684
|
items={[{ label: '← 返回', value: 'back' }]}
|
|
686
685
|
onSelect={() => setStage('slot_config')}
|
|
@@ -229,6 +229,22 @@ export const call: LocalJSXCommandCall = async (onDone, context, args) => {
|
|
|
229
229
|
return null;
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
+
// Same-repo logs didn't find it — search across ALL projects.
|
|
233
|
+
// This handles the case where the user runs `bingo` from a different
|
|
234
|
+
// directory than where the session was originally created (the session
|
|
235
|
+
// file lives under ~/.claude/projects/{originalCwd}/). A direct UUID
|
|
236
|
+
// resume should always be location-independent.
|
|
237
|
+
const allLogs = await loadAllProjectsMessageLogs();
|
|
238
|
+
const allMatchingLogs = allLogs
|
|
239
|
+
.filter(l => getSessionIdFromLog(l) === maybeSessionId)
|
|
240
|
+
.sort((a, b) => b.modified.getTime() - a.modified.getTime());
|
|
241
|
+
if (allMatchingLogs.length > 0) {
|
|
242
|
+
const log = allMatchingLogs[0]!;
|
|
243
|
+
const fullLog = isLiteLog(log) ? await loadFullLog(log) : log;
|
|
244
|
+
void onResume(maybeSessionId, fullLog, 'slash_command_session_id');
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
|
|
232
248
|
// Enriched logs didn't find it — try direct file lookup. This handles
|
|
233
249
|
// sessions filtered out by enrichLogs (e.g., first message >16KB makes
|
|
234
250
|
// firstPrompt extraction fail, causing the session to be dropped).
|
|
@@ -70,7 +70,7 @@ export function IdeOnboardingDialog(t0) {
|
|
|
70
70
|
}
|
|
71
71
|
let t6;
|
|
72
72
|
if ($[8] !== ideName) {
|
|
73
|
-
t6 = <>{t5}<Text>Welcome to
|
|
73
|
+
t6 = <>{t5}<Text>Welcome to Bingo Code for {ideName}</Text></>;
|
|
74
74
|
$[8] = ideName;
|
|
75
75
|
$[9] = t6;
|
|
76
76
|
} else {
|
|
@@ -9,7 +9,7 @@ export function WelcomeV2() {
|
|
|
9
9
|
if (env.terminal === "Apple_Terminal") {
|
|
10
10
|
let t0;
|
|
11
11
|
if ($[0] !== theme) {
|
|
12
|
-
t0 = <AppleTerminalWelcomeV2 theme={theme} welcomeMessage="Welcome to
|
|
12
|
+
t0 = <AppleTerminalWelcomeV2 theme={theme} welcomeMessage="Welcome to Bingo Code" />;
|
|
13
13
|
$[0] = theme;
|
|
14
14
|
$[1] = t0;
|
|
15
15
|
} else {
|
|
@@ -28,7 +28,7 @@ export function WelcomeV2() {
|
|
|
28
28
|
let t7;
|
|
29
29
|
let t8;
|
|
30
30
|
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
|
31
|
-
t0 = <Text><Text color="claude">{"Welcome to
|
|
31
|
+
t0 = <Text><Text color="claude">{"Welcome to Bingo Code"} </Text><Text dimColor={true}>v{MACRO.VERSION} </Text></Text>;
|
|
32
32
|
t1 = <Text>{"\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026"}</Text>;
|
|
33
33
|
t2 = <Text>{" "}</Text>;
|
|
34
34
|
t3 = <Text>{" "}</Text>;
|
|
@@ -113,7 +113,7 @@ export function WelcomeV2() {
|
|
|
113
113
|
let t5;
|
|
114
114
|
let t6;
|
|
115
115
|
if ($[18] === Symbol.for("react.memo_cache_sentinel")) {
|
|
116
|
-
t0 = <Text><Text color="claude">{"Welcome to
|
|
116
|
+
t0 = <Text><Text color="claude">{"Welcome to Bingo Code"} </Text><Text dimColor={true}>v{MACRO.VERSION} </Text></Text>;
|
|
117
117
|
t1 = <Text>{"\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026"}</Text>;
|
|
118
118
|
t2 = <Text>{" "}</Text>;
|
|
119
119
|
t3 = <Text>{" * \u2588\u2588\u2588\u2588\u2588\u2593\u2593\u2591 "}</Text>;
|
|
@@ -1,22 +1,11 @@
|
|
|
1
1
|
// Provider presets — loaded from providers.yaml at startup
|
|
2
|
-
// Original work inspired by cc-switch (https://github.com/farion1231/cc-switch) by Jason Young, MIT License
|
|
3
2
|
|
|
4
|
-
//@C:ID=M.PP.providerPresets;K=M;V=2.0;P=Import dependencies;D=API;M=Providers;S=ModelConfiguration
|
|
5
3
|
import { readFileSync } from 'fs'
|
|
6
4
|
import { fileURLToPath } from 'url'
|
|
7
5
|
import { parse } from 'yaml'
|
|
8
6
|
import path from 'path'
|
|
9
7
|
import type { ApiFormat } from '../types/provider.js'
|
|
10
8
|
|
|
11
|
-
//@C:ID=T.PP.ModelMapping;K=T;V=1.0;P=Define model type mappings;D=API;M=Providers;S=ModelConfiguration
|
|
12
|
-
export type ModelMapping = {
|
|
13
|
-
main: string
|
|
14
|
-
haiku: string
|
|
15
|
-
sonnet: string
|
|
16
|
-
opus: string
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
//@C:ID=T.PP.ProviderField;K=T;V=1.0;P=Define per-provider field descriptor;D=API;M=Providers;S=ModelConfiguration
|
|
20
9
|
export type ProviderField = {
|
|
21
10
|
/** Field key: 'name' | 'apiKey' | 'baseUrl' map to top-level fields; others go into extra.<key> */
|
|
22
11
|
key: string
|
|
@@ -30,20 +19,34 @@ export type ProviderField = {
|
|
|
30
19
|
default?: string
|
|
31
20
|
}
|
|
32
21
|
|
|
33
|
-
//@C:ID=T.PP.ProviderPreset;K=T;V=2.0;P=Define provider preset structure;D=API;M=Providers;S=ModelConfiguration
|
|
34
22
|
export type ProviderPreset = {
|
|
35
23
|
id: string
|
|
36
24
|
name: string
|
|
25
|
+
/** Default base URL for this provider (can be overridden by user) */
|
|
37
26
|
baseUrl: string
|
|
38
27
|
apiFormat: ApiFormat
|
|
39
|
-
defaultModels: ModelMapping
|
|
40
28
|
needsApiKey: boolean
|
|
41
29
|
websiteUrl: string
|
|
30
|
+
/**
|
|
31
|
+
* Relative path to the models list endpoint, e.g. '/v1/models'.
|
|
32
|
+
* Empty string means dynamic model fetching is not supported.
|
|
33
|
+
*/
|
|
34
|
+
modelsUrl: string
|
|
35
|
+
/**
|
|
36
|
+
* Auth header style for the models list request:
|
|
37
|
+
* 'bearer' → Authorization: Bearer <apiKey>
|
|
38
|
+
* 'x-api-key' → x-api-key: <apiKey> (+ anthropic-version header)
|
|
39
|
+
*/
|
|
40
|
+
modelsAuthStyle: 'bearer' | 'x-api-key'
|
|
41
|
+
/**
|
|
42
|
+
* Field name in the response JSON that contains the model array.
|
|
43
|
+
* Almost always 'data' (OpenAI-compatible standard).
|
|
44
|
+
*/
|
|
45
|
+
modelsDataPath: string
|
|
42
46
|
/** Ordered list of fields to render when adding a new provider from this preset */
|
|
43
47
|
fields: ProviderField[]
|
|
44
48
|
}
|
|
45
49
|
|
|
46
|
-
//@C:ID=D.PP.PROVIDER_PRESETS;K=D;V=2.0;P=Load provider presets from yaml;D=API;M=Providers;S=ModelConfiguration
|
|
47
50
|
function loadPresetsFromYaml(): ProviderPreset[] {
|
|
48
51
|
try {
|
|
49
52
|
const yamlPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'providers.yaml')
|
|
@@ -52,20 +55,27 @@ function loadPresetsFromYaml(): ProviderPreset[] {
|
|
|
52
55
|
if (!Array.isArray(presets) || presets.length === 0) {
|
|
53
56
|
throw new Error('providers.yaml missing presets array')
|
|
54
57
|
}
|
|
55
|
-
// Ensure fields is always an array
|
|
56
|
-
return presets.map(p => ({
|
|
58
|
+
// Ensure fields is always an array and apply defaults for optional fields
|
|
59
|
+
return presets.map(p => ({
|
|
60
|
+
modelsUrl: '',
|
|
61
|
+
modelsAuthStyle: 'bearer' as const,
|
|
62
|
+
modelsDataPath: 'data',
|
|
63
|
+
...p,
|
|
64
|
+
fields: Array.isArray(p.fields) ? p.fields : [],
|
|
65
|
+
}))
|
|
57
66
|
} catch (err) {
|
|
58
67
|
console.error('[providerPresets] Failed to load providers.yaml, falling back to defaults:', err)
|
|
59
|
-
// Minimal fallback so the server can still start
|
|
60
68
|
return [
|
|
61
69
|
{
|
|
62
70
|
id: 'official',
|
|
63
71
|
name: 'Claude Official',
|
|
64
72
|
baseUrl: '',
|
|
65
73
|
apiFormat: 'anthropic',
|
|
66
|
-
defaultModels: { main: '', haiku: '', sonnet: '', opus: '' },
|
|
67
74
|
needsApiKey: false,
|
|
68
75
|
websiteUrl: 'https://www.anthropic.com/claude-code',
|
|
76
|
+
modelsUrl: '/v1/models',
|
|
77
|
+
modelsAuthStyle: 'x-api-key',
|
|
78
|
+
modelsDataPath: 'data',
|
|
69
79
|
fields: [{ key: 'name', label: 'Provider 昵称', required: true }],
|
|
70
80
|
},
|
|
71
81
|
{
|
|
@@ -73,9 +83,11 @@ function loadPresetsFromYaml(): ProviderPreset[] {
|
|
|
73
83
|
name: 'Custom',
|
|
74
84
|
baseUrl: '',
|
|
75
85
|
apiFormat: 'anthropic',
|
|
76
|
-
defaultModels: { main: '', haiku: '', sonnet: '', opus: '' },
|
|
77
86
|
needsApiKey: true,
|
|
78
87
|
websiteUrl: '',
|
|
88
|
+
modelsUrl: '/v1/models',
|
|
89
|
+
modelsAuthStyle: 'bearer',
|
|
90
|
+
modelsDataPath: 'data',
|
|
79
91
|
fields: [
|
|
80
92
|
{ key: 'name', label: 'Provider 昵称', required: true },
|
|
81
93
|
{ key: 'baseUrl', label: 'Base URL', required: true },
|
|
@@ -1,145 +1,207 @@
|
|
|
1
|
-
version: 2
|
|
2
|
-
|
|
3
|
-
# Provider 预设配置
|
|
4
|
-
# fields 数组声明新增时需填写的字段
|
|
5
|
-
# key: 'name' | 'apiKey' | 'baseUrl' 直接映射到顶层字段,其余存入 extra.<key>
|
|
6
|
-
# secret: true 时前端使用密码掩码显示
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
- key:
|
|
68
|
-
label:
|
|
69
|
-
required: true
|
|
70
|
-
secret:
|
|
71
|
-
placeholder: '
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
1
|
+
version: 2
|
|
2
|
+
|
|
3
|
+
# Provider 预设配置
|
|
4
|
+
# fields 数组声明新增时需填写的字段
|
|
5
|
+
# key: 'name' | 'apiKey' | 'baseUrl' 直接映射到顶层字段,其余存入 extra.<key>
|
|
6
|
+
# secret: true 时前端使用密码掩码显示
|
|
7
|
+
#
|
|
8
|
+
# modelsUrl: 相对于 baseUrl 的模型列表路径,空字符串表示不支持动态拉取
|
|
9
|
+
# modelsAuthStyle: bearer → Authorization: Bearer <apiKey>
|
|
10
|
+
# x-api-key → x-api-key: <apiKey> + anthropic-version header
|
|
11
|
+
# modelsDataPath: 响应 JSON 中模型数组的字段名(几乎总是 'data')
|
|
12
|
+
|
|
13
|
+
presets:
|
|
14
|
+
- id: official
|
|
15
|
+
name: Claude Official
|
|
16
|
+
baseUrl: ''
|
|
17
|
+
apiFormat: anthropic
|
|
18
|
+
needsApiKey: false
|
|
19
|
+
websiteUrl: https://www.anthropic.com/claude-code
|
|
20
|
+
modelsUrl: /v1/models
|
|
21
|
+
modelsAuthStyle: x-api-key
|
|
22
|
+
modelsDataPath: data
|
|
23
|
+
fields:
|
|
24
|
+
- key: name
|
|
25
|
+
label: Provider 昵称
|
|
26
|
+
required: true
|
|
27
|
+
secret: false
|
|
28
|
+
placeholder: 'e.g. Claude Official'
|
|
29
|
+
|
|
30
|
+
- id: openai
|
|
31
|
+
name: OpenAI
|
|
32
|
+
baseUrl: https://api.openai.com/v1
|
|
33
|
+
apiFormat: openai_chat
|
|
34
|
+
needsApiKey: true
|
|
35
|
+
websiteUrl: https://platform.openai.com
|
|
36
|
+
modelsUrl: /v1/models
|
|
37
|
+
modelsAuthStyle: bearer
|
|
38
|
+
modelsDataPath: data
|
|
39
|
+
fields:
|
|
40
|
+
- key: name
|
|
41
|
+
label: Provider 昵称
|
|
42
|
+
required: true
|
|
43
|
+
secret: false
|
|
44
|
+
placeholder: 'e.g. My OpenAI'
|
|
45
|
+
- key: apiKey
|
|
46
|
+
label: API Key
|
|
47
|
+
required: true
|
|
48
|
+
secret: true
|
|
49
|
+
placeholder: 'sk-...'
|
|
50
|
+
- key: baseUrl
|
|
51
|
+
label: Base URL (Optional)
|
|
52
|
+
required: false
|
|
53
|
+
secret: false
|
|
54
|
+
default: https://api.openai.com/v1
|
|
55
|
+
placeholder: 'https://api.openai.com/v1'
|
|
56
|
+
|
|
57
|
+
- id: gemini
|
|
58
|
+
name: Google Gemini
|
|
59
|
+
baseUrl: https://generativelanguage.googleapis.com/v1beta/openai
|
|
60
|
+
apiFormat: openai_chat
|
|
61
|
+
needsApiKey: true
|
|
62
|
+
websiteUrl: https://aistudio.google.com
|
|
63
|
+
modelsUrl: /v1/models
|
|
64
|
+
modelsAuthStyle: bearer
|
|
65
|
+
modelsDataPath: data
|
|
66
|
+
fields:
|
|
67
|
+
- key: name
|
|
68
|
+
label: Provider 昵称
|
|
69
|
+
required: true
|
|
70
|
+
secret: false
|
|
71
|
+
placeholder: 'e.g. My Gemini'
|
|
72
|
+
- key: apiKey
|
|
73
|
+
label: API Key
|
|
74
|
+
required: true
|
|
75
|
+
secret: true
|
|
76
|
+
placeholder: 'Gemini API Key'
|
|
77
|
+
|
|
78
|
+
- id: mistral
|
|
79
|
+
name: Mistral AI
|
|
80
|
+
baseUrl: https://api.mistral.ai/v1
|
|
81
|
+
apiFormat: openai_chat
|
|
82
|
+
needsApiKey: true
|
|
83
|
+
websiteUrl: https://console.mistral.ai
|
|
84
|
+
modelsUrl: /v1/models
|
|
85
|
+
modelsAuthStyle: bearer
|
|
86
|
+
modelsDataPath: data
|
|
87
|
+
fields:
|
|
88
|
+
- key: name
|
|
89
|
+
label: Provider 昵称
|
|
90
|
+
required: true
|
|
91
|
+
secret: false
|
|
92
|
+
placeholder: 'e.g. My Mistral'
|
|
93
|
+
- key: apiKey
|
|
94
|
+
label: API Key
|
|
95
|
+
required: true
|
|
96
|
+
secret: true
|
|
97
|
+
placeholder: 'Mistral API Key'
|
|
98
|
+
|
|
99
|
+
- id: deepseek
|
|
100
|
+
name: DeepSeek
|
|
101
|
+
baseUrl: https://api.deepseek.com/anthropic
|
|
102
|
+
apiFormat: anthropic
|
|
103
|
+
needsApiKey: true
|
|
104
|
+
websiteUrl: https://platform.deepseek.com
|
|
105
|
+
modelsUrl: /v1/models
|
|
106
|
+
modelsAuthStyle: bearer
|
|
107
|
+
modelsDataPath: data
|
|
108
|
+
fields:
|
|
109
|
+
- key: name
|
|
110
|
+
label: Provider 昵称
|
|
111
|
+
required: true
|
|
112
|
+
secret: false
|
|
113
|
+
placeholder: 'e.g. My DeepSeek'
|
|
114
|
+
- key: apiKey
|
|
115
|
+
label: API Key
|
|
116
|
+
required: true
|
|
117
|
+
secret: true
|
|
118
|
+
placeholder: 'sk-...'
|
|
119
|
+
|
|
120
|
+
- id: zhipuglm
|
|
121
|
+
name: Zhipu GLM
|
|
122
|
+
baseUrl: https://open.bigmodel.cn/api/anthropic
|
|
123
|
+
apiFormat: anthropic
|
|
124
|
+
needsApiKey: true
|
|
125
|
+
websiteUrl: https://open.bigmodel.cn
|
|
126
|
+
modelsUrl: /v1/models
|
|
127
|
+
modelsAuthStyle: bearer
|
|
128
|
+
modelsDataPath: data
|
|
129
|
+
fields:
|
|
130
|
+
- key: name
|
|
131
|
+
label: Provider 昵称
|
|
132
|
+
required: true
|
|
133
|
+
secret: false
|
|
134
|
+
placeholder: 'e.g. My GLM'
|
|
135
|
+
- key: apiKey
|
|
136
|
+
label: API Key
|
|
137
|
+
required: true
|
|
138
|
+
secret: true
|
|
139
|
+
placeholder: '智谱 API Key'
|
|
140
|
+
|
|
141
|
+
- id: kimi
|
|
142
|
+
name: Kimi
|
|
143
|
+
baseUrl: https://api.moonshot.cn/anthropic
|
|
144
|
+
apiFormat: anthropic
|
|
145
|
+
needsApiKey: true
|
|
146
|
+
websiteUrl: https://platform.moonshot.cn
|
|
147
|
+
modelsUrl: /v1/models
|
|
148
|
+
modelsAuthStyle: bearer
|
|
149
|
+
modelsDataPath: data
|
|
150
|
+
fields:
|
|
151
|
+
- key: name
|
|
152
|
+
label: Provider 昵称
|
|
153
|
+
required: true
|
|
154
|
+
secret: false
|
|
155
|
+
placeholder: 'e.g. My Kimi'
|
|
156
|
+
- key: apiKey
|
|
157
|
+
label: API Key
|
|
158
|
+
required: true
|
|
159
|
+
secret: true
|
|
160
|
+
placeholder: 'Moonshot API Key'
|
|
161
|
+
|
|
162
|
+
- id: minimax
|
|
163
|
+
name: MiniMax
|
|
164
|
+
baseUrl: https://api.minimaxi.com/anthropic
|
|
165
|
+
apiFormat: anthropic
|
|
166
|
+
needsApiKey: true
|
|
167
|
+
websiteUrl: https://platform.minimaxi.com
|
|
168
|
+
modelsUrl: /v1/models
|
|
169
|
+
modelsAuthStyle: bearer
|
|
170
|
+
modelsDataPath: data
|
|
171
|
+
fields:
|
|
172
|
+
- key: name
|
|
173
|
+
label: Provider 昵称
|
|
174
|
+
required: true
|
|
175
|
+
secret: false
|
|
176
|
+
placeholder: 'e.g. My MiniMax'
|
|
177
|
+
- key: apiKey
|
|
178
|
+
label: API Key
|
|
179
|
+
required: true
|
|
180
|
+
secret: true
|
|
181
|
+
placeholder: 'MiniMax API Key'
|
|
182
|
+
|
|
183
|
+
- id: custom
|
|
184
|
+
name: Custom
|
|
185
|
+
baseUrl: ''
|
|
186
|
+
apiFormat: openai_chat
|
|
187
|
+
needsApiKey: true
|
|
188
|
+
websiteUrl: ''
|
|
189
|
+
modelsUrl: /v1/models
|
|
190
|
+
modelsAuthStyle: bearer
|
|
191
|
+
modelsDataPath: data
|
|
192
|
+
fields:
|
|
193
|
+
- key: name
|
|
194
|
+
label: Provider 昵称
|
|
195
|
+
required: true
|
|
196
|
+
secret: false
|
|
197
|
+
placeholder: 'e.g. My Custom Provider'
|
|
198
|
+
- key: baseUrl
|
|
199
|
+
label: Base URL
|
|
200
|
+
required: true
|
|
201
|
+
secret: false
|
|
202
|
+
placeholder: 'https://your-api-endpoint.com/v1'
|
|
203
|
+
- key: apiKey
|
|
204
|
+
label: API Key
|
|
205
|
+
required: false
|
|
206
|
+
secret: true
|
|
207
|
+
placeholder: '(可选)API Key'
|
|
@@ -9,12 +9,14 @@
|
|
|
9
9
|
import * as fs from 'fs/promises'
|
|
10
10
|
import * as path from 'path'
|
|
11
11
|
import * as os from 'os'
|
|
12
|
+
import { getDirectFetchOptions } from '../../utils/proxy.ts'
|
|
12
13
|
import { ApiError } from '../middleware/errorHandler.js'
|
|
13
14
|
import { anthropicToOpenaiChat } from '../proxy/transform/anthropicToOpenaiChat.js'
|
|
14
15
|
import { anthropicToOpenaiResponses } from '../proxy/transform/anthropicToOpenaiResponses.js'
|
|
15
16
|
import { openaiChatToAnthropic } from '../proxy/transform/openaiChatToAnthropic.js'
|
|
16
17
|
import { openaiResponsesToAnthropic } from '../proxy/transform/openaiResponsesToAnthropic.js'
|
|
17
18
|
import type { AnthropicRequest, AnthropicResponse } from '../proxy/transform/types.js'
|
|
19
|
+
import { PROVIDER_PRESETS } from '../config/providerPresets.js'
|
|
18
20
|
import type {
|
|
19
21
|
SavedProvider,
|
|
20
22
|
ProvidersIndex,
|
|
@@ -379,29 +381,37 @@ export class ProviderService {
|
|
|
379
381
|
|
|
380
382
|
async fetchProviderModels(id: string): Promise<string[]> {
|
|
381
383
|
const provider = await this.getProvider(id)
|
|
384
|
+
const preset = PROVIDER_PRESETS.find(p => p.id === provider.presetId)
|
|
385
|
+
|
|
382
386
|
const base = provider.baseUrl.replace(/\/+$/, '')
|
|
383
387
|
if (!base) return []
|
|
384
388
|
|
|
385
|
-
const
|
|
389
|
+
const modelsUrl = preset?.modelsUrl || '/v1/models'
|
|
390
|
+
const url = `${base}${modelsUrl}`
|
|
391
|
+
|
|
386
392
|
const headers: Record<string, string> = {
|
|
387
393
|
'Content-Type': 'application/json',
|
|
388
|
-
Authorization: `Bearer ${provider.apiKey}`,
|
|
389
394
|
}
|
|
390
|
-
|
|
391
|
-
|
|
395
|
+
|
|
396
|
+
const authStyle = preset?.modelsAuthStyle || (provider.apiFormat === 'anthropic' ? 'x-api-key' : 'bearer')
|
|
397
|
+
if (authStyle === 'x-api-key') {
|
|
392
398
|
headers['x-api-key'] = provider.apiKey
|
|
393
|
-
delete headers['Authorization']
|
|
394
399
|
headers['anthropic-version'] = '2023-06-01'
|
|
400
|
+
} else {
|
|
401
|
+
headers['Authorization'] = `Bearer ${provider.apiKey}`
|
|
395
402
|
}
|
|
396
403
|
|
|
397
404
|
try {
|
|
398
|
-
const
|
|
405
|
+
const directOpts = getDirectFetchOptions()
|
|
406
|
+
const res = await fetch(url, { headers, signal: AbortSignal.timeout(10000), ...directOpts })
|
|
399
407
|
if (!res.ok) {
|
|
400
408
|
return []
|
|
401
409
|
}
|
|
402
|
-
const data = await res.json() as
|
|
403
|
-
const
|
|
404
|
-
|
|
410
|
+
const data = await res.json() as any
|
|
411
|
+
const dataPath = preset?.modelsDataPath || 'data'
|
|
412
|
+
const list = data[dataPath] ?? data.data ?? data.models ?? []
|
|
413
|
+
if (!Array.isArray(list)) return []
|
|
414
|
+
return list.map((m: any) => (typeof m === 'string' ? m : m.id)).filter(Boolean)
|
|
405
415
|
} catch {
|
|
406
416
|
return []
|
|
407
417
|
}
|
|
@@ -464,11 +474,13 @@ export class ProviderService {
|
|
|
464
474
|
const start = Date.now()
|
|
465
475
|
try {
|
|
466
476
|
const { url, headers, body } = buildDirectTestRequest(base, apiKey, modelId, format)
|
|
477
|
+
const directOpts = getDirectFetchOptions()
|
|
467
478
|
const response = await fetch(url, {
|
|
468
479
|
method: 'POST',
|
|
469
480
|
headers,
|
|
470
481
|
body: JSON.stringify(body),
|
|
471
482
|
signal: AbortSignal.timeout(30000),
|
|
483
|
+
...directOpts,
|
|
472
484
|
})
|
|
473
485
|
|
|
474
486
|
const latencyMs = Date.now() - start
|
|
@@ -526,11 +538,13 @@ export class ProviderService {
|
|
|
526
538
|
}
|
|
527
539
|
|
|
528
540
|
// Call upstream with transformed request
|
|
541
|
+
const directOpts = getDirectFetchOptions()
|
|
529
542
|
const response = await fetch(upstreamUrl, {
|
|
530
543
|
method: 'POST',
|
|
531
544
|
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
|
|
532
545
|
body: JSON.stringify(transformedBody),
|
|
533
546
|
signal: AbortSignal.timeout(30000),
|
|
547
|
+
...directOpts,
|
|
534
548
|
})
|
|
535
549
|
|
|
536
550
|
if (!response.ok) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Provider types — preset-based provider configuration.
|
|
3
3
|
*
|
|
4
|
-
* Providers are stored in ~/.claude/
|
|
5
|
-
* The active provider's env vars are written to ~/.claude/settings.json.
|
|
4
|
+
* Providers are stored in ~/.claude/bingo/providers.json as a lightweight index.
|
|
5
|
+
* The active provider's env vars are written to ~/.claude/bingo/settings.json.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { z } from 'zod'
|
|
@@ -44,7 +44,7 @@ export const CreateProviderSchema = z.object({
|
|
|
44
44
|
apiKey: z.string(),
|
|
45
45
|
baseUrl: z.string(),
|
|
46
46
|
apiFormat: ApiFormatSchema.default('anthropic'),
|
|
47
|
-
models: ModelMappingSchema,
|
|
47
|
+
models: ModelMappingSchema.default({ main: '', haiku: '', sonnet: '', opus: '' }).optional(),
|
|
48
48
|
notes: z.string().optional(),
|
|
49
49
|
extra: z.record(z.any()).optional(),
|
|
50
50
|
}).catchall(z.any())
|
package/src/utils/managedEnv.ts
CHANGED
|
@@ -93,15 +93,15 @@ function filterSettingsEnv(
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
/**
|
|
96
|
-
* Read env vars from ~/.claude/
|
|
96
|
+
* Read env vars from ~/.claude/bingo/settings.json (Bingo-specific provider
|
|
97
97
|
* config). This file is written by ProviderService.syncToSettings() and
|
|
98
98
|
* contains ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN, model defaults, etc.
|
|
99
99
|
* Returns an empty object if the file doesn't exist or is invalid.
|
|
100
100
|
*/
|
|
101
101
|
function getCcHahaSettingsEnv(): Record<string, string> {
|
|
102
102
|
try {
|
|
103
|
-
const
|
|
104
|
-
const raw = readFileSync(
|
|
103
|
+
const bingoSettings = join(getClaudeConfigHomeDir(), 'bingo', 'settings.json')
|
|
104
|
+
const raw = readFileSync(bingoSettings, 'utf-8')
|
|
105
105
|
const parsed = JSON.parse(raw) as { env?: Record<string, string> }
|
|
106
106
|
return parsed.env ?? {}
|
|
107
107
|
} catch {
|
package/src/utils/proxy.ts
CHANGED
|
@@ -318,6 +318,33 @@ export function getProxyFetchOptions(opts?: { forAnthropicAPI?: boolean }): {
|
|
|
318
318
|
return { ...base, ...getTLSFetchOptions() }
|
|
319
319
|
}
|
|
320
320
|
|
|
321
|
+
/**
|
|
322
|
+
* Get fetch options that bypass any system proxies to test direct connectivity.
|
|
323
|
+
*/
|
|
324
|
+
export function getDirectFetchOptions(): {
|
|
325
|
+
tls?: TLSConfig
|
|
326
|
+
dispatcher?: undici.Dispatcher
|
|
327
|
+
proxy?: undefined
|
|
328
|
+
keepalive?: false
|
|
329
|
+
} {
|
|
330
|
+
const base = keepAliveDisabled ? ({ keepalive: false } as const) : {}
|
|
331
|
+
|
|
332
|
+
if (typeof Bun !== 'undefined') {
|
|
333
|
+
// In Bun, explicit undefined for proxy bypasses the system proxy
|
|
334
|
+
return { ...base, proxy: undefined, ...getTLSFetchOptions() }
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// In Node.js/undici, a fresh Agent with no proxy settings bypasses system defaults
|
|
338
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
339
|
+
const undiciMod = require('undici') as typeof undici
|
|
340
|
+
const tlsOpts = getTLSFetchOptions()
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
...base,
|
|
344
|
+
dispatcher: new undiciMod.Agent(tlsOpts.dispatcher ? (tlsOpts.dispatcher as any).options : {}),
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
321
348
|
/**
|
|
322
349
|
* Configure global HTTP agents for both axios and undici
|
|
323
350
|
* This ensures all HTTP requests use the proxy and/or mTLS if configured
|