bingocode 1.1.200 → 1.1.202-beta.1

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.
@@ -1,220 +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
- * 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
- }
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, opt-in).
67
+ const fp = parseFloat(process.env.BINGO_FREQUENCY_PENALTY ?? '0')
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
+ }
@@ -9,6 +9,7 @@
9
9
  import * as fs from 'fs/promises'
10
10
  import * as path from 'path'
11
11
  import * as os from 'os'
12
+ import { parse as parseJsonc } from 'jsonc-parser/lib/esm/main.js'
12
13
  import { getDirectFetchOptions } from '../../utils/proxy.ts'
13
14
  import { ApiError } from '../middleware/errorHandler.js'
14
15
  import { anthropicToOpenaiChat } from '../proxy/transform/anthropicToOpenaiChat.js'
@@ -268,6 +269,18 @@ export class ProviderService {
268
269
  // --- Settings sync ---
269
270
 
270
271
  private async syncToSettings(provider: SavedProvider): Promise<void> {
272
+ // Prefer slot-based model IDs when slots are configured, so that the
273
+ // slot table (set via "Configure Model Slots" in the TUI) is the single
274
+ // source of truth. Falling back to provider.models only when no slots
275
+ // exist keeps things working for users who haven't configured slots yet.
276
+ const slots = await this.readSlots()
277
+ const hasSlots = Object.keys(slots).length > 0
278
+ if (hasSlots) {
279
+ await this.syncSettingsForSlots(slots)
280
+ return
281
+ }
282
+
283
+ // Legacy fallback: no slots configured — use provider.models directly
271
284
  const settings = await this.readSettings()
272
285
  const existingEnv = (settings.env as Record<string, string>) || {}
273
286
 
@@ -620,7 +633,9 @@ export class ProviderService {
620
633
  let originalContent: string | null = null
621
634
  try {
622
635
  originalContent = await fs.readFile(settingsPath, "utf-8")
623
- settings = JSON.parse(originalContent)
636
+ // VS Code settings.json uses JSONC (JSON with comments) —
637
+ // parseJsonc handles comments/trailing commas, unlike JSON.parse
638
+ settings = parseJsonc(originalContent) as Record<string, unknown>
624
639
  } catch {
625
640
  // File does not exist yet — start fresh
626
641
  }
@@ -650,7 +665,9 @@ export class ProviderService {
650
665
 
651
666
  try {
652
667
  await fs.mkdir(path.dirname(settingsPath), { recursive: true })
653
- await fs.writeFile(settingsPath, JSON.stringify(settings, null, 4) + '\n')
668
+ const tmpPath = `${settingsPath}.tmp.${Date.now()}`
669
+ await fs.writeFile(tmpPath, JSON.stringify(settings, null, 4) + '\n')
670
+ await fs.rename(tmpPath, settingsPath)
654
671
  writtenPaths.push(settingsPath)
655
672
  } catch (err) {
656
673
  failedPaths.push(settingsPath)
@@ -661,7 +678,9 @@ export class ProviderService {
661
678
  // Fix #11: Attempt restore from backup on write failure
662
679
  if (originalContent !== null) {
663
680
  try {
664
- await fs.writeFile(settingsPath, originalContent, "utf-8")
681
+ const tmpPath2 = `${settingsPath}.tmp.${Date.now()}`
682
+ await fs.writeFile(tmpPath2, originalContent, "utf-8")
683
+ await fs.rename(tmpPath2, settingsPath)
665
684
  console.error(`[ProviderService] Restored ${settingsPath} from in-memory copy after write failure`)
666
685
  } catch {
667
686
  // In-memory restore failed, try .bak file
@@ -694,7 +713,9 @@ export class ProviderService {
694
713
  const original = originalContents.get(p)
695
714
  try {
696
715
  if (original !== null) {
697
- await fs.writeFile(p, original, "utf-8")
716
+ const tmpPath = `${p}.tmp.${Date.now()}`
717
+ await fs.writeFile(tmpPath, original, "utf-8")
718
+ await fs.rename(tmpPath, p)
698
719
  } else {
699
720
  await fs.unlink(p).catch(() => {})
700
721
  }
@@ -754,14 +775,16 @@ export class ProviderService {
754
775
  for (const settingsPath of linkedPaths) {
755
776
  try {
756
777
  const raw = await fs.readFile(settingsPath, "utf-8")
757
- const settings = JSON.parse(raw)
778
+ const settings = parseJsonc(raw) as Record<string, unknown>
758
779
  settings.claudeCode = preLinkClaudeCode[settingsPath] ?? {
759
780
  preferredLocation: "panel",
760
781
  environmentVariables: [
761
782
  { name: "ANTHROPIC_BASE_URL", value: "https://api.anthropic.com" },
762
783
  ],
763
784
  }
764
- await fs.writeFile(settingsPath, JSON.stringify(settings, null, 4) + '\n')
785
+ const tmpPath = `${settingsPath}.tmp.${Date.now()}`
786
+ await fs.writeFile(tmpPath, JSON.stringify(settings, null, 4) + '\n')
787
+ await fs.rename(tmpPath, settingsPath)
765
788
  } catch {
766
789
  // File may have been moved or deleted since linking — harmless
767
790
  }