bingocode 1.0.14 → 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/LICENSE +29 -38
- package/package.json +3 -3
- package/src/cli/ProviderPanel.tsx +724 -725
- package/src/commands/resume/resume.tsx +16 -0
- package/src/server/config/providerPresets.ts +105 -93
- package/src/server/config/providers.yaml +138 -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
|
@@ -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,93 +1,105 @@
|
|
|
1
|
-
// Provider presets — loaded from providers.yaml at startup
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
fields: [
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
1
|
+
// Provider presets — loaded from providers.yaml at startup
|
|
2
|
+
|
|
3
|
+
import { readFileSync } from 'fs'
|
|
4
|
+
import { fileURLToPath } from 'url'
|
|
5
|
+
import { parse } from 'yaml'
|
|
6
|
+
import path from 'path'
|
|
7
|
+
import type { ApiFormat } from '../types/provider.js'
|
|
8
|
+
|
|
9
|
+
export type ProviderField = {
|
|
10
|
+
/** Field key: 'name' | 'apiKey' | 'baseUrl' map to top-level fields; others go into extra.<key> */
|
|
11
|
+
key: string
|
|
12
|
+
/** Human-readable label shown in the CLI form */
|
|
13
|
+
label: string
|
|
14
|
+
required?: boolean
|
|
15
|
+
/** If true, input is masked in the terminal */
|
|
16
|
+
secret?: boolean
|
|
17
|
+
placeholder?: string
|
|
18
|
+
/** Default value pre-filled in the form */
|
|
19
|
+
default?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type ProviderPreset = {
|
|
23
|
+
id: string
|
|
24
|
+
name: string
|
|
25
|
+
/** Default base URL for this provider (can be overridden by user) */
|
|
26
|
+
baseUrl: string
|
|
27
|
+
apiFormat: ApiFormat
|
|
28
|
+
needsApiKey: boolean
|
|
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
|
|
46
|
+
/** Ordered list of fields to render when adding a new provider from this preset */
|
|
47
|
+
fields: ProviderField[]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function loadPresetsFromYaml(): ProviderPreset[] {
|
|
51
|
+
try {
|
|
52
|
+
const yamlPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'providers.yaml')
|
|
53
|
+
const raw = parse(readFileSync(yamlPath, 'utf-8')) as { presets?: ProviderPreset[] }
|
|
54
|
+
const presets = raw?.presets
|
|
55
|
+
if (!Array.isArray(presets) || presets.length === 0) {
|
|
56
|
+
throw new Error('providers.yaml missing presets array')
|
|
57
|
+
}
|
|
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
|
+
}))
|
|
66
|
+
} catch (err) {
|
|
67
|
+
console.error('[providerPresets] Failed to load providers.yaml, falling back to defaults:', err)
|
|
68
|
+
return [
|
|
69
|
+
{
|
|
70
|
+
id: 'official',
|
|
71
|
+
name: 'Claude Official',
|
|
72
|
+
baseUrl: '',
|
|
73
|
+
apiFormat: 'anthropic',
|
|
74
|
+
needsApiKey: false,
|
|
75
|
+
websiteUrl: 'https://www.anthropic.com/claude-code',
|
|
76
|
+
modelsUrl: '/v1/models',
|
|
77
|
+
modelsAuthStyle: 'x-api-key',
|
|
78
|
+
modelsDataPath: 'data',
|
|
79
|
+
fields: [{ key: 'name', label: 'Provider 昵称', required: true }],
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: 'custom',
|
|
83
|
+
name: 'Custom',
|
|
84
|
+
baseUrl: '',
|
|
85
|
+
apiFormat: 'anthropic',
|
|
86
|
+
needsApiKey: true,
|
|
87
|
+
websiteUrl: '',
|
|
88
|
+
modelsUrl: '/v1/models',
|
|
89
|
+
modelsAuthStyle: 'bearer',
|
|
90
|
+
modelsDataPath: 'data',
|
|
91
|
+
fields: [
|
|
92
|
+
{ key: 'name', label: 'Provider 昵称', required: true },
|
|
93
|
+
{ key: 'baseUrl', label: 'Base URL', required: true },
|
|
94
|
+
{ key: 'apiKey', label: 'API Key', required: false, secret: true },
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
]
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export const PROVIDER_PRESETS: ProviderPreset[] = loadPresetsFromYaml()
|
|
102
|
+
|
|
103
|
+
export async function loadProviderPresets(): Promise<ProviderPreset[]> {
|
|
104
|
+
return PROVIDER_PRESETS
|
|
105
|
+
}
|
|
@@ -1,145 +1,138 @@
|
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
- key:
|
|
109
|
-
label:
|
|
110
|
-
required: true
|
|
111
|
-
secret:
|
|
112
|
-
placeholder: '
|
|
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
|
-
secret: false
|
|
140
|
-
placeholder: 'https://your-api-endpoint.com/anthropic'
|
|
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: deepseek
|
|
31
|
+
name: DeepSeek
|
|
32
|
+
baseUrl: https://api.deepseek.com/anthropic
|
|
33
|
+
apiFormat: anthropic
|
|
34
|
+
needsApiKey: true
|
|
35
|
+
websiteUrl: https://platform.deepseek.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 DeepSeek'
|
|
45
|
+
- key: apiKey
|
|
46
|
+
label: API Key
|
|
47
|
+
required: true
|
|
48
|
+
secret: true
|
|
49
|
+
placeholder: 'sk-...'
|
|
50
|
+
|
|
51
|
+
- id: zhipuglm
|
|
52
|
+
name: Zhipu GLM
|
|
53
|
+
baseUrl: https://open.bigmodel.cn/api/anthropic
|
|
54
|
+
apiFormat: anthropic
|
|
55
|
+
needsApiKey: true
|
|
56
|
+
websiteUrl: https://open.bigmodel.cn
|
|
57
|
+
modelsUrl: /v1/models
|
|
58
|
+
modelsAuthStyle: bearer
|
|
59
|
+
modelsDataPath: data
|
|
60
|
+
fields:
|
|
61
|
+
- key: name
|
|
62
|
+
label: Provider 昵称
|
|
63
|
+
required: true
|
|
64
|
+
secret: false
|
|
65
|
+
placeholder: 'e.g. My GLM'
|
|
66
|
+
- key: apiKey
|
|
67
|
+
label: API Key
|
|
68
|
+
required: true
|
|
69
|
+
secret: true
|
|
70
|
+
placeholder: '智谱 API Key'
|
|
71
|
+
|
|
72
|
+
- id: kimi
|
|
73
|
+
name: Kimi
|
|
74
|
+
baseUrl: https://api.moonshot.cn/anthropic
|
|
75
|
+
apiFormat: anthropic
|
|
76
|
+
needsApiKey: true
|
|
77
|
+
websiteUrl: https://platform.moonshot.cn
|
|
78
|
+
modelsUrl: /v1/models
|
|
79
|
+
modelsAuthStyle: bearer
|
|
80
|
+
modelsDataPath: data
|
|
81
|
+
fields:
|
|
82
|
+
- key: name
|
|
83
|
+
label: Provider 昵称
|
|
84
|
+
required: true
|
|
85
|
+
secret: false
|
|
86
|
+
placeholder: 'e.g. My Kimi'
|
|
87
|
+
- key: apiKey
|
|
88
|
+
label: API Key
|
|
89
|
+
required: true
|
|
90
|
+
secret: true
|
|
91
|
+
placeholder: 'Moonshot API Key'
|
|
92
|
+
|
|
93
|
+
- id: minimax
|
|
94
|
+
name: MiniMax
|
|
95
|
+
baseUrl: https://api.minimaxi.com/anthropic
|
|
96
|
+
apiFormat: anthropic
|
|
97
|
+
needsApiKey: true
|
|
98
|
+
websiteUrl: https://platform.minimaxi.com
|
|
99
|
+
modelsUrl: /v1/models
|
|
100
|
+
modelsAuthStyle: bearer
|
|
101
|
+
modelsDataPath: data
|
|
102
|
+
fields:
|
|
103
|
+
- key: name
|
|
104
|
+
label: Provider 昵称
|
|
105
|
+
required: true
|
|
106
|
+
secret: false
|
|
107
|
+
placeholder: 'e.g. My MiniMax'
|
|
108
|
+
- key: apiKey
|
|
109
|
+
label: API Key
|
|
110
|
+
required: true
|
|
111
|
+
secret: true
|
|
112
|
+
placeholder: 'MiniMax API Key'
|
|
113
|
+
|
|
114
|
+
- id: custom
|
|
115
|
+
name: Custom
|
|
116
|
+
baseUrl: ''
|
|
117
|
+
apiFormat: openai_chat
|
|
118
|
+
needsApiKey: true
|
|
119
|
+
websiteUrl: ''
|
|
120
|
+
modelsUrl: /v1/models
|
|
121
|
+
modelsAuthStyle: bearer
|
|
122
|
+
modelsDataPath: data
|
|
123
|
+
fields:
|
|
124
|
+
- key: name
|
|
125
|
+
label: Provider 昵称
|
|
126
|
+
required: true
|
|
127
|
+
secret: false
|
|
128
|
+
placeholder: 'e.g. My Custom Provider'
|
|
129
|
+
- key: baseUrl
|
|
130
|
+
label: Base URL
|
|
131
|
+
required: true
|
|
132
|
+
secret: false
|
|
133
|
+
placeholder: 'https://your-api-endpoint.com/v1'
|
|
134
|
+
- key: apiKey
|
|
135
|
+
label: API Key
|
|
136
|
+
required: false
|
|
137
|
+
secret: true
|
|
138
|
+
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 {
|