bingocode 1.0.15 → 1.0.16

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bingocode",
3
- "version": "1.0.15",
3
+ "version": "1.0.16",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "claude": "bin/claude-win.cjs",
@@ -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); setErr(null);
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 models(前端填的 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: resolvedModels,
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 ok = res?.data?.ok ?? true;
228
- setOpMsg(ok ? `连通性正常 -> ${id}` : `连通性异常 -> ${id}`);
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
- setCurrentSlotName(it.value as string);
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 => ({ id: p.id, models: (r.data as { models: string[] }).models }))
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
- setStage('slot_select_model');
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 <Text color="yellow">{slotLoadingMsg}</Text>;
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 连接失败或未返回模型列表)</Text>
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).
@@ -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 => ({ ...p, fields: Array.isArray(p.fields) ? p.fields : [] }))
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 },
@@ -4,6 +4,11 @@ version: 2
4
4
  # fields 数组声明新增时需填写的字段
5
5
  # key: 'name' | 'apiKey' | 'baseUrl' 直接映射到顶层字段,其余存入 extra.<key>
6
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')
7
12
 
8
13
  presets:
9
14
  - id: official
@@ -12,11 +17,9 @@ presets:
12
17
  apiFormat: anthropic
13
18
  needsApiKey: false
14
19
  websiteUrl: https://www.anthropic.com/claude-code
15
- defaultModels:
16
- main: ''
17
- haiku: ''
18
- sonnet: ''
19
- opus: ''
20
+ modelsUrl: /v1/models
21
+ modelsAuthStyle: x-api-key
22
+ modelsDataPath: data
20
23
  fields:
21
24
  - key: name
22
25
  label: Provider 昵称
@@ -30,11 +33,9 @@ presets:
30
33
  apiFormat: anthropic
31
34
  needsApiKey: true
32
35
  websiteUrl: https://platform.deepseek.com
33
- defaultModels:
34
- main: DeepSeek-V3.2
35
- haiku: DeepSeek-V3.2
36
- sonnet: DeepSeek-V3.2
37
- opus: DeepSeek-V3.2
36
+ modelsUrl: /v1/models
37
+ modelsAuthStyle: bearer
38
+ modelsDataPath: data
38
39
  fields:
39
40
  - key: name
40
41
  label: Provider 昵称
@@ -53,11 +54,9 @@ presets:
53
54
  apiFormat: anthropic
54
55
  needsApiKey: true
55
56
  websiteUrl: https://open.bigmodel.cn
56
- defaultModels:
57
- main: glm-5
58
- haiku: glm-5
59
- sonnet: glm-5
60
- opus: glm-5
57
+ modelsUrl: /v1/models
58
+ modelsAuthStyle: bearer
59
+ modelsDataPath: data
61
60
  fields:
62
61
  - key: name
63
62
  label: Provider 昵称
@@ -76,11 +75,9 @@ presets:
76
75
  apiFormat: anthropic
77
76
  needsApiKey: true
78
77
  websiteUrl: https://platform.moonshot.cn
79
- defaultModels:
80
- main: kimi-k2.5
81
- haiku: kimi-k2.5
82
- sonnet: kimi-k2.5
83
- opus: kimi-k2.5
78
+ modelsUrl: /v1/models
79
+ modelsAuthStyle: bearer
80
+ modelsDataPath: data
84
81
  fields:
85
82
  - key: name
86
83
  label: Provider 昵称
@@ -99,11 +96,9 @@ presets:
99
96
  apiFormat: anthropic
100
97
  needsApiKey: true
101
98
  websiteUrl: https://platform.minimaxi.com
102
- defaultModels:
103
- main: MiniMax-M2.7
104
- haiku: MiniMax-M2.7
105
- sonnet: MiniMax-M2.7
106
- opus: MiniMax-M2.7
99
+ modelsUrl: /v1/models
100
+ modelsAuthStyle: bearer
101
+ modelsDataPath: data
107
102
  fields:
108
103
  - key: name
109
104
  label: Provider 昵称
@@ -122,11 +117,9 @@ presets:
122
117
  apiFormat: openai_chat
123
118
  needsApiKey: true
124
119
  websiteUrl: ''
125
- defaultModels:
126
- main: ''
127
- haiku: ''
128
- sonnet: ''
129
- opus: ''
120
+ modelsUrl: /v1/models
121
+ modelsAuthStyle: bearer
122
+ modelsDataPath: data
130
123
  fields:
131
124
  - key: name
132
125
  label: Provider 昵称
@@ -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 url = `${base}/v1/models`
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
- // anthropic format uses x-api-key header style
391
- if (provider.apiFormat === 'anthropic') {
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 res = await fetch(url, { headers, signal: AbortSignal.timeout(10000) })
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 { data?: { id: string }[]; models?: { id: string }[] }
403
- const list = data.data ?? data.models ?? []
404
- return list.map((m: { id: string }) => m.id).filter(Boolean)
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/cc-haha/providers.json as a lightweight index.
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())
@@ -93,15 +93,15 @@ function filterSettingsEnv(
93
93
  }
94
94
 
95
95
  /**
96
- * Read env vars from ~/.claude/cc-haha/settings.json (Haha-specific provider
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 ccHahaSettings = join(getClaudeConfigHomeDir(), 'cc-haha', 'settings.json')
104
- const raw = readFileSync(ccHahaSettings, 'utf-8')
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 {
@@ -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