bingocode 1.0.38 → 1.0.41
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
|
@@ -980,7 +980,7 @@ export const CliMenuManager: React.FC = () => {
|
|
|
980
980
|
</Box>
|
|
981
981
|
|
|
982
982
|
{/* ── 消息区 ── */}
|
|
983
|
-
<Box height={MSGS_H} flexDirection="column"
|
|
983
|
+
<Box height={MSGS_H} flexDirection="column">
|
|
984
984
|
{loadingMsgs && <Text color="yellow">加载消息中...</Text>}
|
|
985
985
|
{msgsErr && <Text color="red">错误: {msgsErr}</Text>}
|
|
986
986
|
{!loadingMsgs && !msgsErr && displayMsgs.length === 0 && (
|
|
@@ -1024,11 +1024,16 @@ export const CliMenuManager: React.FC = () => {
|
|
|
1024
1024
|
setHistoryMenuStage('window');
|
|
1025
1025
|
}
|
|
1026
1026
|
}}
|
|
1027
|
-
itemComponent={({ isSelected, label
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1027
|
+
itemComponent={({ isSelected, label }) => {
|
|
1028
|
+
const it = groupedHistoryItems.find(i => i.label === label);
|
|
1029
|
+
const isGroup = it?.isGroup;
|
|
1030
|
+
const color = it?.color;
|
|
1031
|
+
return (
|
|
1032
|
+
<Text color={isGroup ? 'gray' : (color ? color : (isSelected ? 'cyan' : undefined))}>
|
|
1033
|
+
{label}
|
|
1034
|
+
</Text>
|
|
1035
|
+
)
|
|
1036
|
+
}}
|
|
1032
1037
|
/>
|
|
1033
1038
|
<Hint>{i18nMap[lang].historyHint}</Hint>
|
|
1034
1039
|
</Box>
|
|
@@ -21,9 +21,28 @@ import { openaiChatStreamToAnthropic } from './streaming/openaiChatStreamToAnthr
|
|
|
21
21
|
import { openaiResponsesStreamToAnthropic } from './streaming/openaiResponsesStreamToAnthropic.js'
|
|
22
22
|
import type { AnthropicRequest } from './transform/types.js'
|
|
23
23
|
import type { SlotName } from '../types/provider.js'
|
|
24
|
+
import { logForDebugging } from '../../utils/debug.js'
|
|
25
|
+
import { appendFile, mkdir } from 'node:fs/promises'
|
|
26
|
+
import { join } from 'node:path'
|
|
27
|
+
import { homedir } from 'node:os'
|
|
24
28
|
|
|
25
29
|
const providerService = new ProviderService()
|
|
26
30
|
|
|
31
|
+
async function logToFile(message: string) {
|
|
32
|
+
// Disabled log output for production
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function sendAnthropicError(message: string, _model: string | undefined, status = 502): Response {
|
|
36
|
+
const fullMessage = `[Bingo Proxy] ${message}`
|
|
37
|
+
void logToFile(`ERROR: ${fullMessage} (status: ${status})`)
|
|
38
|
+
|
|
39
|
+
// 统一返回纯文本以规避 CLI 的 JSON 字符串打印行为,确保报错清晰且不带 JSON 外壳
|
|
40
|
+
return new Response(fullMessage, {
|
|
41
|
+
status: status,
|
|
42
|
+
headers: { 'Content-Type': 'text/plain' }
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
27
46
|
function buildUpstreamHeaders(apiKey: string): Record<string, string> {
|
|
28
47
|
const headers: Record<string, string> = {
|
|
29
48
|
'Content-Type': 'application/json',
|
|
@@ -43,23 +62,41 @@ function buildUpstreamHeaders(apiKey: string): Record<string, string> {
|
|
|
43
62
|
|
|
44
63
|
/**
|
|
45
64
|
* Identify which slot a model name belongs to.
|
|
46
|
-
*
|
|
65
|
+
* Checks for exact matches in provider models first, then falls back to keyword matching.
|
|
47
66
|
*/
|
|
48
|
-
function identifySlot(modelName: string): SlotName {
|
|
67
|
+
async function identifySlot(modelName: string): Promise<SlotName> {
|
|
49
68
|
const m = modelName.toLowerCase()
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
69
|
+
void logToFile(`Identifying slot. Input: "${modelName}"`)
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const { providers } = await providerService.listProviders()
|
|
73
|
+
for (const config of providers) {
|
|
74
|
+
if (config.models) {
|
|
75
|
+
for (const [slot, id] of Object.entries(config.models)) {
|
|
76
|
+
if (id && id.toLowerCase() === m) {
|
|
77
|
+
void logToFile(`Match found in config! Slot: ${slot} (ID: ${id})`)
|
|
78
|
+
return slot as SlotName
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} catch (e) {
|
|
84
|
+
void logToFile(`Error reading providers for identification: ${e}`)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let result: SlotName = 'main'
|
|
88
|
+
if (m.includes('opus')) result = 'opus'
|
|
89
|
+
else if (m.includes('sonnet')) result = 'sonnet'
|
|
90
|
+
else if (m.includes('haiku')) result = 'haiku'
|
|
91
|
+
|
|
92
|
+
void logToFile(`Fallback identification result: ${result}`)
|
|
93
|
+
return result
|
|
54
94
|
}
|
|
55
95
|
|
|
56
96
|
export async function handleProxyRequest(req: Request, url: URL): Promise<Response> {
|
|
57
97
|
// Only handle POST /proxy/v1/messages
|
|
58
98
|
if (req.method !== 'POST' || url.pathname !== '/proxy/v1/messages') {
|
|
59
|
-
return
|
|
60
|
-
{ error: 'Not Found', message: 'Proxy only handles POST /proxy/v1/messages' },
|
|
61
|
-
{ status: 404 },
|
|
62
|
-
)
|
|
99
|
+
return sendAnthropicError('Not Found: Proxy only handles POST /proxy/v1/messages', undefined, 404)
|
|
63
100
|
}
|
|
64
101
|
|
|
65
102
|
// Parse request body
|
|
@@ -67,20 +104,19 @@ export async function handleProxyRequest(req: Request, url: URL): Promise<Respon
|
|
|
67
104
|
try {
|
|
68
105
|
body = (await req.json()) as AnthropicRequest
|
|
69
106
|
} catch {
|
|
70
|
-
return
|
|
71
|
-
{ type: 'error', error: { type: 'invalid_request_error', message: 'Invalid JSON in request body' } },
|
|
72
|
-
{ status: 400 },
|
|
73
|
-
)
|
|
107
|
+
return sendAnthropicError('Invalid JSON in request body', undefined, 400)
|
|
74
108
|
}
|
|
75
109
|
|
|
76
110
|
const isStream = body.stream === true
|
|
77
111
|
|
|
78
112
|
// --- Slot-based routing ---
|
|
79
|
-
const slot = identifySlot(body.model ?? '')
|
|
113
|
+
const slot = await identifySlot(body.model ?? '')
|
|
114
|
+
const reqId = Math.random().toString(36).substring(7)
|
|
115
|
+
void logToFile(`[${reqId}] Body Model: "${body.model}". Decided Slot: "${slot}"`)
|
|
116
|
+
|
|
80
117
|
const slotConfig = await providerService.getProviderForSlot(slot)
|
|
81
118
|
|
|
82
119
|
if (slotConfig) {
|
|
83
|
-
// Use the slot's configured modelId instead of the original Claude model name
|
|
84
120
|
const proxiedBody: AnthropicRequest = { ...body, model: slotConfig.modelId }
|
|
85
121
|
const baseUrl = slotConfig.baseUrl.replace(/\/+$/, '')
|
|
86
122
|
const uiLabel = slotConfig.label || null
|
|
@@ -94,40 +130,20 @@ export async function handleProxyRequest(req: Request, url: URL): Promise<Respon
|
|
|
94
130
|
return await handleOpenaiResponses(proxiedBody, baseUrl, slotConfig.apiKey, isStream, uiLabel)
|
|
95
131
|
}
|
|
96
132
|
} catch (err) {
|
|
97
|
-
|
|
98
|
-
return
|
|
99
|
-
{
|
|
100
|
-
type: 'error',
|
|
101
|
-
error: {
|
|
102
|
-
type: 'api_error',
|
|
103
|
-
message: err instanceof Error ? err.message : String(err),
|
|
104
|
-
},
|
|
105
|
-
},
|
|
106
|
-
{ status: 502 },
|
|
107
|
-
)
|
|
133
|
+
logForDebugging(`[HANDLER][${reqId}] Slot "${slot}" upstream connection failed: ${err}`, { level: 'error' })
|
|
134
|
+
return sendAnthropicError(`API Connection Failed: Ensure baseUrl is correct. Error: ${err instanceof Error ? err.message : String(err)}`, body.model, 502)
|
|
108
135
|
}
|
|
109
136
|
}
|
|
110
137
|
|
|
111
138
|
// --- Fallback: legacy single-activeId routing ---
|
|
112
139
|
const config = await providerService.getActiveProviderForProxy()
|
|
113
140
|
if (!config) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
type: 'error',
|
|
117
|
-
error: {
|
|
118
|
-
type: 'invalid_request_error',
|
|
119
|
-
message: `No provider configured for slot "${slot}". Please configure slots in the Provider panel.`,
|
|
120
|
-
},
|
|
121
|
-
},
|
|
122
|
-
{ status: 400 },
|
|
123
|
-
)
|
|
141
|
+
logForDebugging(`[HANDLER][${reqId}] No provider configured for slot "${slot}"`, { level: 'warn' })
|
|
142
|
+
return sendAnthropicError(`No provider configured for slot "${slot}". Please configure slots in the Provider panel.`, body.model)
|
|
124
143
|
}
|
|
125
144
|
|
|
126
145
|
if (config.apiFormat === 'anthropic') {
|
|
127
|
-
return
|
|
128
|
-
{ type: 'error', error: { type: 'invalid_request_error', message: 'Active provider uses anthropic format — proxy not needed' } },
|
|
129
|
-
{ status: 400 },
|
|
130
|
-
)
|
|
146
|
+
return sendAnthropicError('Active provider uses anthropic format — proxy not needed', body.model)
|
|
131
147
|
}
|
|
132
148
|
|
|
133
149
|
const baseUrl = config.baseUrl.replace(/\/+$/, '')
|
|
@@ -139,17 +155,8 @@ export async function handleProxyRequest(req: Request, url: URL): Promise<Respon
|
|
|
139
155
|
return await handleOpenaiResponses(body, baseUrl, config.apiKey, isStream)
|
|
140
156
|
}
|
|
141
157
|
} catch (err) {
|
|
142
|
-
|
|
143
|
-
return
|
|
144
|
-
{
|
|
145
|
-
type: 'error',
|
|
146
|
-
error: {
|
|
147
|
-
type: 'api_error',
|
|
148
|
-
message: err instanceof Error ? err.message : String(err),
|
|
149
|
-
},
|
|
150
|
-
},
|
|
151
|
-
{ status: 502 },
|
|
152
|
-
)
|
|
158
|
+
logForDebugging(`[HANDLER][${reqId}] Upstream connection failed: ${err}`, { level: 'error' })
|
|
159
|
+
return sendAnthropicError(`API Connection Failed: Ensure baseUrl is correct. Error: ${err instanceof Error ? err.message : String(err)}`, body.model, 502)
|
|
153
160
|
}
|
|
154
161
|
}
|
|
155
162
|
|
|
@@ -176,12 +183,22 @@ async function handleAnthropicPassthrough(
|
|
|
176
183
|
signal: isStream ? AbortSignal.timeout(30_000) : AbortSignal.timeout(300_000),
|
|
177
184
|
})
|
|
178
185
|
|
|
179
|
-
|
|
186
|
+
if (!upstream.ok) {
|
|
187
|
+
const errText = await upstream.text().catch(() => '')
|
|
188
|
+
const errorMessage = `Upstream returned HTTP ${upstream.status}: ${errText.slice(0, 500)}`
|
|
189
|
+
return sendAnthropicError(errorMessage, body.model, 502)
|
|
190
|
+
}
|
|
180
191
|
|
|
181
192
|
if (isStream) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
193
|
+
if (!upstream.body) {
|
|
194
|
+
return sendAnthropicError('Upstream returned no body for stream', body.model)
|
|
195
|
+
}
|
|
196
|
+
return new Response(upstream.body, {
|
|
197
|
+
status: 200,
|
|
198
|
+
headers: {
|
|
199
|
+
'Content-Type': 'application/json',
|
|
200
|
+
},
|
|
201
|
+
})
|
|
185
202
|
}
|
|
186
203
|
|
|
187
204
|
const responseBody = await upstream.json()
|
|
@@ -209,12 +226,21 @@ async function handleOpenaiChat(
|
|
|
209
226
|
})
|
|
210
227
|
|
|
211
228
|
if (!upstream.ok) {
|
|
212
|
-
|
|
229
|
+
const errText = await upstream.text().catch(() => '')
|
|
230
|
+
let errorMessage = `Upstream returned HTTP ${upstream.status}: ${errText.slice(0, 500)}`
|
|
231
|
+
let status = upstream.status
|
|
232
|
+
|
|
233
|
+
if (upstream.status === 404) {
|
|
234
|
+
status = 502
|
|
235
|
+
errorMessage = `API Connection Failed: The baseUrl path might be incorrect (404 Not Found). Detail: ${errText}`
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return sendAnthropicError(errorMessage, body.model, status)
|
|
213
239
|
}
|
|
214
240
|
|
|
215
241
|
if (isStream) {
|
|
216
242
|
if (!upstream.body) {
|
|
217
|
-
return
|
|
243
|
+
return sendAnthropicError('Upstream returned no body for stream', body.model)
|
|
218
244
|
}
|
|
219
245
|
const anthropicStream = openaiChatStreamToAnthropic(upstream.body, uiLabel || body.model)
|
|
220
246
|
return new Response(anthropicStream, {
|
|
@@ -252,24 +278,20 @@ async function handleOpenaiResponses(
|
|
|
252
278
|
|
|
253
279
|
if (!upstream.ok) {
|
|
254
280
|
const errText = await upstream.text().catch(() => '')
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
)
|
|
281
|
+
let errorMessage = `Upstream returned HTTP ${upstream.status}: ${errText.slice(0, 500)}`
|
|
282
|
+
let status = upstream.status
|
|
283
|
+
|
|
284
|
+
if (upstream.status === 404) {
|
|
285
|
+
status = 502
|
|
286
|
+
errorMessage = `API Connection Failed: The baseUrl path might be incorrect (404 Not Found). Detail: ${errText}`
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return sendAnthropicError(errorMessage, body.model, status)
|
|
265
290
|
}
|
|
266
291
|
|
|
267
292
|
if (isStream) {
|
|
268
293
|
if (!upstream.body) {
|
|
269
|
-
return
|
|
270
|
-
{ type: 'error', error: { type: 'api_error', message: 'Upstream returned no body for stream' } },
|
|
271
|
-
{ status: 502 },
|
|
272
|
-
)
|
|
294
|
+
return sendAnthropicError('Upstream returned no body for stream', body.model)
|
|
273
295
|
}
|
|
274
296
|
const anthropicStream = openaiResponsesStreamToAnthropic(upstream.body, uiLabel || body.model)
|
|
275
297
|
return new Response(anthropicStream, {
|
|
@@ -33,7 +33,7 @@ export function anthropicToOpenaiChat(body: AnthropicRequest): OpenAIChatRequest
|
|
|
33
33
|
|
|
34
34
|
// Convert messages
|
|
35
35
|
for (const msg of body.messages) {
|
|
36
|
-
convertMessage(msg, messages)
|
|
36
|
+
convertMessage(msg, messages, body.model)
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
// Build request
|
|
@@ -90,7 +90,7 @@ export function anthropicToOpenaiChat(body: AnthropicRequest): OpenAIChatRequest
|
|
|
90
90
|
return result
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
function convertMessage(msg: AnthropicMessage, output: OpenAIChatMessage[]): void {
|
|
93
|
+
function convertMessage(msg: AnthropicMessage, output: OpenAIChatMessage[], model: string = ''): void {
|
|
94
94
|
const content = msg.content
|
|
95
95
|
|
|
96
96
|
// Simple string content
|
|
@@ -108,7 +108,7 @@ function convertMessage(msg: AnthropicMessage, output: OpenAIChatMessage[]): voi
|
|
|
108
108
|
if (msg.role === 'user') {
|
|
109
109
|
convertUserMessage(content, output)
|
|
110
110
|
} else {
|
|
111
|
-
convertAssistantMessage(content, output)
|
|
111
|
+
convertAssistantMessage(content, output, model)
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
|
|
@@ -147,13 +147,16 @@ function convertUserMessage(blocks: AnthropicContentBlock[], output: OpenAIChatM
|
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
function convertAssistantMessage(blocks: AnthropicContentBlock[], output: OpenAIChatMessage[]): void {
|
|
150
|
+
function convertAssistantMessage(blocks: AnthropicContentBlock[], output: OpenAIChatMessage[], model: string = ''): void {
|
|
151
151
|
let textContent = ''
|
|
152
|
+
let reasoningContent = ''
|
|
152
153
|
const toolCalls: OpenAIToolCall[] = []
|
|
153
154
|
|
|
154
155
|
for (const block of blocks) {
|
|
155
156
|
if (block.type === 'text') {
|
|
156
157
|
textContent += block.text
|
|
158
|
+
} else if (block.type === 'thinking') {
|
|
159
|
+
reasoningContent += block.thinking
|
|
157
160
|
} else if (block.type === 'tool_use') {
|
|
158
161
|
toolCalls.push({
|
|
159
162
|
id: block.id,
|
|
@@ -164,7 +167,6 @@ function convertAssistantMessage(blocks: AnthropicContentBlock[], output: OpenAI
|
|
|
164
167
|
},
|
|
165
168
|
})
|
|
166
169
|
}
|
|
167
|
-
// Skip thinking blocks — no OpenAI equivalent
|
|
168
170
|
}
|
|
169
171
|
|
|
170
172
|
const msg: OpenAIChatMessage = {
|
|
@@ -172,6 +174,11 @@ function convertAssistantMessage(blocks: AnthropicContentBlock[], output: OpenAI
|
|
|
172
174
|
content: textContent || null,
|
|
173
175
|
}
|
|
174
176
|
|
|
177
|
+
// Only pass reasoning_content back for DeepSeek models to satisfy their mandatory back-transmission rule
|
|
178
|
+
if (reasoningContent && model.toLowerCase().includes('deepseek')) {
|
|
179
|
+
(msg as any).reasoning_content = reasoningContent
|
|
180
|
+
}
|
|
181
|
+
|
|
175
182
|
if (toolCalls.length > 0) {
|
|
176
183
|
msg.tool_calls = toolCalls
|
|
177
184
|
}
|