@autotask/atools-tool 0.1.2 → 0.1.3

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/README.md CHANGED
@@ -15,20 +15,39 @@ atools-tool <command>
15
15
  Available command:
16
16
 
17
17
  ```bash
18
- atools-tool config-openclaw
18
+ atools-tool openclaw
19
19
  ```
20
20
 
21
21
  Interactive flow:
22
22
 
23
- 1. Select default model: `gpt-5.2` / `gpt-5.3-codex` / `gpt-5.4`
24
- 2. Configure reasoning: `off` / `on`
25
- 3. Configure thinking strength: `off|minimal|low|medium|high|xhigh`
26
- 4. Input API key (leave empty to keep current key)
23
+ 1. Check local proxy status (`127.0.0.1:18888`); if not running, ask whether to start it first
24
+ 2. Select default model: `gpt-5.2` / `gpt-5.3-codex` / `gpt-5.4`
25
+ 3. Configure reasoning: `off` / `on`
26
+ 4. Configure thinking strength: `off|minimal|low|medium|high|xhigh`
27
+ 5. Input API key (leave empty to keep current key)
28
+
29
+ Selection UX:
30
+
31
+ - TTY terminal: use `↑/↓` and `Enter`
32
+ - Non-TTY: falls back to number input
33
+
34
+ Rollback to codex-compatible defaults:
35
+
36
+ ```bash
37
+ atools-tool openclaw
38
+ ```
39
+
40
+ Recommended selections:
41
+
42
+ 1. `gpt-5.3-codex`
43
+ 2. `off`
44
+ 3. `low`
45
+ 4. Input your sub2api-compatible key
27
46
 
28
47
  ## Install (npm)
29
48
 
30
49
  ```bash
31
- npm i -g atools-tool
50
+ npm i -g @autotask/atools-tool
32
51
  ```
33
52
 
34
53
  Optional installer command:
@@ -37,6 +56,12 @@ Optional installer command:
37
56
  atools-tool-install --help
38
57
  ```
39
58
 
59
+ `atools-tool-install` service behavior by platform:
60
+
61
+ - Linux: systemd user service
62
+ - macOS: launchd user agent (`~/Library/LaunchAgents`)
63
+ - Windows: Task Scheduler user task (ONLOGON + start now)
64
+
40
65
  ## Proxy Command
41
66
 
42
67
  ```bash
@@ -62,8 +87,11 @@ curl -fsSL https://raw.githubusercontent.com/aak1247/sub2api-openclaw-proxy/main
62
87
  - patches `~/.openclaw/openclaw.json`
63
88
  - patches `~/.openclaw/agents/main/agent/models.json`
64
89
  - patches `~/.openclaw/agents/main/agent/auth-profiles.json` (if API key is available)
65
- - creates/enables `~/.config/systemd/user/openclaw-atools-proxy.service`
66
- - restarts `openclaw-gateway.service` (unless `--no-restart`)
90
+ - service install by platform:
91
+ - Linux: creates/enables `~/.config/systemd/user/openclaw-atools-proxy.service`
92
+ - macOS: creates/enables `~/Library/LaunchAgents/openclaw-atools-proxy.plist`
93
+ - Windows: creates/updates scheduled task `openclaw-atools-proxy`
94
+ - restarts `openclaw-gateway.service` only on Linux (unless `--no-restart`)
67
95
 
68
96
  Default proxy log:
69
97
 
package/bin/cli.mjs CHANGED
@@ -47,7 +47,8 @@ Usage:
47
47
 
48
48
  Commands:
49
49
  serve Run proxy service
50
- config-openclaw Interactive OpenClaw model/reasoning/thinking/key setup
50
+ openclaw Interactive OpenClaw setup (sub2api proxy check + model/reasoning/thinking/key)
51
+ config-openclaw Alias of 'openclaw'
51
52
 
52
53
  Options:
53
54
  --openclaw-home <path> OpenClaw home dir (default: ~/.openclaw)
@@ -71,7 +72,7 @@ async function main() {
71
72
  return;
72
73
  }
73
74
 
74
- if (args.cmd === 'config-openclaw') {
75
+ if (args.cmd === 'config-openclaw' || args.cmd === 'openclaw') {
75
76
  await runConfigOpenclaw({ openclawHome: args.openclawHome });
76
77
  return;
77
78
  }
@@ -2,12 +2,23 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import os from 'node:os';
4
4
  import process from 'node:process';
5
- import readline from 'node:readline/promises';
5
+ import net from 'node:net';
6
+ import { spawn, spawnSync } from 'node:child_process';
7
+ import { fileURLToPath } from 'node:url';
8
+ import readlinePromises from 'node:readline/promises';
9
+ import readline from 'node:readline';
6
10
 
7
11
  const PROVIDER = 'sub2api';
8
12
  const MODEL_OPTIONS = ['gpt-5.2', 'gpt-5.3-codex', 'gpt-5.4'];
9
13
  const THINKING_OPTIONS = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh'];
10
14
 
15
+ const PROXY_HOST = '127.0.0.1';
16
+ const PROXY_PORT = 18888;
17
+ const PROXY_BASE_URL = `http://${PROXY_HOST}:${PROXY_PORT}/v1`;
18
+ const PROXY_UPSTREAM = 'https://sub2api.atools.live';
19
+ const PROXY_LOG_FILE = path.join(os.tmpdir(), 'openclaw', 'atools-compat-proxy.log');
20
+ const PROXY_SERVICE = 'openclaw-atools-proxy.service';
21
+
11
22
  function readJson(filePath) {
12
23
  if (!fs.existsSync(filePath)) {
13
24
  throw new Error(`missing file: ${filePath}`);
@@ -28,7 +39,19 @@ function backup(filePath) {
28
39
  fs.copyFileSync(filePath, `${filePath}.bak.${stamp}`);
29
40
  }
30
41
 
31
- async function askChoice(rl, question, options, defaultIndex = 0) {
42
+ function sleep(ms) {
43
+ return new Promise((resolve) => setTimeout(resolve, ms));
44
+ }
45
+
46
+ function canUseArrowMenu() {
47
+ return Boolean(
48
+ process.stdin.isTTY
49
+ && process.stdout.isTTY
50
+ && typeof process.stdin.setRawMode === 'function'
51
+ );
52
+ }
53
+
54
+ async function askChoiceNumber(rl, question, options, defaultIndex = 0) {
32
55
  while (true) {
33
56
  process.stdout.write(`\n${question}\n`);
34
57
  options.forEach((item, idx) => {
@@ -45,14 +68,153 @@ async function askChoice(rl, question, options, defaultIndex = 0) {
45
68
  }
46
69
  }
47
70
 
71
+ async function askChoiceArrow(question, options, defaultIndex = 0) {
72
+ return new Promise((resolve, reject) => {
73
+ let index = defaultIndex;
74
+ process.stdout.write(`\n${question}\n`);
75
+
76
+ const draw = () => {
77
+ readline.clearLine(process.stdout, 0);
78
+ readline.cursorTo(process.stdout, 0);
79
+ process.stdout.write(`> ${options[index]} (↑/↓, Enter)`);
80
+ };
81
+
82
+ const cleanup = () => {
83
+ process.stdin.off('keypress', onKeyPress);
84
+ if (process.stdin.isTTY && typeof process.stdin.setRawMode === 'function') {
85
+ process.stdin.setRawMode(false);
86
+ }
87
+ };
88
+
89
+ const onKeyPress = (str, key = {}) => {
90
+ if (key.ctrl && key.name === 'c') {
91
+ cleanup();
92
+ reject(new Error('aborted by user'));
93
+ return;
94
+ }
95
+
96
+ if (key.name === 'up') {
97
+ index = (index - 1 + options.length) % options.length;
98
+ draw();
99
+ return;
100
+ }
101
+ if (key.name === 'down') {
102
+ index = (index + 1) % options.length;
103
+ draw();
104
+ return;
105
+ }
106
+ if (key.name === 'return' || key.name === 'enter') {
107
+ cleanup();
108
+ readline.clearLine(process.stdout, 0);
109
+ readline.cursorTo(process.stdout, 0);
110
+ process.stdout.write(`已选择: ${options[index]}\n`);
111
+ resolve(options[index]);
112
+ return;
113
+ }
114
+
115
+ const n = Number(str);
116
+ if (Number.isInteger(n) && n >= 1 && n <= options.length) {
117
+ index = n - 1;
118
+ draw();
119
+ }
120
+ };
121
+
122
+ try {
123
+ readline.emitKeypressEvents(process.stdin);
124
+ process.stdin.setRawMode(true);
125
+ process.stdin.resume();
126
+ process.stdin.on('keypress', onKeyPress);
127
+ draw();
128
+ } catch (err) {
129
+ cleanup();
130
+ reject(err);
131
+ }
132
+ });
133
+ }
134
+
135
+ async function askChoice(rl, question, options, defaultIndex = 0) {
136
+ if (!canUseArrowMenu()) {
137
+ return askChoiceNumber(rl, question, options, defaultIndex);
138
+ }
139
+ return askChoiceArrow(question, options, defaultIndex);
140
+ }
141
+
142
+ async function askInputArrow(defaultValue = '') {
143
+ return new Promise((resolve, reject) => {
144
+ let value = '';
145
+ process.stdout.write('请输入 key: ');
146
+
147
+ const draw = () => {
148
+ readline.clearLine(process.stdout, 1);
149
+ readline.cursorTo(process.stdout, 10);
150
+ process.stdout.write(value);
151
+ };
152
+
153
+ const cleanup = () => {
154
+ process.stdin.off('keypress', onKeyPress);
155
+ if (process.stdin.isTTY && typeof process.stdin.setRawMode === 'function') {
156
+ process.stdin.setRawMode(false);
157
+ }
158
+ };
159
+
160
+ const onKeyPress = (str, key = {}) => {
161
+ if (key.ctrl && key.name === 'c') {
162
+ cleanup();
163
+ reject(new Error('aborted by user'));
164
+ return;
165
+ }
166
+ if (key.name === 'return' || key.name === 'enter') {
167
+ cleanup();
168
+ process.stdout.write('\n');
169
+ resolve(value.trim() || defaultValue || '');
170
+ return;
171
+ }
172
+ if (key.name === 'backspace') {
173
+ value = value.slice(0, -1);
174
+ draw();
175
+ return;
176
+ }
177
+ if (!key.ctrl && !key.meta && typeof str === 'string' && str.length > 0) {
178
+ value += str;
179
+ draw();
180
+ }
181
+ };
182
+
183
+ try {
184
+ readline.emitKeypressEvents(process.stdin);
185
+ process.stdin.setRawMode(true);
186
+ process.stdin.resume();
187
+ process.stdin.on('keypress', onKeyPress);
188
+ } catch (err) {
189
+ cleanup();
190
+ reject(err);
191
+ }
192
+ });
193
+ }
194
+
48
195
  async function askKey(rl, currentKey) {
49
196
  const masked = currentKey ? `${currentKey.slice(0, 6)}...${currentKey.slice(-4)}` : '未设置';
50
- process.stdout.write(`\n4) 输入 API Key(当前: ${masked})\n`);
197
+ const question = `4) 输入 API Key(当前: ${masked})`;
198
+ process.stdout.write(`\n${question}\n`);
51
199
  process.stdout.write('留空表示保持当前 key\n');
200
+ if (canUseArrowMenu()) {
201
+ return askInputArrow(currentKey);
202
+ }
52
203
  const input = (await rl.question('请输入 key: ')).trim();
53
204
  return input || currentKey || '';
54
205
  }
55
206
 
207
+ async function askKeyWithGuard(rl, currentKey) {
208
+ let selectedKey = await askKey(rl, currentKey);
209
+ while (/^sk-or-v1-/i.test(selectedKey)) {
210
+ process.stdout.write('\n检测到你输入的是 OpenRouter key(sk-or-v1- 开头),这不适用于 sub2api /v1 代理模式。\n');
211
+ const choice = await askChoice(rl, '是否重新输入 key?', ['重新输入 key(推荐)', '继续使用当前 key'], 0);
212
+ if (choice === '继续使用当前 key') break;
213
+ selectedKey = await askKey(rl, selectedKey);
214
+ }
215
+ return selectedKey;
216
+ }
217
+
56
218
  function ensureModel(providerObj, modelId, reasoning) {
57
219
  providerObj.models = providerObj.models || [];
58
220
  let model = providerObj.models.find((m) => m && m.id === modelId);
@@ -75,6 +237,190 @@ function ensureModel(providerObj, modelId, reasoning) {
75
237
  model.api = model.api || 'openai-responses';
76
238
  }
77
239
 
240
+ function escapeRegExp(text) {
241
+ return String(text).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
242
+ }
243
+
244
+ function upsertEnvLine(content, key, value) {
245
+ const line = `Environment=${key}=${value}`;
246
+ const re = new RegExp(`^Environment=${escapeRegExp(key)}=.*$`, 'm');
247
+ if (re.test(content)) {
248
+ return content.replace(re, line);
249
+ }
250
+ const serviceIdx = content.indexOf('[Service]');
251
+ if (serviceIdx === -1) {
252
+ return `${content.trimEnd()}\n${line}\n`;
253
+ }
254
+ const afterServiceHeader = content.indexOf('\n', serviceIdx);
255
+ if (afterServiceHeader < 0) {
256
+ return `${content}\n${line}\n`;
257
+ }
258
+ return `${content.slice(0, afterServiceHeader + 1)}${line}\n${content.slice(afterServiceHeader + 1)}`;
259
+ }
260
+
261
+ function configureLinuxProxyService() {
262
+ const servicePath = path.join(os.homedir(), '.config/systemd/user', PROXY_SERVICE);
263
+ if (!fs.existsSync(servicePath)) {
264
+ return { exists: false, changed: false, servicePath };
265
+ }
266
+ const original = fs.readFileSync(servicePath, 'utf8');
267
+ let next = original;
268
+ next = upsertEnvLine(next, 'SUB2API_UPSTREAM', PROXY_UPSTREAM);
269
+ next = upsertEnvLine(next, 'SUB2API_FALLBACK_UPSTREAM', '');
270
+ next = upsertEnvLine(next, 'SUB2API_FALLBACK_API_KEY', '');
271
+ next = upsertEnvLine(next, 'SUB2API_COMPAT_DROP_TOOLS_ON_COMPACT', '0');
272
+ next = upsertEnvLine(next, 'SUB2API_COMPAT_PORT', String(PROXY_PORT));
273
+ next = upsertEnvLine(next, 'SUB2API_COMPAT_LOG', PROXY_LOG_FILE);
274
+
275
+ if (next === original) {
276
+ return { exists: true, changed: false, servicePath };
277
+ }
278
+ backup(servicePath);
279
+ fs.writeFileSync(servicePath, next, 'utf8');
280
+ return { exists: true, changed: true, servicePath };
281
+ }
282
+
283
+ function runSystemctl(args) {
284
+ const out = spawnSync('systemctl', ['--user', ...args], { encoding: 'utf8' });
285
+ return out;
286
+ }
287
+
288
+ function isLinuxProxyServiceActive() {
289
+ const out = runSystemctl(['is-active', PROXY_SERVICE]);
290
+ return out.status === 0 && String(out.stdout || '').trim() === 'active';
291
+ }
292
+
293
+ function startLinuxProxyService() {
294
+ const reload = runSystemctl(['daemon-reload']);
295
+ if (reload.status !== 0) return false;
296
+ const start = runSystemctl(['enable', '--now', PROXY_SERVICE]);
297
+ return start.status === 0;
298
+ }
299
+
300
+ function startDetachedProxy() {
301
+ try {
302
+ const cliPath = fileURLToPath(new URL('../bin/cli.mjs', import.meta.url));
303
+ const child = spawn(
304
+ process.execPath,
305
+ [
306
+ cliPath,
307
+ 'serve',
308
+ '--port',
309
+ String(PROXY_PORT),
310
+ '--upstream',
311
+ PROXY_UPSTREAM,
312
+ '--log-file',
313
+ PROXY_LOG_FILE
314
+ ],
315
+ {
316
+ detached: true,
317
+ stdio: 'ignore',
318
+ env: {
319
+ ...process.env,
320
+ SUB2API_UPSTREAM: PROXY_UPSTREAM,
321
+ SUB2API_FALLBACK_UPSTREAM: '',
322
+ SUB2API_FALLBACK_API_KEY: ''
323
+ }
324
+ }
325
+ );
326
+ child.unref();
327
+ return true;
328
+ } catch {
329
+ return false;
330
+ }
331
+ }
332
+
333
+ async function isPortOpen(host, port, timeoutMs = 900) {
334
+ return new Promise((resolve) => {
335
+ const socket = net.createConnection({ host, port });
336
+ let done = false;
337
+
338
+ const finish = (ok) => {
339
+ if (done) return;
340
+ done = true;
341
+ socket.destroy();
342
+ resolve(ok);
343
+ };
344
+
345
+ socket.on('connect', () => finish(true));
346
+ socket.on('error', () => finish(false));
347
+ socket.setTimeout(timeoutMs, () => finish(false));
348
+ });
349
+ }
350
+
351
+ async function ensureProxyReady() {
352
+ if (process.platform === 'linux') {
353
+ const service = configureLinuxProxyService();
354
+ if (service.exists && service.changed) {
355
+ // Service file changed: reload + restart so it points back to sub2api.atools.live.
356
+ startLinuxProxyService();
357
+ await sleep(400);
358
+ }
359
+ }
360
+
361
+ if (await isPortOpen(PROXY_HOST, PROXY_PORT)) return true;
362
+
363
+ // Linux fallback: service may exist but not running.
364
+ if (process.platform === 'linux') {
365
+ const service = configureLinuxProxyService();
366
+ if (service.exists && isLinuxProxyServiceActive()) {
367
+ if (await isPortOpen(PROXY_HOST, PROXY_PORT)) return true;
368
+ }
369
+ }
370
+
371
+ process.stdout.write('\n检测到本地代理未运行(127.0.0.1:18888)。\n');
372
+ const promptRl = readlinePromises.createInterface({
373
+ input: process.stdin,
374
+ output: process.stdout
375
+ });
376
+ let action = '';
377
+ try {
378
+ action = await askChoice(
379
+ promptRl,
380
+ 'atools 模型依赖本地代理,是否现在启动代理?',
381
+ ['启动代理(推荐)', '取消并退出'],
382
+ 0
383
+ );
384
+ } finally {
385
+ promptRl.close();
386
+ }
387
+
388
+ if (action === '取消并退出') {
389
+ return false;
390
+ }
391
+
392
+ let started = false;
393
+ if (process.platform === 'linux') {
394
+ started = startLinuxProxyService();
395
+ }
396
+ if (!started) {
397
+ started = startDetachedProxy();
398
+ }
399
+
400
+ if (!started) return false;
401
+
402
+ for (let i = 0; i < 5; i += 1) {
403
+ await sleep(500);
404
+ if (await isPortOpen(PROXY_HOST, PROXY_PORT)) {
405
+ return true;
406
+ }
407
+ }
408
+ return false;
409
+ }
410
+
411
+ function printRestartHint() {
412
+ process.stdout.write('\n后续建议:\n');
413
+ if (process.platform === 'linux') {
414
+ process.stdout.write('- Linux(systemd): systemctl --user restart openclaw-gateway.service\n');
415
+ } else if (process.platform === 'darwin') {
416
+ process.stdout.write('- macOS: 重启 OpenClaw 进程,或重新执行你本地的 openclaw 启动命令\n');
417
+ } else if (process.platform === 'win32') {
418
+ process.stdout.write('- Windows: 重启 OpenClaw 进程或服务(若以服务方式运行)\n');
419
+ } else {
420
+ process.stdout.write('- 请重启 OpenClaw 进程,使新配置生效\n');
421
+ }
422
+ }
423
+
78
424
  export async function runConfigOpenclaw({ openclawHome } = {}) {
79
425
  const home = openclawHome || process.env.OPENCLAW_HOME || path.join(os.homedir(), '.openclaw');
80
426
  const openclawJsonPath = path.join(home, 'openclaw.json');
@@ -83,17 +429,26 @@ export async function runConfigOpenclaw({ openclawHome } = {}) {
83
429
 
84
430
  const openclawCfg = readJson(openclawJsonPath);
85
431
  const modelsCfg = readJson(modelsJsonPath);
86
- let authCfg = fs.existsSync(authProfilesPath)
432
+ const authCfg = fs.existsSync(authProfilesPath)
87
433
  ? readJson(authProfilesPath)
88
434
  : { version: 1, profiles: {}, lastGood: {}, usageStats: {} };
89
435
 
90
- const rl = readline.createInterface({
91
- input: process.stdin,
92
- output: process.stdout
93
- });
436
+ let rl = null;
94
437
 
95
438
  try {
96
439
  process.stdout.write(`OpenClaw Home: ${home}\n`);
440
+ process.stdout.write(`固定代理目标: ${PROXY_UPSTREAM}\n`);
441
+ process.stdout.write(`固定 Provider Base URL: ${PROXY_BASE_URL}\n`);
442
+
443
+ const proxyReady = await ensureProxyReady();
444
+ if (!proxyReady) {
445
+ process.stdout.write('\n已取消:本地代理未就绪,未修改 OpenClaw 模型配置。\n');
446
+ return;
447
+ }
448
+ rl = readlinePromises.createInterface({
449
+ input: process.stdin,
450
+ output: process.stdout
451
+ });
97
452
 
98
453
  const selectedModel = await askChoice(rl, '1) 选择默认模型', MODEL_OPTIONS, 1);
99
454
  const reasoningSwitch = await askChoice(rl, '2) 配置推理开关', ['off', 'on'], 0);
@@ -104,13 +459,17 @@ export async function runConfigOpenclaw({ openclawHome } = {}) {
104
459
  modelsCfg?.providers?.[PROVIDER]?.apiKey ||
105
460
  authCfg?.profiles?.[`${PROVIDER}:default`]?.key ||
106
461
  '';
107
- const selectedKey = await askKey(rl, currentKey);
462
+ const selectedKey = await askKeyWithGuard(rl, currentKey);
108
463
 
109
464
  openclawCfg.models = openclawCfg.models || {};
110
465
  openclawCfg.models.providers = openclawCfg.models.providers || {};
111
466
  openclawCfg.models.providers[PROVIDER] = openclawCfg.models.providers[PROVIDER] || {};
467
+ openclawCfg.models.providers[PROVIDER].baseUrl = PROXY_BASE_URL;
112
468
  openclawCfg.models.providers[PROVIDER].api = 'openai-responses';
113
469
  openclawCfg.models.providers[PROVIDER].authHeader = true;
470
+ openclawCfg.models.providers[PROVIDER].headers = openclawCfg.models.providers[PROVIDER].headers || {};
471
+ openclawCfg.models.providers[PROVIDER].headers['User-Agent'] = 'codex_cli_rs/0.101.0 (Ubuntu 24.4.0; x86_64) WindowsTerminal';
472
+ openclawCfg.models.providers[PROVIDER].headers.Accept = 'application/json';
114
473
  if (selectedKey) {
115
474
  openclawCfg.models.providers[PROVIDER].apiKey = selectedKey;
116
475
  openclawCfg.models.providers[PROVIDER].auth = 'api-key';
@@ -125,11 +484,24 @@ export async function runConfigOpenclaw({ openclawHome } = {}) {
125
484
  openclawCfg.agents.defaults.models = openclawCfg.agents.defaults.models || {};
126
485
  openclawCfg.agents.defaults.models[`${PROVIDER}/${selectedModel}`] =
127
486
  openclawCfg.agents.defaults.models[`${PROVIDER}/${selectedModel}`] || { alias: `ATools ${selectedModel}` };
487
+ if (Array.isArray(openclawCfg.agents.list)) {
488
+ openclawCfg.agents.list = openclawCfg.agents.list.map((agent) => {
489
+ if (!agent || typeof agent !== 'object') return agent;
490
+ if (typeof agent.model === 'string' && agent.model.startsWith(`${PROVIDER}/`)) {
491
+ return { ...agent, model: `${PROVIDER}/${selectedModel}` };
492
+ }
493
+ return agent;
494
+ });
495
+ }
128
496
 
129
497
  modelsCfg.providers = modelsCfg.providers || {};
130
498
  modelsCfg.providers[PROVIDER] = modelsCfg.providers[PROVIDER] || {};
499
+ modelsCfg.providers[PROVIDER].baseUrl = PROXY_BASE_URL;
131
500
  modelsCfg.providers[PROVIDER].api = 'openai-responses';
132
501
  modelsCfg.providers[PROVIDER].authHeader = true;
502
+ modelsCfg.providers[PROVIDER].headers = modelsCfg.providers[PROVIDER].headers || {};
503
+ modelsCfg.providers[PROVIDER].headers['User-Agent'] = 'codex_cli_rs/0.101.0 (Ubuntu 24.4.0; x86_64) WindowsTerminal';
504
+ modelsCfg.providers[PROVIDER].headers.Accept = 'application/json';
133
505
  if (selectedKey) {
134
506
  modelsCfg.providers[PROVIDER].apiKey = selectedKey;
135
507
  modelsCfg.providers[PROVIDER].auth = 'api-key';
@@ -161,6 +533,8 @@ export async function runConfigOpenclaw({ openclawHome } = {}) {
161
533
  writeJson(authProfilesPath, authCfg);
162
534
 
163
535
  process.stdout.write('\n配置已更新:\n');
536
+ process.stdout.write(`- Proxy Upstream: ${PROXY_UPSTREAM}\n`);
537
+ process.stdout.write(`- Provider Base URL: ${PROXY_BASE_URL}\n`);
164
538
  process.stdout.write(`- 默认模型: ${PROVIDER}/${selectedModel}\n`);
165
539
  process.stdout.write(`- 推理开关: ${reasoning ? 'on' : 'off'}\n`);
166
540
  process.stdout.write(`- 思考强度: ${selectedThinking}\n`);
@@ -168,7 +542,8 @@ export async function runConfigOpenclaw({ openclawHome } = {}) {
168
542
  process.stdout.write(`- 已写入: ${openclawJsonPath}\n`);
169
543
  process.stdout.write(`- 已写入: ${modelsJsonPath}\n`);
170
544
  process.stdout.write(`- 已写入: ${authProfilesPath}\n`);
545
+ printRestartHint();
171
546
  } finally {
172
- rl.close();
547
+ if (rl) rl.close();
173
548
  }
174
549
  }
package/lib/install.mjs CHANGED
@@ -8,8 +8,8 @@ const DEFAULT_PROVIDER = 'sub2api';
8
8
  const DEFAULT_MODEL = 'gpt-5.3-codex';
9
9
  const DEFAULT_PORT = 18888;
10
10
  const DEFAULT_UPSTREAM = 'https://sub2api.atools.live';
11
- const DEFAULT_FALLBACK_UPSTREAM = 'https://gmn.chuangzuoli.com/v1';
12
- const DEFAULT_LOG_FILE = '/tmp/openclaw/atools-compat-proxy.log';
11
+ const DEFAULT_FALLBACK_UPSTREAM = '';
12
+ const DEFAULT_LOG_FILE = path.join(os.tmpdir(), 'openclaw', 'atools-compat-proxy.log');
13
13
  const DEFAULT_SERVICE_NAME = 'openclaw-atools-proxy.service';
14
14
  const OPENCLAW_GATEWAY_SERVICE = 'openclaw-gateway.service';
15
15
 
@@ -47,6 +47,46 @@ function ensureDir(dir) {
47
47
  fs.mkdirSync(dir, { recursive: true });
48
48
  }
49
49
 
50
+ function defaultServiceId(name = DEFAULT_SERVICE_NAME) {
51
+ return String(name || DEFAULT_SERVICE_NAME).replace(/\.service$/i, '');
52
+ }
53
+
54
+ function buildProxyServeArgs({ port, upstream, fallbackUpstream, fallbackApiKey, maxReqBytes, logFile }) {
55
+ const args = [
56
+ 'serve',
57
+ '--port',
58
+ String(port),
59
+ '--upstream',
60
+ String(upstream),
61
+ '--log-file',
62
+ String(logFile),
63
+ '--max-req-bytes',
64
+ String(maxReqBytes)
65
+ ];
66
+ if (fallbackUpstream) {
67
+ args.push('--fallback-upstream', String(fallbackUpstream));
68
+ }
69
+ if (fallbackApiKey) {
70
+ args.push('--fallback-api-key', String(fallbackApiKey));
71
+ }
72
+ return args;
73
+ }
74
+
75
+ function escapeXml(text) {
76
+ return String(text)
77
+ .replace(/&/g, '&amp;')
78
+ .replace(/</g, '&lt;')
79
+ .replace(/>/g, '&gt;')
80
+ .replace(/"/g, '&quot;')
81
+ .replace(/'/g, '&apos;');
82
+ }
83
+
84
+ function quoteWindowsArg(arg) {
85
+ const s = String(arg ?? '');
86
+ if (!/[\s"]/g.test(s)) return s;
87
+ return `"${s.replace(/"/g, '\\"')}"`;
88
+ }
89
+
50
90
  function parseArgs(argv) {
51
91
  const out = {
52
92
  openclawHome: process.env.OPENCLAW_HOME || path.join(os.homedir(), '.openclaw'),
@@ -95,34 +135,27 @@ function parseArgs(argv) {
95
135
  }
96
136
 
97
137
  function renderSystemdService({
98
- serviceName,
99
138
  nodeBin,
100
139
  cliPath,
101
- port,
102
- upstream,
140
+ serveArgs,
103
141
  fallbackUpstream,
104
- maxReqBytes,
105
- fallbackApiKey,
106
- logFile
142
+ fallbackApiKey
107
143
  }) {
108
144
  const fallbackEnv = fallbackUpstream
109
145
  ? `Environment=SUB2API_FALLBACK_UPSTREAM=${fallbackUpstream}\nEnvironment=SUB2API_FALLBACK_API_KEY=${fallbackApiKey || ''}\n`
110
146
  : '';
147
+ const execArgs = [nodeBin, cliPath, ...serveArgs].join(' ');
111
148
  return `[Unit]
112
149
  Description=OpenClaw ATools Compatibility Proxy
113
150
  After=network-online.target
114
151
  Wants=network-online.target
115
152
 
116
153
  [Service]
117
- ExecStart=${nodeBin} ${cliPath} serve --port ${port} --upstream ${upstream} --log-file ${logFile}
154
+ ExecStart=${execArgs}
118
155
  Restart=always
119
156
  RestartSec=2
120
157
  Environment=HOME=${os.homedir()}
121
158
  Environment=TMPDIR=/tmp
122
- Environment=SUB2API_COMPAT_PORT=${port}
123
- Environment=SUB2API_UPSTREAM=${upstream}
124
- Environment=SUB2API_COMPAT_LOG=${logFile}
125
- Environment=SUB2API_COMPAT_MAX_REQ_BYTES=${maxReqBytes}
126
159
  Environment="SUB2API_COMPAT_USER_AGENT=codex_cli_rs/0.101.0 (Ubuntu 24.4.0; x86_64) WindowsTerminal"
127
160
  ${fallbackEnv}
128
161
 
@@ -131,6 +164,42 @@ WantedBy=default.target
131
164
  `;
132
165
  }
133
166
 
167
+ function renderLaunchdPlist({ label, nodeBin, cliPath, serveArgs, logFile }) {
168
+ const args = [nodeBin, cliPath, ...serveArgs];
169
+ const argsXml = args.map((arg) => ` <string>${escapeXml(arg)}</string>`).join('\n');
170
+ const launchLog = `${logFile}.launchd.log`;
171
+ return `<?xml version="1.0" encoding="UTF-8"?>
172
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
173
+ <plist version="1.0">
174
+ <dict>
175
+ <key>Label</key>
176
+ <string>${escapeXml(label)}</string>
177
+ <key>ProgramArguments</key>
178
+ <array>
179
+ ${argsXml}
180
+ </array>
181
+ <key>RunAtLoad</key>
182
+ <true/>
183
+ <key>KeepAlive</key>
184
+ <true/>
185
+ <key>WorkingDirectory</key>
186
+ <string>${escapeXml(os.homedir())}</string>
187
+ <key>EnvironmentVariables</key>
188
+ <dict>
189
+ <key>HOME</key>
190
+ <string>${escapeXml(os.homedir())}</string>
191
+ <key>SUB2API_COMPAT_USER_AGENT</key>
192
+ <string>codex_cli_rs/0.101.0 (Ubuntu 24.4.0; x86_64) WindowsTerminal</string>
193
+ </dict>
194
+ <key>StandardOutPath</key>
195
+ <string>${escapeXml(launchLog)}</string>
196
+ <key>StandardErrorPath</key>
197
+ <string>${escapeXml(launchLog)}</string>
198
+ </dict>
199
+ </plist>
200
+ `;
201
+ }
202
+
134
203
  function detectApiKey(openclawHome, explicitKey) {
135
204
  if (explicitKey) return explicitKey;
136
205
  const authPath = path.join(openclawHome, 'agents/main/agent/auth-profiles.json');
@@ -297,7 +366,7 @@ function runSystemctl(args, { dryRun } = {}) {
297
366
  execFileSync('systemctl', ['--user', ...args], { stdio: 'inherit' });
298
367
  }
299
368
 
300
- function installService({ serviceName, content, dryRun }) {
369
+ function installLinuxService({ serviceName, content, dryRun }) {
301
370
  const serviceDir = path.join(os.homedir(), '.config/systemd/user');
302
371
  const servicePath = path.join(serviceDir, serviceName);
303
372
  ensureDir(serviceDir);
@@ -317,12 +386,105 @@ function installService({ serviceName, content, dryRun }) {
317
386
  runSystemctl(['enable', '--now', serviceName], { dryRun });
318
387
  }
319
388
 
389
+ function runLaunchctl(args, { dryRun, allowFailure = false } = {}) {
390
+ if (dryRun) {
391
+ info(`dry-run: launchctl ${args.join(' ')}`);
392
+ return;
393
+ }
394
+ try {
395
+ execFileSync('launchctl', args, { stdio: 'inherit' });
396
+ } catch (err) {
397
+ if (allowFailure) return;
398
+ throw err;
399
+ }
400
+ }
401
+
402
+ function installMacService({ serviceName, content, dryRun }) {
403
+ const serviceId = defaultServiceId(serviceName);
404
+ const launchAgentDir = path.join(os.homedir(), 'Library', 'LaunchAgents');
405
+ const plistPath = path.join(launchAgentDir, `${serviceId}.plist`);
406
+ const uid = typeof process.getuid === 'function' ? process.getuid() : null;
407
+ if (uid == null) {
408
+ throw new Error('failed to resolve current uid for launchctl');
409
+ }
410
+
411
+ ensureDir(launchAgentDir);
412
+ if (dryRun) {
413
+ info(`dry-run: would write ${plistPath}`);
414
+ } else {
415
+ if (fs.existsSync(plistPath)) {
416
+ const bak = backup(plistPath);
417
+ if (bak) info(`backup created: ${bak}`);
418
+ }
419
+ fs.writeFileSync(plistPath, content, 'utf8');
420
+ info(`wrote: ${plistPath}`);
421
+ }
422
+
423
+ const target = `gui/${uid}/${serviceId}`;
424
+ runLaunchctl(['bootout', target], { dryRun, allowFailure: true });
425
+ runLaunchctl(['bootstrap', `gui/${uid}`, plistPath], { dryRun });
426
+ runLaunchctl(['enable', target], { dryRun, allowFailure: true });
427
+ runLaunchctl(['kickstart', '-k', target], { dryRun, allowFailure: true });
428
+ }
429
+
430
+ function runSchtasks(args, { dryRun, allowFailure = false } = {}) {
431
+ if (dryRun) {
432
+ info(`dry-run: schtasks ${args.join(' ')}`);
433
+ return;
434
+ }
435
+ try {
436
+ execFileSync('schtasks', args, { stdio: 'inherit' });
437
+ } catch (err) {
438
+ if (allowFailure) return;
439
+ throw err;
440
+ }
441
+ }
442
+
443
+ function installWindowsService({ serviceName, nodeBin, cliPath, serveArgs, dryRun }) {
444
+ const taskName = defaultServiceId(serviceName);
445
+ const command = [nodeBin, cliPath, ...serveArgs].map(quoteWindowsArg).join(' ');
446
+ runSchtasks(
447
+ ['/Create', '/F', '/SC', 'ONLOGON', '/RL', 'LIMITED', '/TN', taskName, '/TR', command],
448
+ { dryRun }
449
+ );
450
+ runSchtasks(['/Run', '/TN', taskName], { dryRun, allowFailure: true });
451
+ }
452
+
453
+ function installService({
454
+ serviceName,
455
+ linuxServiceContent,
456
+ macPlistContent,
457
+ nodeBin,
458
+ cliPath,
459
+ serveArgs,
460
+ dryRun
461
+ }) {
462
+ if (process.platform === 'linux') {
463
+ installLinuxService({ serviceName, content: linuxServiceContent, dryRun });
464
+ return;
465
+ }
466
+ if (process.platform === 'darwin') {
467
+ installMacService({ serviceName, content: macPlistContent, dryRun });
468
+ return;
469
+ }
470
+ if (process.platform === 'win32') {
471
+ installWindowsService({ serviceName, nodeBin, cliPath, serveArgs, dryRun });
472
+ return;
473
+ }
474
+ throw new Error(`unsupported platform for service install: ${process.platform}`);
475
+ }
476
+
320
477
  function restartOpenclawGateway({ noRestart, dryRun }) {
321
478
  if (noRestart) {
322
479
  info('skip gateway restart (--no-restart)');
323
480
  return;
324
481
  }
325
482
 
483
+ if (process.platform !== 'linux') {
484
+ info(`skip gateway restart on ${process.platform} (please restart OpenClaw manually)`);
485
+ return;
486
+ }
487
+
326
488
  try {
327
489
  runSystemctl(['restart', OPENCLAW_GATEWAY_SERVICE], { dryRun });
328
490
  } catch {
@@ -342,10 +504,10 @@ Options:
342
504
  --model <id> Default: gpt-5.3-codex
343
505
  --port <number> Default: 18888
344
506
  --upstream <url> Default: https://sub2api.atools.live
345
- --fallback-upstream <url> Default: https://gmn.chuangzuoli.com/v1
507
+ --fallback-upstream <url> Default: empty (disabled)
346
508
  --max-req-bytes <number> Default: 68000
347
- --log-file <path> Default: /tmp/openclaw/atools-compat-proxy.log
348
- --service-name <name> Default: openclaw-atools-proxy.service
509
+ --log-file <path> Default: <tmp>/openclaw/atools-compat-proxy.log
510
+ --service-name <name> Linux: *.service; macOS/Windows: service id/task name
349
511
  --api-key <key> Prefer explicit key; fallback env SUB_OAI_KEY then auth-profiles
350
512
  --fallback-api-key <key> Prefer explicit key; fallback env FALLBACK_OAI_KEY then openclaw gmn key
351
513
  --no-restart Do not restart openclaw-gateway.service
@@ -366,8 +528,17 @@ export async function runInstall(rawArgs = process.argv.slice(2)) {
366
528
  const cliPath = fileURLToPath(new URL('../bin/cli.mjs', import.meta.url));
367
529
  const apiKey = detectApiKey(args.openclawHome, args.apiKey);
368
530
  const fallbackApiKey = detectFallbackApiKey(args.openclawHome, args.fallbackApiKey);
531
+ const serveArgs = buildProxyServeArgs({
532
+ port: args.port,
533
+ upstream: args.upstream,
534
+ fallbackUpstream: args.fallbackUpstream,
535
+ fallbackApiKey,
536
+ maxReqBytes: args.maxReqBytes,
537
+ logFile: args.logFile
538
+ });
369
539
 
370
540
  info(`openclawHome=${args.openclawHome}`);
541
+ info(`platform=${process.platform}`);
371
542
  info(`provider/model=${args.provider}/${args.model}`);
372
543
  info(`proxy=http://127.0.0.1:${args.port}/v1 -> ${args.upstream}`);
373
544
  if (args.fallbackUpstream) {
@@ -378,19 +549,30 @@ export async function runInstall(rawArgs = process.argv.slice(2)) {
378
549
  patchAgentModels(args.openclawHome, args.provider, args.model, args.port, apiKey, args.dryRun);
379
550
  patchAuthProfiles(args.openclawHome, args.provider, apiKey, args.dryRun);
380
551
 
381
- const serviceContent = renderSystemdService({
382
- serviceName: args.serviceName,
552
+ const linuxServiceContent = renderSystemdService({
383
553
  nodeBin,
384
554
  cliPath,
385
- port: args.port,
386
- upstream: args.upstream,
555
+ serveArgs,
387
556
  fallbackUpstream: args.fallbackUpstream,
388
- maxReqBytes: args.maxReqBytes,
389
- fallbackApiKey,
557
+ fallbackApiKey
558
+ });
559
+ const macPlistContent = renderLaunchdPlist({
560
+ label: defaultServiceId(args.serviceName),
561
+ nodeBin,
562
+ cliPath,
563
+ serveArgs,
390
564
  logFile: args.logFile
391
565
  });
392
566
 
393
- installService({ serviceName: args.serviceName, content: serviceContent, dryRun: args.dryRun });
567
+ installService({
568
+ serviceName: args.serviceName,
569
+ linuxServiceContent,
570
+ macPlistContent,
571
+ nodeBin,
572
+ cliPath,
573
+ serveArgs,
574
+ dryRun: args.dryRun
575
+ });
394
576
  restartOpenclawGateway({ noRestart: args.noRestart, dryRun: args.dryRun });
395
577
 
396
578
  info('done');
@@ -1,5 +1,6 @@
1
1
  import http from 'node:http';
2
2
  import fs from 'node:fs';
3
+ import path from 'node:path';
3
4
 
4
5
  const DEFAULT_PORT = 18888;
5
6
  const DEFAULT_UPSTREAM = 'https://sub2api.atools.live';
@@ -9,9 +10,10 @@ const DEFAULT_FALLBACK_UPSTREAM = '';
9
10
  const DEFAULT_FALLBACK_API_KEY = '';
10
11
  const DEFAULT_MAX_REQ_BYTES = 68000;
11
12
  const DEFAULT_DROP_TOOLS_ON_COMPACT = false;
13
+ const DEFAULT_STRIP_PREVIOUS_RESPONSE_ID = false;
12
14
 
13
15
  function ensureParentDir(filePath) {
14
- const dir = filePath.replace(/\/[^/]*$/, '');
16
+ const dir = path.dirname(filePath);
15
17
  if (dir) {
16
18
  fs.mkdirSync(dir, { recursive: true });
17
19
  }
@@ -54,12 +56,13 @@ function normalizeContent(content) {
54
56
  return [toInputText('')];
55
57
  }
56
58
 
57
- function normalizeResponsesPayload(payload) {
59
+ function normalizeResponsesPayload(payload, { stripPreviousResponseId = DEFAULT_STRIP_PREVIOUS_RESPONSE_ID } = {}) {
58
60
  if (!payload || typeof payload !== 'object') return payload;
59
61
  const out = { ...payload };
60
62
 
61
- // Some upstream gateways reject/unstable with this field.
62
- if ('previous_response_id' in out) delete out.previous_response_id;
63
+ if (stripPreviousResponseId && 'previous_response_id' in out) {
64
+ delete out.previous_response_id;
65
+ }
63
66
 
64
67
  if (typeof out.input === 'string') {
65
68
  out.input = [{ role: 'user', content: normalizeContent(out.input) }];
@@ -84,7 +87,28 @@ function trimTextPart(part, maxChars) {
84
87
  return { ...part, text: text.length > maxChars ? text.slice(0, maxChars) : text };
85
88
  }
86
89
 
87
- function compactResponsesPayload(payload, { dropToolsOnCompact = DEFAULT_DROP_TOOLS_ON_COMPACT } = {}) {
90
+ function compactInputMessages(messages, maxRecentMessages = 8) {
91
+ if (!Array.isArray(messages)) return messages;
92
+ const recent = messages.slice(-Math.max(1, maxRecentMessages));
93
+ let latestSystem = null;
94
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
95
+ const msg = messages[i];
96
+ if (!msg || typeof msg !== 'object') continue;
97
+ if (msg.role === 'system' || msg.role === 'developer') {
98
+ latestSystem = msg;
99
+ break;
100
+ }
101
+ }
102
+ if (latestSystem && !recent.includes(latestSystem)) {
103
+ return [latestSystem, ...recent];
104
+ }
105
+ return recent;
106
+ }
107
+
108
+ function compactResponsesPayload(payload, {
109
+ dropToolsOnCompact = DEFAULT_DROP_TOOLS_ON_COMPACT,
110
+ maxRecentMessages
111
+ } = {}) {
88
112
  if (!payload || typeof payload !== 'object') return payload;
89
113
  const out = { ...payload };
90
114
 
@@ -104,13 +128,11 @@ function compactResponsesPayload(payload, { dropToolsOnCompact = DEFAULT_DROP_TO
104
128
  }
105
129
  return item;
106
130
  });
107
- const systemMsgs = normalized.filter((m) => m && typeof m === 'object' && (m.role === 'system' || m.role === 'developer')).slice(-1);
108
- const userMsgs = normalized.filter((m) => m && typeof m === 'object' && m.role === 'user').slice(-2);
109
- const merged = [...systemMsgs, ...userMsgs];
110
- out.input = merged.length > 0 ? merged : normalized.slice(-1);
131
+ const resolvedMaxRecentMessages = Number.isInteger(maxRecentMessages)
132
+ ? Math.max(1, maxRecentMessages)
133
+ : (('previous_response_id' in out) ? 14 : 8);
134
+ out.input = compactInputMessages(normalized, resolvedMaxRecentMessages);
111
135
  }
112
-
113
- if ('previous_response_id' in out) delete out.previous_response_id;
114
136
  return out;
115
137
  }
116
138
 
@@ -149,6 +171,8 @@ export async function createProxyServer(options = {}) {
149
171
  const maxReqBytes = Number(options.maxReqBytes ?? process.env.SUB2API_COMPAT_MAX_REQ_BYTES ?? DEFAULT_MAX_REQ_BYTES);
150
172
  const dropToolsOnCompact = options.dropToolsOnCompact
151
173
  ?? ['1', 'true', 'yes', 'on'].includes(String(process.env.SUB2API_COMPAT_DROP_TOOLS_ON_COMPACT || '').toLowerCase());
174
+ const stripPreviousResponseId = options.stripPreviousResponseId
175
+ ?? ['1', 'true', 'yes', 'on'].includes(String(process.env.SUB2API_COMPAT_STRIP_PREVIOUS_RESPONSE_ID || '').toLowerCase());
152
176
 
153
177
  const server = http.createServer(async (req, res) => {
154
178
  const startAt = Date.now();
@@ -167,34 +191,74 @@ export async function createProxyServer(options = {}) {
167
191
  const isResponsesPath = req.method === 'POST' && url.pathname.endsWith('/responses');
168
192
  let body = rawBody;
169
193
  let compactBody = null;
194
+ let aggressiveBody = null;
195
+ let minimalBody = null;
170
196
 
171
197
  if (isResponsesPath && rawBody.length > 0) {
172
198
  try {
173
199
  const parsed = JSON.parse(rawBody.toString('utf8'));
174
- const normalized = normalizeResponsesPayload(parsed);
200
+ const normalized = normalizeResponsesPayload(parsed, { stripPreviousResponseId });
175
201
  body = Buffer.from(JSON.stringify(normalized));
176
- const compacted = compactResponsesPayload(normalized, { dropToolsOnCompact });
177
- const compactBuf = Buffer.from(JSON.stringify(compacted));
178
- if (compactBuf.length + 512 < body.length) {
179
- compactBody = compactBuf;
202
+ if (body.length > maxReqBytes) {
203
+ const compacted = compactResponsesPayload(normalized, { dropToolsOnCompact });
204
+ const compactBuf = Buffer.from(JSON.stringify(compacted));
205
+ if (compactBuf.length + 512 < body.length) {
206
+ compactBody = compactBuf;
207
+ }
208
+ const aggressive = compactResponsesPayload(normalized, {
209
+ dropToolsOnCompact,
210
+ maxRecentMessages: 3
211
+ });
212
+ const aggressiveBuf = Buffer.from(JSON.stringify(aggressive));
213
+ if (aggressiveBuf.length + 256 < body.length) {
214
+ aggressiveBody = aggressiveBuf;
215
+ }
216
+ const minimal = compactResponsesPayload(normalized, {
217
+ dropToolsOnCompact,
218
+ maxRecentMessages: 1
219
+ });
220
+ const minimalBuf = Buffer.from(JSON.stringify(minimal));
221
+ if (minimalBuf.length + 128 < body.length) {
222
+ minimalBody = minimalBuf;
223
+ }
180
224
  }
181
225
  } catch {
182
226
  // Keep original body if parse fails.
183
227
  }
184
228
  }
185
229
 
230
+ const requestBodies = [body];
231
+ if (compactBody && !compactBody.equals(body)) {
232
+ requestBodies.push(compactBody);
233
+ }
234
+ if (aggressiveBody && !aggressiveBody.equals(body) && !(compactBody && aggressiveBody.equals(compactBody))) {
235
+ requestBodies.push(aggressiveBody);
236
+ }
237
+ if (
238
+ minimalBody
239
+ && !minimalBody.equals(body)
240
+ && !(compactBody && minimalBody.equals(compactBody))
241
+ && !(aggressiveBody && minimalBody.equals(aggressiveBody))
242
+ ) {
243
+ requestBodies.push(minimalBody);
244
+ }
245
+
186
246
  let resp;
187
247
  let attempts = 0;
188
248
  let via = 'primary';
189
249
  let primaryStatus = null;
190
250
  let fallbackStatus = null;
191
251
  let compacted = false;
252
+ let strategy = 'full';
192
253
  const maxAttempts = isResponsesPath ? retryMax : 1;
193
254
  while (attempts < maxAttempts) {
194
255
  attempts += 1;
195
- const shouldCompactNow = Boolean(compactBody && (attempts > 1 || body.length > maxReqBytes));
196
- const requestBody = shouldCompactNow ? compactBody : body;
197
- compacted = shouldCompactNow;
256
+ const strategyIndex = Math.min(requestBodies.length - 1, attempts - 1);
257
+ const requestBody = requestBodies[strategyIndex];
258
+ compacted = strategyIndex > 0;
259
+ strategy = strategyIndex === 0
260
+ ? 'full'
261
+ : (strategyIndex === 1 ? 'compact' : (strategyIndex === 2 ? 'aggressive' : 'minimal'));
198
262
  const primaryHeaders = isResponsesPath ? buildHeaders(baseHeaders, requestBody.length) : baseHeaders;
199
263
  const fallbackHeaders = isResponsesPath
200
264
  ? buildHeaders(baseHeaders, requestBody.length, fallbackApiKey)
@@ -244,8 +308,9 @@ export async function createProxyServer(options = {}) {
244
308
  primaryStatus,
245
309
  fallbackStatus,
246
310
  compacted,
311
+ strategy,
247
312
  reqBytes: rawBody.length,
248
- upstreamReqBytes: compactBody && compacted ? compactBody.length : body.length,
313
+ upstreamReqBytes: requestBodies[Math.min(requestBodies.length - 1, attempts - 1)].length,
249
314
  respBytes: outBuffer.length,
250
315
  durationMs: Date.now() - startAt
251
316
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autotask/atools-tool",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "ATools CLI for OpenClaw proxy compatibility and interactive model configuration",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -33,7 +33,7 @@
33
33
  },
34
34
  "scripts": {
35
35
  "start": "node ./bin/cli.mjs serve",
36
- "config:openclaw": "node ./bin/cli.mjs config-openclaw",
36
+ "config:openclaw": "node ./bin/cli.mjs openclaw",
37
37
  "install:local": "node ./bin/install.mjs",
38
38
  "pack:check": "npm pack"
39
39
  },