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.
- package/bin/bingo-win.cjs +218 -215
- package/package.json +1 -1
- package/src/server/config/providers.yaml +229 -229
- package/src/server/proxy/transform/anthropicToOpenaiChat.ts +220 -220
- package/src/server/services/providerService.ts +29 -6
- package/src/tools/AgentTool/UI.tsx +73 -8
- package/src/utils/context.ts +1 -1
|
@@ -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
|
|
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
|
-
}
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
}
|