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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bingocode",
3
- "version": "1.0.38",
3
+ "version": "1.0.41",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "claude": "bin/claude-win.cjs",
@@ -980,7 +980,7 @@ export const CliMenuManager: React.FC = () => {
980
980
  </Box>
981
981
 
982
982
  {/* ── 消息区 ── */}
983
- <Box height={MSGS_H} flexDirection="column" overflow="hidden">
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, color, isGroup }) => (
1028
- <Text color={isGroup ? 'gray' : (color ? color : (isSelected ? 'cyan' : undefined))}>
1029
- {label}
1030
- </Text>
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
- * Claude Code sends model names like "claude-3-5-haiku-20241022".
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
- if (m.includes('haiku')) return 'haiku'
51
- if (m.includes('sonnet')) return 'sonnet'
52
- if (m.includes('opus')) return 'opus'
53
- return 'main'
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 Response.json(
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 Response.json(
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
- console.error(`[Proxy] Slot "${slot}" upstream request failed:`, err)
98
- return Response.json(
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
- return Response.json(
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 Response.json(
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
- console.error('[Proxy] Upstream request failed:', err)
143
- return Response.json(
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
- // ... (existing error checks)
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
- // Anthropic pass-through doesn't easily support label injection without parsing SSE
183
- // So for native anthropic format, we might just pass original stream
184
- return new Response(upstream.body, { /* ... */ })
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
- // ... error handling
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 Response.json(/* ... */)
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
- return Response.json(
256
- {
257
- type: 'error',
258
- error: {
259
- type: 'api_error',
260
- message: `Upstream returned HTTP ${upstream.status}: ${errText.slice(0, 500)}`,
261
- },
262
- },
263
- { status: upstream.status },
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 Response.json(
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
  }