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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bingocode",
3
- "version": "1.0.15",
3
+ "version": "1.0.17",
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).
@@ -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 Claude Code for {ideName}</Text></>;
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 Claude Code" />;
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 Claude Code"} </Text><Text dimColor={true}>v{MACRO.VERSION} </Text></Text>;
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 Claude Code"} </Text><Text dimColor={true}>v{MACRO.VERSION} </Text></Text>;
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 => ({ ...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 },
@@ -1,145 +1,207 @@
1
- version: 2
2
-
3
- # Provider 预设配置
4
- # fields 数组声明新增时需填写的字段
5
- # key: 'name' | 'apiKey' | 'baseUrl' 直接映射到顶层字段,其余存入 extra.<key>
6
- # secret: true 时前端使用密码掩码显示
7
-
8
- presets:
9
- - id: official
10
- name: Claude Official
11
- baseUrl: ''
12
- apiFormat: anthropic
13
- needsApiKey: false
14
- websiteUrl: https://www.anthropic.com/claude-code
15
- defaultModels:
16
- main: ''
17
- haiku: ''
18
- sonnet: ''
19
- opus: ''
20
- fields:
21
- - key: name
22
- label: Provider 昵称
23
- required: true
24
- secret: false
25
- placeholder: 'e.g. Claude Official'
26
-
27
- - id: deepseek
28
- name: DeepSeek
29
- baseUrl: https://api.deepseek.com/anthropic
30
- apiFormat: anthropic
31
- needsApiKey: true
32
- 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
38
- fields:
39
- - key: name
40
- label: Provider 昵称
41
- required: true
42
- secret: false
43
- placeholder: 'e.g. My DeepSeek'
44
- - key: apiKey
45
- label: API Key
46
- required: true
47
- secret: true
48
- placeholder: 'sk-...'
49
-
50
- - id: zhipuglm
51
- name: Zhipu GLM
52
- baseUrl: https://open.bigmodel.cn/api/anthropic
53
- apiFormat: anthropic
54
- needsApiKey: true
55
- websiteUrl: https://open.bigmodel.cn
56
- defaultModels:
57
- main: glm-5
58
- haiku: glm-5
59
- sonnet: glm-5
60
- opus: glm-5
61
- fields:
62
- - key: name
63
- label: Provider 昵称
64
- required: true
65
- secret: false
66
- placeholder: 'e.g. My GLM'
67
- - key: apiKey
68
- label: API Key
69
- required: true
70
- secret: true
71
- placeholder: '智谱 API Key'
72
-
73
- - id: kimi
74
- name: Kimi
75
- baseUrl: https://api.moonshot.cn/anthropic
76
- apiFormat: anthropic
77
- needsApiKey: true
78
- 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
84
- fields:
85
- - key: name
86
- label: Provider 昵称
87
- required: true
88
- secret: false
89
- placeholder: 'e.g. My Kimi'
90
- - key: apiKey
91
- label: API Key
92
- required: true
93
- secret: true
94
- placeholder: 'Moonshot API Key'
95
-
96
- - id: minimax
97
- name: MiniMax
98
- baseUrl: https://api.minimaxi.com/anthropic
99
- apiFormat: anthropic
100
- needsApiKey: true
101
- 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
107
- fields:
108
- - key: name
109
- label: Provider 昵称
110
- required: true
111
- secret: false
112
- placeholder: 'e.g. My MiniMax'
113
- - key: apiKey
114
- label: API Key
115
- required: true
116
- secret: true
117
- placeholder: 'MiniMax API Key'
118
-
119
- - id: custom
120
- name: Custom
121
- baseUrl: ''
122
- apiFormat: openai_chat
123
- needsApiKey: true
124
- websiteUrl: ''
125
- defaultModels:
126
- main: ''
127
- haiku: ''
128
- sonnet: ''
129
- opus: ''
130
- fields:
131
- - key: name
132
- label: Provider 昵称
133
- required: true
134
- secret: false
135
- placeholder: 'e.g. My Custom Provider'
136
- - key: baseUrl
137
- label: Base URL
138
- required: true
139
- secret: false
140
- placeholder: 'https://your-api-endpoint.com/v1'
141
- - key: apiKey
142
- label: API Key
143
- required: false
144
- secret: true
145
- placeholder: '(可选)API Key'
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 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