bingocode 1.1.201 → 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/proxy/transform/anthropicToOpenaiChat.ts +220 -220
package/bin/bingo-win.cjs
CHANGED
|
@@ -1,215 +1,218 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const { spawn, spawnSync } = require('node:child_process');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const os = require('os');
|
|
6
|
-
const fs = require('fs');
|
|
7
|
-
|
|
8
|
-
process.env.NoDefaultCurrentDirectoryInExePath = '1';
|
|
9
|
-
|
|
10
|
-
// ── 首次部署:将默认 bingo 配置复制到 ~/.claude/bingo/ ──
|
|
11
|
-
/**
|
|
12
|
-
* 更加健壮的根目录定位:
|
|
13
|
-
* 1. 如果 preload.ts 在 ../ (当前 bin/ 目录下运行)
|
|
14
|
-
* 2. 否则查找同级及上级目录中的 package.json
|
|
15
|
-
*/
|
|
16
|
-
function getProjectRoot() {
|
|
17
|
-
let curr = __dirname;
|
|
18
|
-
try {
|
|
19
|
-
while (curr !== path.dirname(curr)) {
|
|
20
|
-
if (fs.existsSync(path.join(curr, 'preload.ts')) || fs.existsSync(path.join(curr, 'package.json'))) {
|
|
21
|
-
return curr;
|
|
22
|
-
}
|
|
23
|
-
const parent = path.dirname(curr);
|
|
24
|
-
if (fs.existsSync(path.join(parent, 'preload.ts'))) return parent;
|
|
25
|
-
curr = parent;
|
|
26
|
-
}
|
|
27
|
-
} catch (err) {
|
|
28
|
-
// 防止权限拒绝等导致挂死
|
|
29
|
-
}
|
|
30
|
-
return path.join(__dirname, '..');
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const ROOT_DIR = getProjectRoot();
|
|
34
|
-
|
|
35
|
-
(function deployBingoDefaults() {
|
|
36
|
-
const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
|
37
|
-
const bingoDir = path.join(configDir, 'bingo');
|
|
38
|
-
const targetSettings = path.join(bingoDir, 'settings.json');
|
|
39
|
-
|
|
40
|
-
// 只在 settings.json 不存在时才部署
|
|
41
|
-
if (!fs.existsSync(targetSettings)) {
|
|
42
|
-
const defaultsDir = path.join(ROOT_DIR, 'config', 'bingo-defaults');
|
|
43
|
-
const srcSettings = path.join(defaultsDir, 'settings.json');
|
|
44
|
-
|
|
45
|
-
if (fs.existsSync(srcSettings)) {
|
|
46
|
-
try {
|
|
47
|
-
if (!fs.existsSync(bingoDir)) {
|
|
48
|
-
fs.mkdirSync(bingoDir, { recursive: true });
|
|
49
|
-
}
|
|
50
|
-
fs.copyFileSync(srcSettings, targetSettings);
|
|
51
|
-
console.log('[bingo] 首次启动:已部署默认配置到', targetSettings);
|
|
52
|
-
} catch (err) {
|
|
53
|
-
console.warn('[bingo] 部署默认配置失败:', err.message);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
})();
|
|
58
|
-
|
|
59
|
-
// 自动定位 bun.exe(纯文件系统查找,无子进程,无 DEP0190 警告)
|
|
60
|
-
function resolveBunExe() {
|
|
61
|
-
// 1. 用户指定路径
|
|
62
|
-
if (process.env.BUN_PATH && fs.existsSync(process.env.BUN_PATH)) {
|
|
63
|
-
return process.env.BUN_PATH;
|
|
64
|
-
}
|
|
65
|
-
const home = os.homedir();
|
|
66
|
-
const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
|
|
67
|
-
const candidates = [
|
|
68
|
-
// npm install -g bun 的真实 exe(最常见)
|
|
69
|
-
path.join(appData, 'npm', 'node_modules', 'bun', 'bin', 'bun.exe'),
|
|
70
|
-
// bun 官方安装脚本位置
|
|
71
|
-
path.join(home, '.bun', 'bin', 'bun.exe'),
|
|
72
|
-
];
|
|
73
|
-
// 遍历 PATH 中每个目录查找 bun.exe
|
|
74
|
-
for (const dir of (process.env.PATH || '').split(path.delimiter)) {
|
|
75
|
-
candidates.push(path.join(dir, 'bun.exe'));
|
|
76
|
-
}
|
|
77
|
-
for (const c of candidates) {
|
|
78
|
-
try { if (fs.existsSync(c)) return c; } catch (_) {}
|
|
79
|
-
}
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// 检查 bun 是否可用
|
|
84
|
-
function bunExists() {
|
|
85
|
-
return resolveBunExe() !== null;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// 安装 bun(通过 npm install -g bun)
|
|
89
|
-
function installBun() {
|
|
90
|
-
console.log('[bingocode] bun 未检测到,正在通过 npm install -g bun 安装...');
|
|
91
|
-
|
|
92
|
-
try {
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
console.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
//
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const
|
|
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
|
-
const
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { spawn, spawnSync } = require('node:child_process');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
|
|
8
|
+
process.env.NoDefaultCurrentDirectoryInExePath = '1';
|
|
9
|
+
|
|
10
|
+
// ── 首次部署:将默认 bingo 配置复制到 ~/.claude/bingo/ ──
|
|
11
|
+
/**
|
|
12
|
+
* 更加健壮的根目录定位:
|
|
13
|
+
* 1. 如果 preload.ts 在 ../ (当前 bin/ 目录下运行)
|
|
14
|
+
* 2. 否则查找同级及上级目录中的 package.json
|
|
15
|
+
*/
|
|
16
|
+
function getProjectRoot() {
|
|
17
|
+
let curr = __dirname;
|
|
18
|
+
try {
|
|
19
|
+
while (curr !== path.dirname(curr)) {
|
|
20
|
+
if (fs.existsSync(path.join(curr, 'preload.ts')) || fs.existsSync(path.join(curr, 'package.json'))) {
|
|
21
|
+
return curr;
|
|
22
|
+
}
|
|
23
|
+
const parent = path.dirname(curr);
|
|
24
|
+
if (fs.existsSync(path.join(parent, 'preload.ts'))) return parent;
|
|
25
|
+
curr = parent;
|
|
26
|
+
}
|
|
27
|
+
} catch (err) {
|
|
28
|
+
// 防止权限拒绝等导致挂死
|
|
29
|
+
}
|
|
30
|
+
return path.join(__dirname, '..');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const ROOT_DIR = getProjectRoot();
|
|
34
|
+
|
|
35
|
+
(function deployBingoDefaults() {
|
|
36
|
+
const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
|
37
|
+
const bingoDir = path.join(configDir, 'bingo');
|
|
38
|
+
const targetSettings = path.join(bingoDir, 'settings.json');
|
|
39
|
+
|
|
40
|
+
// 只在 settings.json 不存在时才部署
|
|
41
|
+
if (!fs.existsSync(targetSettings)) {
|
|
42
|
+
const defaultsDir = path.join(ROOT_DIR, 'config', 'bingo-defaults');
|
|
43
|
+
const srcSettings = path.join(defaultsDir, 'settings.json');
|
|
44
|
+
|
|
45
|
+
if (fs.existsSync(srcSettings)) {
|
|
46
|
+
try {
|
|
47
|
+
if (!fs.existsSync(bingoDir)) {
|
|
48
|
+
fs.mkdirSync(bingoDir, { recursive: true });
|
|
49
|
+
}
|
|
50
|
+
fs.copyFileSync(srcSettings, targetSettings);
|
|
51
|
+
console.log('[bingo] 首次启动:已部署默认配置到', targetSettings);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
console.warn('[bingo] 部署默认配置失败:', err.message);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
})();
|
|
58
|
+
|
|
59
|
+
// 自动定位 bun.exe(纯文件系统查找,无子进程,无 DEP0190 警告)
|
|
60
|
+
function resolveBunExe() {
|
|
61
|
+
// 1. 用户指定路径
|
|
62
|
+
if (process.env.BUN_PATH && fs.existsSync(process.env.BUN_PATH)) {
|
|
63
|
+
return process.env.BUN_PATH;
|
|
64
|
+
}
|
|
65
|
+
const home = os.homedir();
|
|
66
|
+
const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
|
|
67
|
+
const candidates = [
|
|
68
|
+
// npm install -g bun 的真实 exe(最常见)
|
|
69
|
+
path.join(appData, 'npm', 'node_modules', 'bun', 'bin', 'bun.exe'),
|
|
70
|
+
// bun 官方安装脚本位置
|
|
71
|
+
path.join(home, '.bun', 'bin', 'bun.exe'),
|
|
72
|
+
];
|
|
73
|
+
// 遍历 PATH 中每个目录查找 bun.exe
|
|
74
|
+
for (const dir of (process.env.PATH || '').split(path.delimiter)) {
|
|
75
|
+
candidates.push(path.join(dir, 'bun.exe'));
|
|
76
|
+
}
|
|
77
|
+
for (const c of candidates) {
|
|
78
|
+
try { if (fs.existsSync(c)) return c; } catch (_) {}
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 检查 bun 是否可用
|
|
84
|
+
function bunExists() {
|
|
85
|
+
return resolveBunExe() !== null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 安装 bun(通过 npm install -g bun)
|
|
89
|
+
function installBun() {
|
|
90
|
+
console.log('[bingocode] bun 未检测到,正在通过 npm install -g bun 安装...');
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const installArgs = ['install', '-g', 'bun', '--allow-scripts=bun', '--loglevel', 'error'];
|
|
94
|
+
const npmResult = process.platform === 'win32'
|
|
95
|
+
? spawnSync(
|
|
96
|
+
process.env.ComSpec || 'cmd.exe',
|
|
97
|
+
['/d', '/s', '/c', 'npm ' + installArgs.join(' ')],
|
|
98
|
+
{ stdio: 'inherit' }
|
|
99
|
+
)
|
|
100
|
+
: spawnSync('npm', installArgs, { stdio: 'inherit' });
|
|
101
|
+
if (npmResult.error || npmResult.status !== 0) {
|
|
102
|
+
throw new Error(`npm install -g bun 失败,exit code ${npmResult.status ?? npmResult.error?.message}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
console.log('[bingocode] bun 安装完成,正在启动...');
|
|
106
|
+
return true;
|
|
107
|
+
} catch (err) {
|
|
108
|
+
console.error(`[bingocode] bun 自动安装失败: ${err.message}`);
|
|
109
|
+
console.log('[bingocode] 请手动安装 bun: npm install -g bun');
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!bunExists()) {
|
|
115
|
+
if (!installBun()) {
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 安装完成后重新解析 bun 路径
|
|
121
|
+
const bunExe = resolveBunExe();
|
|
122
|
+
if (!bunExe) {
|
|
123
|
+
console.error('[bingocode] 安装后仍找不到 bun.exe,请重新打开终端后再试,或手动安装 bun: npm install -g bun');
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Bingo Manager 入口
|
|
128
|
+
const entry = path.join(ROOT_DIR, 'src', 'entrypoints', 'manager.tsx');
|
|
129
|
+
|
|
130
|
+
// preload shim
|
|
131
|
+
const preload = path.join(ROOT_DIR, 'preload.ts');
|
|
132
|
+
if (!fs.existsSync(preload)) {
|
|
133
|
+
console.error('[bingocode] 找不到 preload.ts,MACRO 将无法注入:' + preload);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 检查 .env
|
|
138
|
+
let envFlag = '';
|
|
139
|
+
const envPath = path.join(ROOT_DIR, '.env');
|
|
140
|
+
if (fs.existsSync(envPath)) {
|
|
141
|
+
envFlag = `--env-file=${envPath}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const extraArgs = process.argv.slice(2);
|
|
145
|
+
|
|
146
|
+
// ── Start tray daemon if not already running ────────────────────────────────
|
|
147
|
+
const RUNTIME_DIR = path.join(os.homedir(), '.claude-cli', 'runtime');
|
|
148
|
+
const DAEMON_LOCK_FILE = path.join(RUNTIME_DIR, 'daemon.lock');
|
|
149
|
+
|
|
150
|
+
function serverHealthy() {
|
|
151
|
+
return new Promise((resolve) => {
|
|
152
|
+
const http = require('http');
|
|
153
|
+
const req = http.get('http://127.0.0.1:3456/health', { timeout: 1000 }, (res) => {
|
|
154
|
+
let data = '';
|
|
155
|
+
res.on('data', (c) => (data += c));
|
|
156
|
+
res.on('end', () => {
|
|
157
|
+
try {
|
|
158
|
+
resolve(res.statusCode === 200 && JSON.parse(data).status === 'ok');
|
|
159
|
+
} catch {
|
|
160
|
+
resolve(false);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
req.on('error', () => resolve(false));
|
|
165
|
+
req.setTimeout(1000, () => { req.destroy(); resolve(false); });
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function isDaemonAlive() {
|
|
170
|
+
try {
|
|
171
|
+
const pid = parseInt(fs.readFileSync(DAEMON_LOCK_FILE, 'utf-8').trim(), 10);
|
|
172
|
+
process.kill(pid, 0); // signal 0 == probe only
|
|
173
|
+
return true;
|
|
174
|
+
} catch {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const CHECK_MS = 3000;
|
|
180
|
+
|
|
181
|
+
async function startTrayDaemonIfNeeded() {
|
|
182
|
+
// Daemon PID lock prevents race: only one daemon runs across multiple bingo launches
|
|
183
|
+
if (isDaemonAlive()) {
|
|
184
|
+
console.log('[bingo] Daemon already running, skipping daemon start');
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// stale lock cleanup
|
|
189
|
+
try { fs.unlinkSync(DAEMON_LOCK_FILE); } catch {}
|
|
190
|
+
|
|
191
|
+
console.log('[bingo] Starting tray daemon...');
|
|
192
|
+
const trayEntry = path.join(ROOT_DIR, 'src', 'entrypoints', 'tray-only.ts');
|
|
193
|
+
const daemon = spawn(bunExe, ['--preload=' + preload, trayEntry], {
|
|
194
|
+
detached: true,
|
|
195
|
+
stdio: 'ignore',
|
|
196
|
+
env: { ...process.env },
|
|
197
|
+
});
|
|
198
|
+
daemon.unref();
|
|
199
|
+
|
|
200
|
+
// Wait for health check
|
|
201
|
+
const start = Date.now();
|
|
202
|
+
while (Date.now() - start < CHECK_MS) {
|
|
203
|
+
if (await serverHealthy()) {
|
|
204
|
+
console.log('[bingo] Tray daemon started successfully');
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
await (new Promise((r) => setTimeout(r, 300)));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Start daemon, then launch CLI independently
|
|
212
|
+
startTrayDaemonIfNeeded().catch(() => {});
|
|
213
|
+
|
|
214
|
+
// ── Launch CLI (connects to existing server) ───────────────────────────────
|
|
215
|
+
const args = [`--preload=${preload}`, envFlag, entry, ...extraArgs].filter(Boolean);
|
|
216
|
+
|
|
217
|
+
// 用绝对路径 spawn,不依赖 shell 解析 PATH
|
|
218
|
+
const child = spawn(bunExe, args, { stdio: 'inherit' });
|
package/package.json
CHANGED
|
@@ -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
|
+
}
|