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
|
@@ -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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
|