bingocode 1.1.204 → 1.1.205

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.1.204",
3
+ "version": "1.1.205",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "claude": "bin/claude-win.cjs",
@@ -140,7 +140,8 @@ export async function handleProxyRequest(req: Request, url: URL): Promise<Respon
140
140
 
141
141
  try {
142
142
  if (slotConfig.apiFormat === 'anthropic') {
143
- return await handleAnthropicPassthrough(proxiedBody, baseUrl, slotConfig.apiKey, isStream, uiLabel, betaHeader)
143
+ const stripThinking = slotConfig.extra?.strip_thinking === true
144
+ return await handleAnthropicPassthrough(proxiedBody, baseUrl, slotConfig.apiKey, isStream, uiLabel, betaHeader, stripThinking)
144
145
  } else if (slotConfig.apiFormat === 'openai_chat') {
145
146
  return await handleOpenaiChat(proxiedBody, baseUrl, slotConfig.apiKey, isStream, uiLabel)
146
147
  } else {
@@ -188,6 +189,7 @@ async function handleAnthropicPassthrough(
188
189
  isStream: boolean,
189
190
  uiLabel: string | null = null,
190
191
  betaHeader: string | null = null,
192
+ stripThinking: boolean = false,
191
193
  ): Promise<Response> {
192
194
  const url = v1Url(baseUrl, 'messages')
193
195
  const upstreamHeaders: Record<string, string> = {
@@ -197,10 +199,18 @@ async function handleAnthropicPassthrough(
197
199
  }
198
200
  if (betaHeader) upstreamHeaders['anthropic-beta'] = betaHeader
199
201
 
202
+ // Strip `thinking` only when the provider opts in via extra.strip_thinking.
203
+ // LiteLLM-backed upstreams (e.g. vtscc) convert `thinking` → `reasoning_effort`,
204
+ // which together_ai and other backends reject with 400. Real Anthropic API
205
+ // supports `thinking`, so we leave it intact unless explicitly told to strip.
206
+ const forwardBody = stripThinking && body.thinking
207
+ ? { ...body, thinking: undefined }
208
+ : body
209
+
200
210
  const upstream = await fetch(url, {
201
211
  method: 'POST',
202
212
  headers: upstreamHeaders,
203
- body: JSON.stringify(body),
213
+ body: JSON.stringify(forwardBody),
204
214
  signal: isStream ? AbortSignal.timeout(STREAM_TIMEOUT_MS) : AbortSignal.timeout(300_000),
205
215
  })
206
216
 
@@ -1,247 +1,220 @@
1
- /**
2
- * Request transformation: Anthropic Messages → OpenAI Chat Completions
3
- * Derived from cc-switch (https://github.com/farion1231/cc-switch)
4
- * Original work by Jason Young, MIT License
5
- */
6
-
7
- import type {
8
- AnthropicRequest,
9
- AnthropicContentBlock,
10
- AnthropicMessage,
11
- OpenAIChatRequest,
12
- OpenAIChatMessage,
13
- OpenAIChatContentPart,
14
- OpenAIToolCall,
15
- OpenAITool,
16
- } from './types.js'
17
-
18
- // Params broadly supported by OpenAI-compatible chat completion upstreams.
19
- // Anything outside this set (e.g. reasoning_effort) is dropped when
20
- // BINGO_DROP_PARAMS is enabled to avoid upstream 400 rejections.
21
- const OPENAI_CHAT_ALLOWED_PARAMS = new Set<keyof OpenAIChatRequest>([
22
- 'model',
23
- 'messages',
24
- 'max_tokens',
25
- 'max_completion_tokens',
26
- 'stream',
27
- 'temperature',
28
- 'top_p',
29
- 'stop',
30
- 'tools',
31
- 'tool_choice',
32
- 'frequency_penalty',
33
- ])
34
-
35
- /**
36
- * Convert Anthropic Messages request to OpenAI Chat Completions request.
37
- */
38
- export function anthropicToOpenaiChat(body: AnthropicRequest): OpenAIChatRequest {
39
- const messages: OpenAIChatMessage[] = []
40
-
41
- // Convert system prompt
42
- if (body.system) {
43
- if (typeof body.system === 'string') {
44
- messages.push({ role: 'system', content: body.system })
45
- } else if (Array.isArray(body.system)) {
46
- const text = body.system.map((b) => b.text).join('\n')
47
- messages.push({ role: 'system', content: text })
48
- }
49
- }
50
-
51
- // Convert messages
52
- for (const msg of body.messages) {
53
- convertMessage(msg, messages, body.model)
54
- }
55
-
56
- // Build request
57
- const result: OpenAIChatRequest = {
58
- model: body.model,
59
- messages,
60
- stream: body.stream,
61
- }
62
-
63
- // max_tokens — cap to avoid upstream 400 errors from Claude's high defaults (e.g. 64k).
64
- // DeepSeek: tools/thinking fail above 8192. Other providers: 32768 covers most upstreams.
65
- // GPT models (gpt-*): use max_completion_tokens instead of max_tokens (required by newer GPT models).
66
- if (body.max_tokens !== undefined) {
67
- const modelLower = body.model.toLowerCase()
68
- if (modelLower.includes('deepseek')) {
69
- result.max_tokens = Math.min(body.max_tokens, 8192)
70
- } else if (modelLower.startsWith('gpt-') || modelLower.startsWith('o1') || modelLower.startsWith('o3') || modelLower.startsWith('o4')) {
71
- result.max_completion_tokens = body.max_tokens
72
- } else {
73
- result.max_tokens = Math.min(body.max_tokens, 32768)
74
- }
75
- }
76
-
77
- // temperature & top_p
78
- if (body.temperature !== undefined) result.temperature = body.temperature
79
- if (body.top_p !== undefined) result.top_p = body.top_p
80
-
81
- // frequency_penalty: suppress repetition loops during multi-tool-call sequences.
82
- // Anthropic API has no equivalent; inject for all OpenAI-compatible upstreams.
83
- // Configurable via BINGO_FREQUENCY_PENALTY (default 0, opt-in).
84
- const fp = parseFloat(process.env.BINGO_FREQUENCY_PENALTY ?? '0')
85
- if (!isNaN(fp) && fp !== 0) result.frequency_penalty = fp
86
-
87
- // stop_sequences → stop
88
- if (body.stop_sequences && body.stop_sequences.length > 0) {
89
- result.stop = body.stop_sequences
90
- }
91
-
92
- // tools
93
- if (body.tools && body.tools.length > 0) {
94
- result.tools = body.tools
95
- .filter((t) => t.name !== 'BatchTool')
96
- .map((t): OpenAITool => ({
97
- type: 'function',
98
- function: {
99
- name: t.name,
100
- description: t.description,
101
- parameters: t.input_schema,
102
- },
103
- }))
104
- }
105
-
106
- // tool_choice
107
- if (body.tool_choice !== undefined) {
108
- result.tool_choice = convertToolChoice(body.tool_choice)
109
- }
110
-
111
- // thinking → reasoning_effort
112
- if (body.thinking) {
113
- const budget = body.thinking.budget_tokens
114
- if (budget !== undefined) {
115
- if (budget <= 1024) result.reasoning_effort = 'low'
116
- else if (budget <= 8192) result.reasoning_effort = 'medium'
117
- else result.reasoning_effort = 'high'
118
- } else if (body.thinking.type === 'enabled') {
119
- result.reasoning_effort = 'high'
120
- }
121
- }
122
-
123
- // Drop params that upstreams (e.g. LiteLLM-routed providers) may reject.
124
- // Default ON because many LiteLLM backends (together_ai, etc.) hard-fail on
125
- // unknown params. Set BINGO_DROP_PARAMS=0/false to keep all params.
126
- const dropParams = !/^(0|false|no)$/i.test(process.env.BINGO_DROP_PARAMS ?? '1')
127
- if (dropParams) {
128
- for (const key of Object.keys(result) as Array<keyof OpenAIChatRequest>) {
129
- if (!OPENAI_CHAT_ALLOWED_PARAMS.has(key)) delete result[key]
130
- }
131
- }
132
-
133
- return result
134
- }
135
-
136
- function convertMessage(msg: AnthropicMessage, output: OpenAIChatMessage[], model: string = ''): void {
137
- const content = msg.content
138
-
139
- // Simple string content
140
- if (typeof content === 'string') {
141
- output.push({ role: msg.role, content })
142
- return
143
- }
144
-
145
- // Array content blocks
146
- if (!Array.isArray(content) || content.length === 0) {
147
- output.push({ role: msg.role, content: '' })
148
- return
149
- }
150
-
151
- if (msg.role === 'user') {
152
- convertUserMessage(content, output)
153
- } else {
154
- convertAssistantMessage(content, output, model)
155
- }
156
- }
157
-
158
- function convertUserMessage(blocks: AnthropicContentBlock[], output: OpenAIChatMessage[]): void {
159
- // Separate tool_result blocks from other content
160
- const contentParts: OpenAIChatContentPart[] = []
161
-
162
- for (const block of blocks) {
163
- if (block.type === 'text') {
164
- contentParts.push({ type: 'text', text: block.text })
165
- } else if (block.type === 'image') {
166
- const url = `data:${block.source.media_type};base64,${block.source.data}`
167
- contentParts.push({ type: 'image_url', image_url: { url } })
168
- } else if (block.type === 'tool_result') {
169
- // tool_result separate tool message
170
- const rawContent = typeof block.content === 'string'
171
- ? block.content
172
- : Array.isArray(block.content)
173
- ? block.content.filter((b): b is Extract<AnthropicContentBlock, { type: 'text' }> => b.type === 'text').map((b) => b.text).join('\n')
174
- : ''
175
- const resultContent = block.is_error
176
- ? `<error>${rawContent}</error>`
177
- : rawContent
178
- output.push({
179
- role: 'tool',
180
- tool_call_id: block.tool_use_id,
181
- content: resultContent,
182
- })
183
- }
184
- }
185
-
186
- if (contentParts.length > 0) {
187
- output.push({
188
- role: 'user',
189
- content: contentParts.length === 1 && contentParts[0].type === 'text'
190
- ? contentParts[0].text
191
- : contentParts,
192
- })
193
- }
194
- }
195
-
196
- function convertAssistantMessage(blocks: AnthropicContentBlock[], output: OpenAIChatMessage[], model: string = ''): void {
197
- let textContent = ''
198
- let reasoningContent = ''
199
- const toolCalls: OpenAIToolCall[] = []
200
-
201
- for (const block of blocks) {
202
- if (block.type === 'text') {
203
- textContent += block.text
204
- } else if (block.type === 'thinking') {
205
- reasoningContent += block.thinking
206
- } else if (block.type === 'tool_use') {
207
- toolCalls.push({
208
- id: block.id,
209
- type: 'function',
210
- function: {
211
- name: block.name,
212
- arguments: typeof block.input === 'string' ? block.input : JSON.stringify(block.input),
213
- },
214
- })
215
- }
216
- }
217
-
218
- const msg: OpenAIChatMessage = {
219
- role: 'assistant',
220
- content: textContent || null,
221
- }
222
-
223
- // Only pass reasoning_content back for DeepSeek models to satisfy their mandatory back-transmission rule
224
- if (reasoningContent && model.toLowerCase().includes('deepseek')) {
225
- (msg as any).reasoning_content = reasoningContent
226
- }
227
-
228
- if (toolCalls.length > 0) {
229
- msg.tool_calls = toolCalls
230
- }
231
-
232
- output.push(msg)
233
- }
234
-
235
- function convertToolChoice(choice: unknown): unknown {
236
- if (typeof choice === 'string') return choice
237
- if (typeof choice === 'object' && choice !== null) {
238
- const c = choice as Record<string, unknown>
239
- if (c.type === 'auto') return 'auto'
240
- if (c.type === 'any') return 'required'
241
- if (c.type === 'none') return 'none'
242
- if (c.type === 'tool' && typeof c.name === 'string') {
243
- return { type: 'function', function: { name: c.name } }
244
- }
245
- }
246
- return 'auto'
247
- }
1
+ /**
2
+ * Request transformation: Anthropic Messages → OpenAI Chat Completions
3
+ * Derived from cc-switch (https://github.com/farion1231/cc-switch)
4
+ * Original work by Jason Young, MIT License
5
+ */
6
+
7
+ import type {
8
+ AnthropicRequest,
9
+ AnthropicContentBlock,
10
+ AnthropicMessage,
11
+ OpenAIChatRequest,
12
+ OpenAIChatMessage,
13
+ OpenAIChatContentPart,
14
+ OpenAIToolCall,
15
+ OpenAITool,
16
+ } from './types.js'
17
+
18
+ /**
19
+ * Convert Anthropic Messages request to OpenAI Chat Completions request.
20
+ */
21
+ export function anthropicToOpenaiChat(body: AnthropicRequest): OpenAIChatRequest {
22
+ const messages: OpenAIChatMessage[] = []
23
+
24
+ // Convert system prompt
25
+ if (body.system) {
26
+ if (typeof body.system === 'string') {
27
+ messages.push({ role: 'system', content: body.system })
28
+ } else if (Array.isArray(body.system)) {
29
+ const text = body.system.map((b) => b.text).join('\n')
30
+ messages.push({ role: 'system', content: text })
31
+ }
32
+ }
33
+
34
+ // Convert messages
35
+ for (const msg of body.messages) {
36
+ convertMessage(msg, messages, body.model)
37
+ }
38
+
39
+ // Build request
40
+ const result: OpenAIChatRequest = {
41
+ model: body.model,
42
+ messages,
43
+ stream: body.stream,
44
+ }
45
+
46
+ // max_tokens cap to avoid upstream 400 errors from Claude's high defaults (e.g. 64k).
47
+ // DeepSeek: tools/thinking fail above 8192. Other providers: 32768 covers most upstreams.
48
+ // GPT models (gpt-*): use max_completion_tokens instead of max_tokens (required by newer GPT models).
49
+ if (body.max_tokens !== undefined) {
50
+ const modelLower = body.model.toLowerCase()
51
+ if (modelLower.includes('deepseek')) {
52
+ result.max_tokens = Math.min(body.max_tokens, 8192)
53
+ } else if (modelLower.startsWith('gpt-') || modelLower.startsWith('o1') || modelLower.startsWith('o3') || modelLower.startsWith('o4')) {
54
+ result.max_completion_tokens = body.max_tokens
55
+ } else {
56
+ result.max_tokens = Math.min(body.max_tokens, 32768)
57
+ }
58
+ }
59
+
60
+ // temperature & top_p
61
+ if (body.temperature !== undefined) result.temperature = body.temperature
62
+ if (body.top_p !== undefined) result.top_p = body.top_p
63
+
64
+ // frequency_penalty: suppress repetition loops during multi-tool-call sequences.
65
+ // Anthropic API has no equivalent; inject for all OpenAI-compatible upstreams.
66
+ // Configurable via BINGO_FREQUENCY_PENALTY (default 0.1).
67
+ const fp = parseFloat(process.env.BINGO_FREQUENCY_PENALTY ?? '0.1')
68
+ if (!isNaN(fp) && fp !== 0) result.frequency_penalty = fp
69
+
70
+ // stop_sequences stop
71
+ if (body.stop_sequences && body.stop_sequences.length > 0) {
72
+ result.stop = body.stop_sequences
73
+ }
74
+
75
+ // tools
76
+ if (body.tools && body.tools.length > 0) {
77
+ result.tools = body.tools
78
+ .filter((t) => t.name !== 'BatchTool')
79
+ .map((t): OpenAITool => ({
80
+ type: 'function',
81
+ function: {
82
+ name: t.name,
83
+ description: t.description,
84
+ parameters: t.input_schema,
85
+ },
86
+ }))
87
+ }
88
+
89
+ // tool_choice
90
+ if (body.tool_choice !== undefined) {
91
+ result.tool_choice = convertToolChoice(body.tool_choice)
92
+ }
93
+
94
+ // thinking → reasoning_effort
95
+ if (body.thinking) {
96
+ const budget = body.thinking.budget_tokens
97
+ if (budget !== undefined) {
98
+ if (budget <= 1024) result.reasoning_effort = 'low'
99
+ else if (budget <= 8192) result.reasoning_effort = 'medium'
100
+ else result.reasoning_effort = 'high'
101
+ } else if (body.thinking.type === 'enabled') {
102
+ result.reasoning_effort = 'high'
103
+ }
104
+ }
105
+
106
+ return result
107
+ }
108
+
109
+ function convertMessage(msg: AnthropicMessage, output: OpenAIChatMessage[], model: string = ''): void {
110
+ const content = msg.content
111
+
112
+ // Simple string content
113
+ if (typeof content === 'string') {
114
+ output.push({ role: msg.role, content })
115
+ return
116
+ }
117
+
118
+ // Array content blocks
119
+ if (!Array.isArray(content) || content.length === 0) {
120
+ output.push({ role: msg.role, content: '' })
121
+ return
122
+ }
123
+
124
+ if (msg.role === 'user') {
125
+ convertUserMessage(content, output)
126
+ } else {
127
+ convertAssistantMessage(content, output, model)
128
+ }
129
+ }
130
+
131
+ function convertUserMessage(blocks: AnthropicContentBlock[], output: OpenAIChatMessage[]): void {
132
+ // Separate tool_result blocks from other content
133
+ const contentParts: OpenAIChatContentPart[] = []
134
+
135
+ for (const block of blocks) {
136
+ if (block.type === 'text') {
137
+ contentParts.push({ type: 'text', text: block.text })
138
+ } else if (block.type === 'image') {
139
+ const url = `data:${block.source.media_type};base64,${block.source.data}`
140
+ contentParts.push({ type: 'image_url', image_url: { url } })
141
+ } else if (block.type === 'tool_result') {
142
+ // tool_result → separate tool message
143
+ const rawContent = typeof block.content === 'string'
144
+ ? block.content
145
+ : Array.isArray(block.content)
146
+ ? block.content.filter((b): b is Extract<AnthropicContentBlock, { type: 'text' }> => b.type === 'text').map((b) => b.text).join('\n')
147
+ : ''
148
+ const resultContent = block.is_error
149
+ ? `<error>${rawContent}</error>`
150
+ : rawContent
151
+ output.push({
152
+ role: 'tool',
153
+ tool_call_id: block.tool_use_id,
154
+ content: resultContent,
155
+ })
156
+ }
157
+ }
158
+
159
+ if (contentParts.length > 0) {
160
+ output.push({
161
+ role: 'user',
162
+ content: contentParts.length === 1 && contentParts[0].type === 'text'
163
+ ? contentParts[0].text
164
+ : contentParts,
165
+ })
166
+ }
167
+ }
168
+
169
+ function convertAssistantMessage(blocks: AnthropicContentBlock[], output: OpenAIChatMessage[], model: string = ''): void {
170
+ let textContent = ''
171
+ let reasoningContent = ''
172
+ const toolCalls: OpenAIToolCall[] = []
173
+
174
+ for (const block of blocks) {
175
+ if (block.type === 'text') {
176
+ textContent += block.text
177
+ } else if (block.type === 'thinking') {
178
+ reasoningContent += block.thinking
179
+ } else if (block.type === 'tool_use') {
180
+ toolCalls.push({
181
+ id: block.id,
182
+ type: 'function',
183
+ function: {
184
+ name: block.name,
185
+ arguments: typeof block.input === 'string' ? block.input : JSON.stringify(block.input),
186
+ },
187
+ })
188
+ }
189
+ }
190
+
191
+ const msg: OpenAIChatMessage = {
192
+ role: 'assistant',
193
+ content: textContent || null,
194
+ }
195
+
196
+ // Only pass reasoning_content back for DeepSeek models to satisfy their mandatory back-transmission rule
197
+ if (reasoningContent && model.toLowerCase().includes('deepseek')) {
198
+ (msg as any).reasoning_content = reasoningContent
199
+ }
200
+
201
+ if (toolCalls.length > 0) {
202
+ msg.tool_calls = toolCalls
203
+ }
204
+
205
+ output.push(msg)
206
+ }
207
+
208
+ function convertToolChoice(choice: unknown): unknown {
209
+ if (typeof choice === 'string') return choice
210
+ if (typeof choice === 'object' && choice !== null) {
211
+ const c = choice as Record<string, unknown>
212
+ if (c.type === 'auto') return 'auto'
213
+ if (c.type === 'any') return 'required'
214
+ if (c.type === 'none') return 'none'
215
+ if (c.type === 'tool' && typeof c.name === 'string') {
216
+ return { type: 'function', function: { name: c.name } }
217
+ }
218
+ }
219
+ return 'auto'
220
+ }
@@ -524,6 +524,7 @@ export class ProviderService {
524
524
  apiFormat: ApiFormat
525
525
  modelId: string
526
526
  label?: string | null
527
+ extra?: Record<string, unknown>
527
528
  } | null> {
528
529
  const slots = await this.readSlots()
529
530
  const entry = slots[slotName]
@@ -542,6 +543,7 @@ export class ProviderService {
542
543
  apiFormat: provider.apiFormat ?? 'anthropic',
543
544
  modelId: entry.modelId,
544
545
  label: entry.label || CLAUDE_DEFAULT_LABELS[slotName],
546
+ ...(provider.extra !== undefined && { extra: provider.extra }),
545
547
  }
546
548
  }
547
549