@autotask/atools-tool 0.1.5 → 0.1.7
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 +3 -0
- package/bin/cli.mjs +3 -0
- package/lib/config-openclaw.mjs +6 -0
- package/lib/install.mjs +10 -1
- package/lib/proxy-server.mjs +105 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -68,6 +68,9 @@ atools-tool-install --help
|
|
|
68
68
|
- macOS: launchd user agent (`~/Library/LaunchAgents`)
|
|
69
69
|
- Windows: Task Scheduler user task (ONLOGON + start now)
|
|
70
70
|
|
|
71
|
+
By default, installer does not hard-force a single outbound model.
|
|
72
|
+
Proxy normalizes `sub2api/<model>` to plain model id for codex-compatible upstreams.
|
|
73
|
+
|
|
71
74
|
## Proxy Command
|
|
72
75
|
|
|
73
76
|
```bash
|
package/bin/cli.mjs
CHANGED
|
@@ -8,6 +8,7 @@ function parseArgs(argv) {
|
|
|
8
8
|
openclawHome: undefined,
|
|
9
9
|
port: undefined,
|
|
10
10
|
upstream: undefined,
|
|
11
|
+
forceModel: undefined,
|
|
11
12
|
maxReqBytes: undefined,
|
|
12
13
|
logFile: undefined,
|
|
13
14
|
retryMax: undefined,
|
|
@@ -25,6 +26,7 @@ function parseArgs(argv) {
|
|
|
25
26
|
if (a === '--port') out.port = Number(next());
|
|
26
27
|
else if (a === '--openclaw-home') out.openclawHome = next();
|
|
27
28
|
else if (a === '--upstream') out.upstream = next();
|
|
29
|
+
else if (a === '--force-model') out.forceModel = next();
|
|
28
30
|
else if (a === '--max-req-bytes') out.maxReqBytes = Number(next());
|
|
29
31
|
else if (a === '--log-file') out.logFile = next();
|
|
30
32
|
else if (a === '--retry-max') out.retryMax = Number(next());
|
|
@@ -50,6 +52,7 @@ Options:
|
|
|
50
52
|
--openclaw-home <path> OpenClaw home dir (default: ~/.openclaw)
|
|
51
53
|
--port <number> Listen port (default: 18888)
|
|
52
54
|
--upstream <url> Upstream base URL (default: https://sub2api.atools.live)
|
|
55
|
+
--force-model <id> Force outbound model id (e.g. gpt-5.3-codex)
|
|
53
56
|
--max-req-bytes <number> Compact requests larger than this threshold (default: 68000)
|
|
54
57
|
--log-file <path> Log file (default: /tmp/openclaw/atools-compat-proxy.log)
|
|
55
58
|
--retry-max <number> Retry attempts on 5xx for /responses (default: 6)
|
package/lib/config-openclaw.mjs
CHANGED
|
@@ -258,6 +258,11 @@ function upsertEnvLine(content, key, value) {
|
|
|
258
258
|
return `${content.slice(0, afterServiceHeader + 1)}${line}\n${content.slice(afterServiceHeader + 1)}`;
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
+
function removeEnvLine(content, key) {
|
|
262
|
+
const re = new RegExp(`^Environment=${escapeRegExp(key)}=.*\\n?`, 'gm');
|
|
263
|
+
return content.replace(re, '');
|
|
264
|
+
}
|
|
265
|
+
|
|
261
266
|
function configureLinuxProxyService() {
|
|
262
267
|
const servicePath = path.join(os.homedir(), '.config/systemd/user', PROXY_SERVICE);
|
|
263
268
|
if (!fs.existsSync(servicePath)) {
|
|
@@ -266,6 +271,7 @@ function configureLinuxProxyService() {
|
|
|
266
271
|
const original = fs.readFileSync(servicePath, 'utf8');
|
|
267
272
|
let next = original;
|
|
268
273
|
next = upsertEnvLine(next, 'SUB2API_UPSTREAM', PROXY_UPSTREAM);
|
|
274
|
+
next = removeEnvLine(next, 'SUB2API_COMPAT_FORCE_MODEL');
|
|
269
275
|
next = upsertEnvLine(next, 'SUB2API_COMPAT_DROP_TOOLS_ON_COMPACT', '0');
|
|
270
276
|
next = upsertEnvLine(next, 'SUB2API_COMPAT_PORT', String(PROXY_PORT));
|
|
271
277
|
next = upsertEnvLine(next, 'SUB2API_COMPAT_LOG', PROXY_LOG_FILE);
|
package/lib/install.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
6
6
|
|
|
7
7
|
const DEFAULT_PROVIDER = 'sub2api';
|
|
8
8
|
const DEFAULT_MODEL = 'gpt-5.3-codex';
|
|
9
|
+
const DEFAULT_FORCE_MODEL = '';
|
|
9
10
|
const DEFAULT_PORT = 18888;
|
|
10
11
|
const DEFAULT_UPSTREAM = 'https://sub2api.atools.live';
|
|
11
12
|
const DEFAULT_LOG_FILE = path.join(os.tmpdir(), 'openclaw', 'atools-compat-proxy.log');
|
|
@@ -50,7 +51,7 @@ function defaultServiceId(name = DEFAULT_SERVICE_NAME) {
|
|
|
50
51
|
return String(name || DEFAULT_SERVICE_NAME).replace(/\.service$/i, '');
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
function buildProxyServeArgs({ port, upstream, maxReqBytes, logFile }) {
|
|
54
|
+
function buildProxyServeArgs({ port, upstream, forceModel, maxReqBytes, logFile }) {
|
|
54
55
|
const args = [
|
|
55
56
|
'serve',
|
|
56
57
|
'--port',
|
|
@@ -62,6 +63,9 @@ function buildProxyServeArgs({ port, upstream, maxReqBytes, logFile }) {
|
|
|
62
63
|
'--max-req-bytes',
|
|
63
64
|
String(maxReqBytes)
|
|
64
65
|
];
|
|
66
|
+
if (forceModel) {
|
|
67
|
+
args.push('--force-model', String(forceModel));
|
|
68
|
+
}
|
|
65
69
|
return args;
|
|
66
70
|
}
|
|
67
71
|
|
|
@@ -85,6 +89,7 @@ function parseArgs(argv) {
|
|
|
85
89
|
openclawHome: process.env.OPENCLAW_HOME || path.join(os.homedir(), '.openclaw'),
|
|
86
90
|
provider: DEFAULT_PROVIDER,
|
|
87
91
|
model: DEFAULT_MODEL,
|
|
92
|
+
forceModel: DEFAULT_FORCE_MODEL,
|
|
88
93
|
port: DEFAULT_PORT,
|
|
89
94
|
upstream: DEFAULT_UPSTREAM,
|
|
90
95
|
maxReqBytes: 68000,
|
|
@@ -102,6 +107,7 @@ function parseArgs(argv) {
|
|
|
102
107
|
if (a === '--openclaw-home') out.openclawHome = next();
|
|
103
108
|
else if (a === '--provider') out.provider = next();
|
|
104
109
|
else if (a === '--model') out.model = next();
|
|
110
|
+
else if (a === '--force-model') out.forceModel = next();
|
|
105
111
|
else if (a === '--port') out.port = Number(next());
|
|
106
112
|
else if (a === '--upstream') out.upstream = next();
|
|
107
113
|
else if (a === '--max-req-bytes') out.maxReqBytes = Number(next());
|
|
@@ -480,6 +486,7 @@ Options:
|
|
|
480
486
|
--openclaw-home <path> Default: ~/.openclaw
|
|
481
487
|
--provider <name> Default: sub2api
|
|
482
488
|
--model <id> Default: gpt-5.3-codex
|
|
489
|
+
--force-model <id> Default: disabled (optional hard rewrite)
|
|
483
490
|
--port <number> Default: 18888
|
|
484
491
|
--upstream <url> Default: https://sub2api.atools.live
|
|
485
492
|
--max-req-bytes <number> Default: 68000
|
|
@@ -506,6 +513,7 @@ export async function runInstall(rawArgs = process.argv.slice(2)) {
|
|
|
506
513
|
const serveArgs = buildProxyServeArgs({
|
|
507
514
|
port: args.port,
|
|
508
515
|
upstream: args.upstream,
|
|
516
|
+
forceModel: args.forceModel,
|
|
509
517
|
maxReqBytes: args.maxReqBytes,
|
|
510
518
|
logFile: args.logFile
|
|
511
519
|
});
|
|
@@ -513,6 +521,7 @@ export async function runInstall(rawArgs = process.argv.slice(2)) {
|
|
|
513
521
|
info(`openclawHome=${args.openclawHome}`);
|
|
514
522
|
info(`platform=${process.platform}`);
|
|
515
523
|
info(`provider/model=${args.provider}/${args.model}`);
|
|
524
|
+
info(`proxy forceModel=${args.forceModel || '(disabled)'}`);
|
|
516
525
|
info(`proxy=http://127.0.0.1:${args.port}/v1 -> ${args.upstream}`);
|
|
517
526
|
|
|
518
527
|
patchOpenclawConfig(args.openclawHome, args.provider, args.model, args.port, args.dryRun);
|
package/lib/proxy-server.mjs
CHANGED
|
@@ -9,6 +9,35 @@ const DEFAULT_USER_AGENT = 'codex_cli_rs/0.101.0 (Ubuntu 24.4.0; x86_64) Windows
|
|
|
9
9
|
const DEFAULT_MAX_REQ_BYTES = 68000;
|
|
10
10
|
const DEFAULT_DROP_TOOLS_ON_COMPACT = false;
|
|
11
11
|
const DEFAULT_STRIP_PREVIOUS_RESPONSE_ID = false;
|
|
12
|
+
const DEFAULT_FORCE_MODEL = '';
|
|
13
|
+
const SUPPORTED_MODEL_IDS = new Set(['gpt-5.2', 'gpt-5.3-codex', 'gpt-5.4']);
|
|
14
|
+
const FORWARD_HEADER_ALLOWLIST = new Set([
|
|
15
|
+
'authorization',
|
|
16
|
+
'api-key',
|
|
17
|
+
'x-api-key',
|
|
18
|
+
'openai-organization',
|
|
19
|
+
'openai-project'
|
|
20
|
+
]);
|
|
21
|
+
const RESPONSES_KEY_ALLOWLIST = new Set([
|
|
22
|
+
'model',
|
|
23
|
+
'input',
|
|
24
|
+
'instructions',
|
|
25
|
+
'reasoning',
|
|
26
|
+
'metadata',
|
|
27
|
+
'temperature',
|
|
28
|
+
'max_output_tokens',
|
|
29
|
+
'top_p',
|
|
30
|
+
'stream',
|
|
31
|
+
'store',
|
|
32
|
+
'previous_response_id',
|
|
33
|
+
'tools',
|
|
34
|
+
'tool_choice',
|
|
35
|
+
'parallel_tool_calls',
|
|
36
|
+
'text',
|
|
37
|
+
'truncation',
|
|
38
|
+
'max_tool_calls',
|
|
39
|
+
'prompt_cache_key'
|
|
40
|
+
]);
|
|
12
41
|
|
|
13
42
|
function ensureParentDir(filePath) {
|
|
14
43
|
const dir = path.dirname(filePath);
|
|
@@ -54,13 +83,20 @@ function normalizeContent(content) {
|
|
|
54
83
|
return [toInputText('')];
|
|
55
84
|
}
|
|
56
85
|
|
|
57
|
-
function normalizeResponsesPayload(payload, {
|
|
86
|
+
function normalizeResponsesPayload(payload, {
|
|
87
|
+
stripPreviousResponseId = DEFAULT_STRIP_PREVIOUS_RESPONSE_ID,
|
|
88
|
+
forceModel = DEFAULT_FORCE_MODEL
|
|
89
|
+
} = {}) {
|
|
58
90
|
if (!payload || typeof payload !== 'object') return payload;
|
|
59
91
|
const out = { ...payload };
|
|
60
92
|
|
|
61
93
|
if (stripPreviousResponseId && 'previous_response_id' in out) {
|
|
62
94
|
delete out.previous_response_id;
|
|
63
95
|
}
|
|
96
|
+
const normalizedModel = normalizeModelId(out.model, forceModel);
|
|
97
|
+
if (normalizedModel) {
|
|
98
|
+
out.model = normalizedModel;
|
|
99
|
+
}
|
|
64
100
|
|
|
65
101
|
if (typeof out.input === 'string') {
|
|
66
102
|
out.input = [{ role: 'user', content: normalizeContent(out.input) }];
|
|
@@ -75,6 +111,32 @@ function normalizeResponsesPayload(payload, { stripPreviousResponseId = DEFAULT_
|
|
|
75
111
|
});
|
|
76
112
|
}
|
|
77
113
|
|
|
114
|
+
return pruneResponsesPayload(out);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function normalizeModelId(modelId, forceModel = DEFAULT_FORCE_MODEL) {
|
|
118
|
+
const forced = String(forceModel || '').trim();
|
|
119
|
+
if (forced) return forced;
|
|
120
|
+
if (typeof modelId !== 'string') return '';
|
|
121
|
+
|
|
122
|
+
const raw = modelId.trim();
|
|
123
|
+
if (!raw) return raw;
|
|
124
|
+
if (SUPPORTED_MODEL_IDS.has(raw)) return raw;
|
|
125
|
+
|
|
126
|
+
const slashIndex = raw.lastIndexOf('/');
|
|
127
|
+
if (slashIndex < 0) return raw;
|
|
128
|
+
const tail = raw.slice(slashIndex + 1).trim();
|
|
129
|
+
if (SUPPORTED_MODEL_IDS.has(tail)) return tail;
|
|
130
|
+
return raw;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function pruneResponsesPayload(payload) {
|
|
134
|
+
const out = {};
|
|
135
|
+
for (const [key, value] of Object.entries(payload || {})) {
|
|
136
|
+
if (RESPONSES_KEY_ALLOWLIST.has(key)) {
|
|
137
|
+
out[key] = value;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
78
140
|
return out;
|
|
79
141
|
}
|
|
80
142
|
|
|
@@ -154,6 +216,29 @@ function buildHeaders(baseHeaders, bodyLen) {
|
|
|
154
216
|
return headers;
|
|
155
217
|
}
|
|
156
218
|
|
|
219
|
+
function normalizeHeaderValue(value) {
|
|
220
|
+
if (Array.isArray(value)) return value.join(', ');
|
|
221
|
+
if (value == null) return '';
|
|
222
|
+
return String(value);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function buildCodexLikeHeaders(incomingHeaders, userAgent) {
|
|
226
|
+
const headers = {};
|
|
227
|
+
for (const [rawKey, rawValue] of Object.entries(incomingHeaders || {})) {
|
|
228
|
+
const key = String(rawKey || '').toLowerCase();
|
|
229
|
+
if (!FORWARD_HEADER_ALLOWLIST.has(key)) continue;
|
|
230
|
+
const value = normalizeHeaderValue(rawValue);
|
|
231
|
+
if (value) headers[key] = value;
|
|
232
|
+
}
|
|
233
|
+
headers.accept = 'application/json';
|
|
234
|
+
headers['content-type'] = 'application/json';
|
|
235
|
+
headers['user-agent'] = userAgent;
|
|
236
|
+
headers['x-stainless-lang'] = 'rust';
|
|
237
|
+
headers['x-stainless-package-name'] = 'codex-cli';
|
|
238
|
+
headers['x-stainless-package-version'] = '0.101.0';
|
|
239
|
+
return headers;
|
|
240
|
+
}
|
|
241
|
+
|
|
157
242
|
export async function createProxyServer(options = {}) {
|
|
158
243
|
const port = Number(options.port ?? process.env.SUB2API_COMPAT_PORT ?? DEFAULT_PORT);
|
|
159
244
|
const upstream = options.upstream ?? process.env.SUB2API_UPSTREAM ?? DEFAULT_UPSTREAM;
|
|
@@ -166,6 +251,7 @@ export async function createProxyServer(options = {}) {
|
|
|
166
251
|
?? ['1', 'true', 'yes', 'on'].includes(String(process.env.SUB2API_COMPAT_DROP_TOOLS_ON_COMPACT || '').toLowerCase());
|
|
167
252
|
const stripPreviousResponseId = options.stripPreviousResponseId
|
|
168
253
|
?? ['1', 'true', 'yes', 'on'].includes(String(process.env.SUB2API_COMPAT_STRIP_PREVIOUS_RESPONSE_ID || '').toLowerCase());
|
|
254
|
+
const forceModel = String(options.forceModel ?? process.env.SUB2API_COMPAT_FORCE_MODEL ?? '').trim();
|
|
169
255
|
|
|
170
256
|
const server = http.createServer(async (req, res) => {
|
|
171
257
|
const startAt = Date.now();
|
|
@@ -175,10 +261,7 @@ export async function createProxyServer(options = {}) {
|
|
|
175
261
|
for await (const c of req) chunks.push(c);
|
|
176
262
|
const rawBody = Buffer.concat(chunks);
|
|
177
263
|
|
|
178
|
-
const baseHeaders =
|
|
179
|
-
delete baseHeaders.host;
|
|
180
|
-
delete baseHeaders.connection;
|
|
181
|
-
baseHeaders['user-agent'] = userAgent;
|
|
264
|
+
const baseHeaders = buildCodexLikeHeaders(req.headers, userAgent);
|
|
182
265
|
|
|
183
266
|
const isResponsesPath = req.method === 'POST' && url.pathname.endsWith('/responses');
|
|
184
267
|
let body = rawBody;
|
|
@@ -189,7 +272,12 @@ export async function createProxyServer(options = {}) {
|
|
|
189
272
|
if (isResponsesPath && rawBody.length > 0) {
|
|
190
273
|
try {
|
|
191
274
|
const parsed = JSON.parse(rawBody.toString('utf8'));
|
|
192
|
-
const
|
|
275
|
+
const inputModel = typeof parsed.model === 'string' ? parsed.model : '';
|
|
276
|
+
const normalized = normalizeResponsesPayload(parsed, {
|
|
277
|
+
stripPreviousResponseId,
|
|
278
|
+
forceModel
|
|
279
|
+
});
|
|
280
|
+
const outputModel = typeof normalized.model === 'string' ? normalized.model : '';
|
|
193
281
|
body = Buffer.from(JSON.stringify(normalized));
|
|
194
282
|
if (body.length > maxReqBytes) {
|
|
195
283
|
const compacted = compactResponsesPayload(normalized, { dropToolsOnCompact });
|
|
@@ -214,6 +302,13 @@ export async function createProxyServer(options = {}) {
|
|
|
214
302
|
minimalBody = minimalBuf;
|
|
215
303
|
}
|
|
216
304
|
}
|
|
305
|
+
if (inputModel && outputModel && inputModel !== outputModel) {
|
|
306
|
+
appendLog(logFile, {
|
|
307
|
+
method: req.method,
|
|
308
|
+
path: url.pathname,
|
|
309
|
+
modelRewrite: { from: inputModel, to: outputModel }
|
|
310
|
+
});
|
|
311
|
+
}
|
|
217
312
|
} catch {
|
|
218
313
|
// Keep original body if parse fails.
|
|
219
314
|
}
|
|
@@ -268,6 +363,9 @@ export async function createProxyServer(options = {}) {
|
|
|
268
363
|
});
|
|
269
364
|
|
|
270
365
|
const outBuffer = Buffer.from(await resp.arrayBuffer());
|
|
366
|
+
const upstreamError = resp.status >= 500
|
|
367
|
+
? outBuffer.toString('utf8').replace(/\s+/g, ' ').slice(0, 240)
|
|
368
|
+
: '';
|
|
271
369
|
appendLog(logFile, {
|
|
272
370
|
method: req.method,
|
|
273
371
|
path: url.pathname,
|
|
@@ -279,6 +377,7 @@ export async function createProxyServer(options = {}) {
|
|
|
279
377
|
reqBytes: rawBody.length,
|
|
280
378
|
upstreamReqBytes: requestBodies[Math.min(requestBodies.length - 1, attempts - 1)].length,
|
|
281
379
|
respBytes: outBuffer.length,
|
|
380
|
+
...(upstreamError ? { upstreamError } : {}),
|
|
282
381
|
durationMs: Date.now() - startAt
|
|
283
382
|
});
|
|
284
383
|
res.end(outBuffer);
|