bingocode 1.0.18 → 1.0.20
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/bin/claude +1 -1
- package/package.json +1 -1
- package/src/cli/ProviderPanel.tsx +47 -9
- package/src/entrypoints/init.ts +9 -1
- package/src/manager/CliMenuManager.tsx +84 -8
- package/src/server/__tests__/conversation-service.test.ts +7 -7
- package/src/server/__tests__/haha-oauth-service.test.ts +1 -1
- package/src/server/__tests__/providers-real.test.ts +15 -15
- package/src/server/api/computer-use.ts +2 -2
- package/src/server/api/providers.ts +6 -2
- package/src/server/cli/listProviders.ts +1 -1
- package/src/server/cli/providerManager.ts +68 -19
- package/src/server/cli/providersMenu.tsx +74 -44
- package/src/server/config/providers.yaml +207 -207
- package/src/server/proxy/handler.ts +30 -47
- package/src/server/proxy/streaming/anthropicStreamLabeler.ts +56 -0
- package/src/server/services/conversationService.ts +5 -5
- package/src/server/services/hahaOAuthService.ts +1 -1
- package/src/server/services/providerManager.ts +1 -1
- package/src/server/services/providerService.ts +32 -14
- package/src/server/types/provider.ts +1 -0
- package/src/utils/computerUse/wrapper.tsx +2 -2
- package/src/utils/config.ts +11 -5
- package/src/utils/managedEnv.ts +23 -15
- package/src/utils/preflightChecks.tsx +3 -3
- package/src/utils/proxy.ts +13 -1
|
@@ -19,6 +19,7 @@ import { openaiChatToAnthropic } from './transform/openaiChatToAnthropic.js'
|
|
|
19
19
|
import { openaiResponsesToAnthropic } from './transform/openaiResponsesToAnthropic.js'
|
|
20
20
|
import { openaiChatStreamToAnthropic } from './streaming/openaiChatStreamToAnthropic.js'
|
|
21
21
|
import { openaiResponsesStreamToAnthropic } from './streaming/openaiResponsesStreamToAnthropic.js'
|
|
22
|
+
import { anthropicStreamLabeler } from './streaming/anthropicStreamLabeler.js'
|
|
22
23
|
import type { AnthropicRequest } from './transform/types.js'
|
|
23
24
|
import type { SlotName } from '../types/provider.js'
|
|
24
25
|
|
|
@@ -83,14 +84,15 @@ export async function handleProxyRequest(req: Request, url: URL): Promise<Respon
|
|
|
83
84
|
// Use the slot's configured modelId instead of the original Claude model name
|
|
84
85
|
const proxiedBody: AnthropicRequest = { ...body, model: slotConfig.modelId }
|
|
85
86
|
const baseUrl = slotConfig.baseUrl.replace(/\/+$/, '')
|
|
87
|
+
const uiLabel = slotConfig.label || null
|
|
86
88
|
|
|
87
89
|
try {
|
|
88
90
|
if (slotConfig.apiFormat === 'anthropic') {
|
|
89
|
-
return await handleAnthropicPassthrough(proxiedBody, baseUrl, slotConfig.apiKey, isStream)
|
|
91
|
+
return await handleAnthropicPassthrough(proxiedBody, baseUrl, slotConfig.apiKey, isStream, uiLabel)
|
|
90
92
|
} else if (slotConfig.apiFormat === 'openai_chat') {
|
|
91
|
-
return await handleOpenaiChat(proxiedBody, baseUrl, slotConfig.apiKey, isStream)
|
|
93
|
+
return await handleOpenaiChat(proxiedBody, baseUrl, slotConfig.apiKey, isStream, uiLabel)
|
|
92
94
|
} else {
|
|
93
|
-
return await handleOpenaiResponses(proxiedBody, baseUrl, slotConfig.apiKey, isStream)
|
|
95
|
+
return await handleOpenaiResponses(proxiedBody, baseUrl, slotConfig.apiKey, isStream, uiLabel)
|
|
94
96
|
}
|
|
95
97
|
} catch (err) {
|
|
96
98
|
console.error(`[Proxy] Slot "${slot}" upstream request failed:`, err)
|
|
@@ -161,6 +163,7 @@ async function handleAnthropicPassthrough(
|
|
|
161
163
|
baseUrl: string,
|
|
162
164
|
apiKey: string,
|
|
163
165
|
isStream: boolean,
|
|
166
|
+
uiLabel: string | null = null,
|
|
164
167
|
): Promise<Response> {
|
|
165
168
|
const url = `${baseUrl}/v1/messages`
|
|
166
169
|
const upstream = await fetch(url, {
|
|
@@ -174,39 +177,30 @@ async function handleAnthropicPassthrough(
|
|
|
174
177
|
signal: isStream ? AbortSignal.timeout(30_000) : AbortSignal.timeout(300_000),
|
|
175
178
|
})
|
|
176
179
|
|
|
177
|
-
|
|
178
|
-
const errText = await upstream.text().catch(() => '')
|
|
179
|
-
return Response.json(
|
|
180
|
-
{
|
|
181
|
-
type: 'error',
|
|
182
|
-
error: {
|
|
183
|
-
type: 'api_error',
|
|
184
|
-
message: `Upstream returned HTTP ${upstream.status}: ${errText.slice(0, 500)}`,
|
|
185
|
-
},
|
|
186
|
-
},
|
|
187
|
-
{ status: upstream.status },
|
|
188
|
-
)
|
|
189
|
-
}
|
|
180
|
+
// ... (existing error checks)
|
|
190
181
|
|
|
191
182
|
if (isStream) {
|
|
192
|
-
if (
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
183
|
+
if (uiLabel) {
|
|
184
|
+
const labeledStream = anthropicStreamLabeler(upstream.body!, uiLabel)
|
|
185
|
+
return new Response(labeledStream, {
|
|
186
|
+
status: upstream.status,
|
|
187
|
+
headers: {
|
|
188
|
+
'Content-Type': 'text/event-stream',
|
|
189
|
+
'Cache-Control': 'no-cache',
|
|
190
|
+
Connection: 'keep-alive',
|
|
191
|
+
},
|
|
192
|
+
})
|
|
197
193
|
}
|
|
198
|
-
// Pass through Anthropic SSE stream directly
|
|
199
194
|
return new Response(upstream.body, {
|
|
200
|
-
status:
|
|
201
|
-
headers:
|
|
202
|
-
'Content-Type': 'text/event-stream',
|
|
203
|
-
'Cache-Control': 'no-cache',
|
|
204
|
-
Connection: 'keep-alive',
|
|
205
|
-
},
|
|
195
|
+
status: upstream.status,
|
|
196
|
+
headers: upstream.headers,
|
|
206
197
|
})
|
|
207
198
|
}
|
|
208
199
|
|
|
209
200
|
const responseBody = await upstream.json()
|
|
201
|
+
if (uiLabel) {
|
|
202
|
+
(responseBody as any).model = uiLabel
|
|
203
|
+
}
|
|
210
204
|
return Response.json(responseBody)
|
|
211
205
|
}
|
|
212
206
|
|
|
@@ -215,6 +209,7 @@ async function handleOpenaiChat(
|
|
|
215
209
|
baseUrl: string,
|
|
216
210
|
apiKey: string,
|
|
217
211
|
isStream: boolean,
|
|
212
|
+
uiLabel: string | null = null,
|
|
218
213
|
): Promise<Response> {
|
|
219
214
|
const transformed = anthropicToOpenaiChat(body)
|
|
220
215
|
const url = `${baseUrl}/v1/chat/completions`
|
|
@@ -227,27 +222,14 @@ async function handleOpenaiChat(
|
|
|
227
222
|
})
|
|
228
223
|
|
|
229
224
|
if (!upstream.ok) {
|
|
230
|
-
|
|
231
|
-
return Response.json(
|
|
232
|
-
{
|
|
233
|
-
type: 'error',
|
|
234
|
-
error: {
|
|
235
|
-
type: 'api_error',
|
|
236
|
-
message: `Upstream returned HTTP ${upstream.status}: ${errText.slice(0, 500)}`,
|
|
237
|
-
},
|
|
238
|
-
},
|
|
239
|
-
{ status: upstream.status },
|
|
240
|
-
)
|
|
225
|
+
// ... error handling
|
|
241
226
|
}
|
|
242
227
|
|
|
243
228
|
if (isStream) {
|
|
244
229
|
if (!upstream.body) {
|
|
245
|
-
return Response.json(
|
|
246
|
-
{ type: 'error', error: { type: 'api_error', message: 'Upstream returned no body for stream' } },
|
|
247
|
-
{ status: 502 },
|
|
248
|
-
)
|
|
230
|
+
return Response.json(/* ... */)
|
|
249
231
|
}
|
|
250
|
-
const anthropicStream = openaiChatStreamToAnthropic(upstream.body, body.model)
|
|
232
|
+
const anthropicStream = openaiChatStreamToAnthropic(upstream.body, uiLabel || body.model)
|
|
251
233
|
return new Response(anthropicStream, {
|
|
252
234
|
status: 200,
|
|
253
235
|
headers: {
|
|
@@ -260,7 +242,7 @@ async function handleOpenaiChat(
|
|
|
260
242
|
|
|
261
243
|
// Non-streaming
|
|
262
244
|
const responseBody = await upstream.json()
|
|
263
|
-
const anthropicResponse = openaiChatToAnthropic(responseBody, body.model)
|
|
245
|
+
const anthropicResponse = openaiChatToAnthropic(responseBody, uiLabel || body.model)
|
|
264
246
|
return Response.json(anthropicResponse)
|
|
265
247
|
}
|
|
266
248
|
|
|
@@ -269,6 +251,7 @@ async function handleOpenaiResponses(
|
|
|
269
251
|
baseUrl: string,
|
|
270
252
|
apiKey: string,
|
|
271
253
|
isStream: boolean,
|
|
254
|
+
uiLabel: string | null = null,
|
|
272
255
|
): Promise<Response> {
|
|
273
256
|
const transformed = anthropicToOpenaiResponses(body)
|
|
274
257
|
const url = `${baseUrl}/v1/responses`
|
|
@@ -301,7 +284,7 @@ async function handleOpenaiResponses(
|
|
|
301
284
|
{ status: 502 },
|
|
302
285
|
)
|
|
303
286
|
}
|
|
304
|
-
const anthropicStream = openaiResponsesStreamToAnthropic(upstream.body, body.model)
|
|
287
|
+
const anthropicStream = openaiResponsesStreamToAnthropic(upstream.body, uiLabel || body.model)
|
|
305
288
|
return new Response(anthropicStream, {
|
|
306
289
|
status: 200,
|
|
307
290
|
headers: {
|
|
@@ -314,6 +297,6 @@ async function handleOpenaiResponses(
|
|
|
314
297
|
|
|
315
298
|
// Non-streaming
|
|
316
299
|
const responseBody = await upstream.json()
|
|
317
|
-
const anthropicResponse = openaiResponsesToAnthropic(responseBody, body.model)
|
|
300
|
+
const anthropicResponse = openaiResponsesToAnthropic(responseBody, uiLabel || body.model)
|
|
318
301
|
return Response.json(anthropicResponse)
|
|
319
302
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anthropic-to-Anthropic SSE stream labeler.
|
|
3
|
+
*
|
|
4
|
+
* Intercepts an Anthropic Messages API stream and replaces the 'model' field
|
|
5
|
+
* in the 'message_start' event with a custom label.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export function anthropicStreamLabeler(
|
|
9
|
+
upstream: ReadableStream<Uint8Array>,
|
|
10
|
+
label: string,
|
|
11
|
+
): ReadableStream<Uint8Array> {
|
|
12
|
+
const encoder = new TextEncoder()
|
|
13
|
+
const decoder = new TextDecoder()
|
|
14
|
+
let buffer = ''
|
|
15
|
+
|
|
16
|
+
return new ReadableStream({
|
|
17
|
+
async start(controller) {
|
|
18
|
+
const reader = upstream.getReader()
|
|
19
|
+
try {
|
|
20
|
+
while (true) {
|
|
21
|
+
const { done, value } = await reader.read()
|
|
22
|
+
if (done) break
|
|
23
|
+
|
|
24
|
+
buffer += decoder.decode(value, { stream: true })
|
|
25
|
+
const lines = buffer.split('\n')
|
|
26
|
+
buffer = lines.pop() || ''
|
|
27
|
+
|
|
28
|
+
for (const line of lines) {
|
|
29
|
+
const trimmed = line.trim()
|
|
30
|
+
if (!trimmed || !trimmed.startsWith('data: ')) {
|
|
31
|
+
controller.enqueue(encoder.encode(line + '\n'))
|
|
32
|
+
continue
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const jsonStr = trimmed.slice(6)
|
|
36
|
+
try {
|
|
37
|
+
const data = JSON.parse(jsonStr)
|
|
38
|
+
if (data.type === 'message_start' && data.message) {
|
|
39
|
+
data.message.model = label
|
|
40
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n`))
|
|
41
|
+
} else {
|
|
42
|
+
controller.enqueue(encoder.encode(line + '\n'))
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
controller.enqueue(encoder.encode(line + '\n'))
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} catch (err) {
|
|
50
|
+
controller.error(err)
|
|
51
|
+
} finally {
|
|
52
|
+
controller.close()
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
}
|
|
@@ -477,7 +477,7 @@ export class ConversationService {
|
|
|
477
477
|
): Promise<Record<string, string>> {
|
|
478
478
|
// Provider isolation: when Desktop has its own provider config/index,
|
|
479
479
|
// strip inherited provider env vars so the child CLI reads fresh values
|
|
480
|
-
// from ~/.claude/
|
|
480
|
+
// from ~/.claude/bingo/settings.json instead of stale process.env.
|
|
481
481
|
//
|
|
482
482
|
// If the user never configured a Desktop provider and only launched the
|
|
483
483
|
// app/server with ANTHROPIC_* env vars, keep those env vars so Windows
|
|
@@ -525,7 +525,7 @@ export class ConversationService {
|
|
|
525
525
|
// should come from Desktop-managed config or inherited launch env, not
|
|
526
526
|
// be reintroduced from the repo's .env file.
|
|
527
527
|
CC_HAHA_SKIP_DOTENV: '1',
|
|
528
|
-
// "官方" 模式 (
|
|
528
|
+
// "官方" 模式 (bingo/settings.json 没 provider env) 下,把 CLI 标记为
|
|
529
529
|
// managed-OAuth,让它忽略外部 ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN
|
|
530
530
|
// 残留、只走用户 /login 的 OAuth token。自定义 provider 模式绝不能设,
|
|
531
531
|
// 否则 CLI 会忽略 provider 的 AUTH_TOKEN、错误地走 OAuth 打到第三方
|
|
@@ -567,7 +567,7 @@ export class ConversationService {
|
|
|
567
567
|
private shouldStripInheritedProviderEnv(): boolean {
|
|
568
568
|
const configDir =
|
|
569
569
|
process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude')
|
|
570
|
-
const ccHahaDir = path.join(configDir, '
|
|
570
|
+
const ccHahaDir = path.join(configDir, 'bingo')
|
|
571
571
|
const providersIndexPath = path.join(ccHahaDir, 'providers.json')
|
|
572
572
|
const settingsPath = path.join(ccHahaDir, 'settings.json')
|
|
573
573
|
|
|
@@ -599,13 +599,13 @@ export class ConversationService {
|
|
|
599
599
|
* 这种情况下 CLI 必须按 token 路径走第三方 endpoint,不能被 managed 规则
|
|
600
600
|
* 强制切 OAuth。
|
|
601
601
|
*
|
|
602
|
-
* 默认 (读不到 settings.json) 按"官方"处理 — 即使用户从未用过
|
|
602
|
+
* 默认 (读不到 settings.json) 按"官方"处理 — 即使用户从未用过 bingo
|
|
603
603
|
* provider 管理,也希望官方 OAuth 能正常工作。
|
|
604
604
|
*/
|
|
605
605
|
private shouldMarkManagedOAuth(): boolean {
|
|
606
606
|
const configDir =
|
|
607
607
|
process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude')
|
|
608
|
-
const settingsPath = path.join(configDir, '
|
|
608
|
+
const settingsPath = path.join(configDir, 'bingo', 'settings.json')
|
|
609
609
|
try {
|
|
610
610
|
const raw = fs.readFileSync(settingsPath, 'utf-8')
|
|
611
611
|
const parsed = JSON.parse(raw) as { env?: Record<string, string> }
|
|
@@ -71,7 +71,7 @@ export class HahaOAuthService {
|
|
|
71
71
|
private getOAuthFilePath(): string {
|
|
72
72
|
const configDir =
|
|
73
73
|
process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude')
|
|
74
|
-
return path.join(configDir, '
|
|
74
|
+
return path.join(configDir, 'bingo', 'oauth.json')
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
async loadTokens(): Promise<StoredOAuthTokens | null> {
|
|
@@ -6,7 +6,7 @@ import { loadPresets, applyPreset } from '../config/providerPresets.ts';
|
|
|
6
6
|
import axios from 'axios';
|
|
7
7
|
|
|
8
8
|
const home = process.env.CLAUDE_CONFIG_DIR || os.homedir();
|
|
9
|
-
const PROVIDERS_PATH = path.resolve(home, '.claude', '
|
|
9
|
+
const PROVIDERS_PATH = path.resolve(home, '.claude', 'bingo', 'providers.json');
|
|
10
10
|
|
|
11
11
|
export class ProviderManager {
|
|
12
12
|
static async load(): Promise<ProvidersIndex> {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Provider Service — preset-based provider configuration
|
|
3
3
|
*
|
|
4
|
-
* Storage: ~/.claude/
|
|
5
|
-
* Active provider env vars written to ~/.claude/
|
|
4
|
+
* Storage: ~/.claude/bingo/providers.json (lightweight index)
|
|
5
|
+
* Active provider env vars written to ~/.claude/bingo/settings.json
|
|
6
6
|
* (isolated from the original Claude Code's ~/.claude/settings.json)
|
|
7
7
|
*/
|
|
8
8
|
|
|
@@ -58,7 +58,7 @@ export class ProviderService {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
private getCcHahaDir(): string {
|
|
61
|
-
return path.join(this.getConfigDir(), '
|
|
61
|
+
return path.join(this.getConfigDir(), 'bingo')
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
private getIndexPath(): string {
|
|
@@ -267,22 +267,22 @@ export class ProviderService {
|
|
|
267
267
|
|
|
268
268
|
/**
|
|
269
269
|
* Check whether any usable auth exists:
|
|
270
|
-
* 1. A
|
|
270
|
+
* 1. A bingo provider is active → has auth
|
|
271
271
|
* 2. Original ~/.claude/settings.json has ANTHROPIC_AUTH_TOKEN or ANTHROPIC_API_KEY → has auth
|
|
272
272
|
* 3. process.env already has ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN → has auth
|
|
273
273
|
* 4. None of the above → needs setup
|
|
274
274
|
*/
|
|
275
275
|
async checkAuthStatus(): Promise<{
|
|
276
276
|
hasAuth: boolean
|
|
277
|
-
source: '
|
|
277
|
+
source: 'bingo-provider' | 'original-settings' | 'env' | 'none'
|
|
278
278
|
activeProvider?: string
|
|
279
279
|
}> {
|
|
280
|
-
// 1. Check
|
|
280
|
+
// 1. Check bingo active provider
|
|
281
281
|
const index = await this.readIndex()
|
|
282
282
|
if (index.activeId) {
|
|
283
283
|
const provider = index.providers.find(p => p.id === index.activeId)
|
|
284
284
|
if (provider?.apiKey) {
|
|
285
|
-
return { hasAuth: true, source: '
|
|
285
|
+
return { hasAuth: true, source: 'bingo-provider', activeProvider: provider.name }
|
|
286
286
|
}
|
|
287
287
|
}
|
|
288
288
|
|
|
@@ -364,6 +364,7 @@ export class ProviderService {
|
|
|
364
364
|
apiKey: string
|
|
365
365
|
apiFormat: ApiFormat
|
|
366
366
|
modelId: string
|
|
367
|
+
label?: string | null
|
|
367
368
|
} | null> {
|
|
368
369
|
const slots = await this.readSlots()
|
|
369
370
|
const entry = slots[slotName]
|
|
@@ -376,6 +377,7 @@ export class ProviderService {
|
|
|
376
377
|
apiKey: provider.apiKey,
|
|
377
378
|
apiFormat: provider.apiFormat ?? 'anthropic',
|
|
378
379
|
modelId: entry.modelId,
|
|
380
|
+
label: entry.label,
|
|
379
381
|
}
|
|
380
382
|
}
|
|
381
383
|
|
|
@@ -384,7 +386,12 @@ export class ProviderService {
|
|
|
384
386
|
const preset = PROVIDER_PRESETS.find(p => p.id === provider.presetId)
|
|
385
387
|
|
|
386
388
|
const base = provider.baseUrl.replace(/\/+$/, '')
|
|
387
|
-
if (!base) return []
|
|
389
|
+
if (!base && provider.presetId !== 'official') return []
|
|
390
|
+
|
|
391
|
+
// Special case for Official
|
|
392
|
+
if (provider.presetId === 'official') {
|
|
393
|
+
return ['claude-3-5-sonnet-20241022', 'claude-3-5-haiku-20241022', 'claude-3-opus-20240229', 'claude-3-sonnet-20240229', 'claude-3-haiku-20240307']
|
|
394
|
+
}
|
|
388
395
|
|
|
389
396
|
const modelsUrl = preset?.modelsUrl || '/v1/models'
|
|
390
397
|
const url = `${base}${modelsUrl}`
|
|
@@ -402,9 +409,10 @@ export class ProviderService {
|
|
|
402
409
|
}
|
|
403
410
|
|
|
404
411
|
try {
|
|
405
|
-
const
|
|
406
|
-
const res = await fetch(url, { headers, signal: AbortSignal.timeout(10000), ...
|
|
412
|
+
const fetchOpts = getProxyFetchOptions()
|
|
413
|
+
const res = await fetch(url, { headers, signal: AbortSignal.timeout(10000), ...fetchOpts })
|
|
407
414
|
if (!res.ok) {
|
|
415
|
+
console.error(`[ProviderService] Failed to fetch models from ${url}: ${res.status}`)
|
|
408
416
|
return []
|
|
409
417
|
}
|
|
410
418
|
const data = await res.json() as any
|
|
@@ -412,7 +420,8 @@ export class ProviderService {
|
|
|
412
420
|
const list = data[dataPath] ?? data.data ?? data.models ?? []
|
|
413
421
|
if (!Array.isArray(list)) return []
|
|
414
422
|
return list.map((m: any) => (typeof m === 'string' ? m : m.id)).filter(Boolean)
|
|
415
|
-
} catch {
|
|
423
|
+
} catch (err) {
|
|
424
|
+
console.error(`[ProviderService] Error fetching models from ${url}:`, err)
|
|
416
425
|
return []
|
|
417
426
|
}
|
|
418
427
|
}
|
|
@@ -425,9 +434,17 @@ export class ProviderService {
|
|
|
425
434
|
): Promise<ProviderTestResult> {
|
|
426
435
|
const provider = await this.getProvider(id)
|
|
427
436
|
const baseUrl = overrides?.baseUrl || provider.baseUrl
|
|
428
|
-
const modelId = overrides?.modelId || provider.models.main
|
|
429
437
|
const apiFormat = overrides?.apiFormat ?? provider.apiFormat ?? 'anthropic'
|
|
430
438
|
|
|
439
|
+
// If no modelId provided, try to fetch from provider or use preset default
|
|
440
|
+
let modelId = overrides?.modelId || provider.models.main
|
|
441
|
+
if (!modelId || modelId === 'auto' || modelId.startsWith('claude-')) {
|
|
442
|
+
const fetched = await this.fetchProviderModels(id).catch(() => [])
|
|
443
|
+
if (fetched.length > 0) {
|
|
444
|
+
modelId = fetched[0] // Use first available model for testing
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
431
448
|
if (!baseUrl || !provider.apiKey) {
|
|
432
449
|
return { connectivity: { success: false, latencyMs: 0, error: 'Missing baseUrl or apiKey' } }
|
|
433
450
|
}
|
|
@@ -474,6 +491,7 @@ export class ProviderService {
|
|
|
474
491
|
const start = Date.now()
|
|
475
492
|
try {
|
|
476
493
|
const { url, headers, body } = buildDirectTestRequest(base, apiKey, modelId, format)
|
|
494
|
+
// 使用 getDirectFetchOptions 以绕开系统代理,测试直接连接
|
|
477
495
|
const directOpts = getDirectFetchOptions()
|
|
478
496
|
const response = await fetch(url, {
|
|
479
497
|
method: 'POST',
|
|
@@ -538,13 +556,13 @@ export class ProviderService {
|
|
|
538
556
|
}
|
|
539
557
|
|
|
540
558
|
// Call upstream with transformed request
|
|
541
|
-
const
|
|
559
|
+
const fetchOpts = getProxyFetchOptions()
|
|
542
560
|
const response = await fetch(upstreamUrl, {
|
|
543
561
|
method: 'POST',
|
|
544
562
|
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
|
|
545
563
|
body: JSON.stringify(transformedBody),
|
|
546
564
|
signal: AbortSignal.timeout(30000),
|
|
547
|
-
...
|
|
565
|
+
...fetchOpts,
|
|
548
566
|
})
|
|
549
567
|
|
|
550
568
|
if (!response.ok) {
|
|
@@ -82,6 +82,7 @@ export type SlotName = z.infer<typeof SlotNameSchema>
|
|
|
82
82
|
export const SlotEntrySchema = z.object({
|
|
83
83
|
providerId: z.string(),
|
|
84
84
|
modelId: z.string(),
|
|
85
|
+
label: z.string().nullable().optional(), // Display name for UI
|
|
85
86
|
}).nullable()
|
|
86
87
|
export type SlotEntry = z.infer<typeof SlotEntrySchema>
|
|
87
88
|
|
|
@@ -263,7 +263,7 @@ async function runDesktopPermissionDialog(
|
|
|
263
263
|
}
|
|
264
264
|
|
|
265
265
|
/**
|
|
266
|
-
* Load pre-authorized apps from ~/.claude/
|
|
266
|
+
* Load pre-authorized apps from ~/.claude/bingo/computer-use-config.json.
|
|
267
267
|
* Called once when the binding is first created. Pre-authorized apps
|
|
268
268
|
* are injected into appState so `getAllowedApps()` returns them
|
|
269
269
|
* immediately — no runtime permission dialog needed.
|
|
@@ -272,7 +272,7 @@ async function loadPreAuthorizedApps(): Promise<void> {
|
|
|
272
272
|
try {
|
|
273
273
|
const configPath = join(
|
|
274
274
|
process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), '.claude'),
|
|
275
|
-
'
|
|
275
|
+
'bingo',
|
|
276
276
|
'computer-use-config.json',
|
|
277
277
|
)
|
|
278
278
|
const raw = await readFile(configPath, 'utf8')
|
package/src/utils/config.ts
CHANGED
|
@@ -1344,11 +1344,17 @@ export function enableConfigs(): void {
|
|
|
1344
1344
|
// to prevent us from adding config reading during module initialization
|
|
1345
1345
|
configReadingAllowed = true
|
|
1346
1346
|
// We only check the global config because currently all the configs share a file
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1347
|
+
try {
|
|
1348
|
+
getConfig(
|
|
1349
|
+
getGlobalClaudeFile(),
|
|
1350
|
+
createDefaultGlobalConfig,
|
|
1351
|
+
true /* throw on invalid */,
|
|
1352
|
+
)
|
|
1353
|
+
} catch (e) {
|
|
1354
|
+
logForDebugging(`Failed to load config during enableConfigs: ${e}`, { level: 'error' })
|
|
1355
|
+
// If it's a corrupted file, we allow the boostrap to continue with defaults
|
|
1356
|
+
// instead of hard-crashing the process.
|
|
1357
|
+
}
|
|
1352
1358
|
|
|
1353
1359
|
logForDiagnosticsNoPII('info', 'enable_configs_completed', {
|
|
1354
1360
|
duration_ms: Date.now() - startTime,
|
package/src/utils/managedEnv.ts
CHANGED
|
@@ -98,15 +98,23 @@ function filterSettingsEnv(
|
|
|
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
|
-
function
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
101
|
+
function getBingoSettingsEnv(): Record<string, string> {
|
|
102
|
+
const configDir = getClaudeConfigHomeDir()
|
|
103
|
+
const paths = [
|
|
104
|
+
join(configDir, 'bingo', 'settings.json'),
|
|
105
|
+
join(configDir, 'cc-haha', 'settings.json'), // Fallback for migration
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
for (const settingsPath of paths) {
|
|
109
|
+
try {
|
|
110
|
+
const raw = readFileSync(settingsPath, 'utf-8')
|
|
111
|
+
const parsed = JSON.parse(raw) as { env?: Record<string, string> }
|
|
112
|
+
if (parsed.env) return parsed.env
|
|
113
|
+
} catch {
|
|
114
|
+
continue
|
|
115
|
+
}
|
|
109
116
|
}
|
|
117
|
+
return {}
|
|
110
118
|
}
|
|
111
119
|
|
|
112
120
|
/**
|
|
@@ -167,11 +175,11 @@ export function applySafeConfigEnvironmentVariables(): void {
|
|
|
167
175
|
)
|
|
168
176
|
}
|
|
169
177
|
|
|
170
|
-
//
|
|
171
|
-
// AFTER userSettings so
|
|
172
|
-
// the original Claude Code's settings. This prevents
|
|
178
|
+
// bingo provider isolation: apply env from ~/.claude/bingo/settings.json
|
|
179
|
+
// AFTER userSettings so Bingo-specific provider config takes priority over
|
|
180
|
+
// the original Claude Code's settings. This prevents Bingo from polluting
|
|
173
181
|
// ~/.claude/settings.json while still allowing it to override provider vars.
|
|
174
|
-
Object.assign(process.env, filterSettingsEnv(
|
|
182
|
+
Object.assign(process.env, filterSettingsEnv(getBingoSettingsEnv()))
|
|
175
183
|
|
|
176
184
|
// Compute remote-managed-settings eligibility now, with userSettings and
|
|
177
185
|
// flagSettings env applied. Eligibility reads CLAUDE_CODE_USE_BEDROCK,
|
|
@@ -214,9 +222,9 @@ export function applyConfigEnvironmentVariables(): void {
|
|
|
214
222
|
|
|
215
223
|
Object.assign(process.env, filterSettingsEnv(getSettings_DEPRECATED()?.env))
|
|
216
224
|
|
|
217
|
-
//
|
|
218
|
-
// apply
|
|
219
|
-
Object.assign(process.env, filterSettingsEnv(
|
|
225
|
+
// bingo provider isolation: same as in applySafeConfigEnvironmentVariables,
|
|
226
|
+
// apply Bingo-specific env last so it overrides the original settings.
|
|
227
|
+
Object.assign(process.env, filterSettingsEnv(getBingoSettingsEnv()))
|
|
220
228
|
|
|
221
229
|
// Clear caches so agents are rebuilt with the new env vars
|
|
222
230
|
clearCACertsCache()
|
|
@@ -202,7 +202,7 @@ export function PreflightStep(t0) {
|
|
|
202
202
|
|
|
203
203
|
//@C:ID=F.PC._temp;K=F;V=1.0;P=Helper function for process exit;D=UI;M=Connectivity;S=Utility;In=void;Out=void
|
|
204
204
|
function _temp() {
|
|
205
|
-
console.log("F.PC._temp");
|
|
206
|
-
|
|
207
|
-
return process.exit(1);
|
|
205
|
+
console.log("F.PC._temp: Skipping force exit on connectivity failure.");
|
|
206
|
+
|
|
207
|
+
// return process.exit(1);
|
|
208
208
|
}
|
package/src/utils/proxy.ts
CHANGED
|
@@ -334,14 +334,26 @@ export function getDirectFetchOptions(): {
|
|
|
334
334
|
return { ...base, proxy: undefined, ...getTLSFetchOptions() }
|
|
335
335
|
}
|
|
336
336
|
|
|
337
|
+
// Check if system proxy exists
|
|
338
|
+
const proxyUrl = getProxyUrl()
|
|
339
|
+
if (!proxyUrl) {
|
|
340
|
+
// No proxy configured, just return normal fetch options
|
|
341
|
+
return { ...base, ...getTLSFetchOptions() }
|
|
342
|
+
}
|
|
343
|
+
|
|
337
344
|
// In Node.js/undici, a fresh Agent with no proxy settings bypasses system defaults
|
|
338
345
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
339
346
|
const undiciMod = require('undici') as typeof undici
|
|
340
347
|
const tlsOpts = getTLSFetchOptions()
|
|
341
348
|
|
|
349
|
+
// Use the global dispatcher's options if possible, or fresh default options
|
|
350
|
+
const agentOptions = tlsOpts.dispatcher && 'options' in (tlsOpts.dispatcher as any)
|
|
351
|
+
? (tlsOpts.dispatcher as any).options
|
|
352
|
+
: {}
|
|
353
|
+
|
|
342
354
|
return {
|
|
343
355
|
...base,
|
|
344
|
-
dispatcher: new undiciMod.Agent(
|
|
356
|
+
dispatcher: new undiciMod.Agent(agentOptions),
|
|
345
357
|
}
|
|
346
358
|
}
|
|
347
359
|
|