@autotask/atools-tool 0.1.6 → 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 CHANGED
@@ -68,14 +68,13 @@ 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 enables proxy model rewrite:
72
-
73
- - force outbound model to `gpt-5.3-codex` (for codex-only upstream compatibility)
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.
74
73
 
75
74
  ## Proxy Command
76
75
 
77
76
  ```bash
78
- atools-tool serve --port 18888 --upstream https://sub2api.atools.live --force-model gpt-5.3-codex
77
+ atools-tool serve --port 18888 --upstream https://sub2api.atools.live
79
78
  ```
80
79
 
81
80
  ## Repo Install
@@ -16,7 +16,6 @@ const PROXY_HOST = '127.0.0.1';
16
16
  const PROXY_PORT = 18888;
17
17
  const PROXY_BASE_URL = `http://${PROXY_HOST}:${PROXY_PORT}/v1`;
18
18
  const PROXY_UPSTREAM = 'https://sub2api.atools.live';
19
- const PROXY_FORCE_MODEL = 'gpt-5.3-codex';
20
19
  const PROXY_LOG_FILE = path.join(os.tmpdir(), 'openclaw', 'atools-compat-proxy.log');
21
20
  const PROXY_SERVICE = 'openclaw-atools-proxy.service';
22
21
 
@@ -259,6 +258,11 @@ function upsertEnvLine(content, key, value) {
259
258
  return `${content.slice(0, afterServiceHeader + 1)}${line}\n${content.slice(afterServiceHeader + 1)}`;
260
259
  }
261
260
 
261
+ function removeEnvLine(content, key) {
262
+ const re = new RegExp(`^Environment=${escapeRegExp(key)}=.*\\n?`, 'gm');
263
+ return content.replace(re, '');
264
+ }
265
+
262
266
  function configureLinuxProxyService() {
263
267
  const servicePath = path.join(os.homedir(), '.config/systemd/user', PROXY_SERVICE);
264
268
  if (!fs.existsSync(servicePath)) {
@@ -267,7 +271,7 @@ function configureLinuxProxyService() {
267
271
  const original = fs.readFileSync(servicePath, 'utf8');
268
272
  let next = original;
269
273
  next = upsertEnvLine(next, 'SUB2API_UPSTREAM', PROXY_UPSTREAM);
270
- next = upsertEnvLine(next, 'SUB2API_COMPAT_FORCE_MODEL', PROXY_FORCE_MODEL);
274
+ next = removeEnvLine(next, 'SUB2API_COMPAT_FORCE_MODEL');
271
275
  next = upsertEnvLine(next, 'SUB2API_COMPAT_DROP_TOOLS_ON_COMPACT', '0');
272
276
  next = upsertEnvLine(next, 'SUB2API_COMPAT_PORT', String(PROXY_PORT));
273
277
  next = upsertEnvLine(next, 'SUB2API_COMPAT_LOG', PROXY_LOG_FILE);
@@ -317,8 +321,7 @@ function startDetachedProxy() {
317
321
  stdio: 'ignore',
318
322
  env: {
319
323
  ...process.env,
320
- SUB2API_UPSTREAM: PROXY_UPSTREAM,
321
- SUB2API_COMPAT_FORCE_MODEL: PROXY_FORCE_MODEL
324
+ SUB2API_UPSTREAM: PROXY_UPSTREAM
322
325
  }
323
326
  }
324
327
  );
package/lib/install.mjs CHANGED
@@ -6,7 +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 = 'gpt-5.3-codex';
9
+ const DEFAULT_FORCE_MODEL = '';
10
10
  const DEFAULT_PORT = 18888;
11
11
  const DEFAULT_UPSTREAM = 'https://sub2api.atools.live';
12
12
  const DEFAULT_LOG_FILE = path.join(os.tmpdir(), 'openclaw', 'atools-compat-proxy.log');
@@ -486,7 +486,7 @@ Options:
486
486
  --openclaw-home <path> Default: ~/.openclaw
487
487
  --provider <name> Default: sub2api
488
488
  --model <id> Default: gpt-5.3-codex
489
- --force-model <id> Default: gpt-5.3-codex (proxy rewrites outbound model)
489
+ --force-model <id> Default: disabled (optional hard rewrite)
490
490
  --port <number> Default: 18888
491
491
  --upstream <url> Default: https://sub2api.atools.live
492
492
  --max-req-bytes <number> Default: 68000
@@ -10,6 +10,34 @@ 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
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
+ ]);
13
41
 
14
42
  function ensureParentDir(filePath) {
15
43
  const dir = path.dirname(filePath);
@@ -65,8 +93,9 @@ function normalizeResponsesPayload(payload, {
65
93
  if (stripPreviousResponseId && 'previous_response_id' in out) {
66
94
  delete out.previous_response_id;
67
95
  }
68
- if (forceModel) {
69
- out.model = forceModel;
96
+ const normalizedModel = normalizeModelId(out.model, forceModel);
97
+ if (normalizedModel) {
98
+ out.model = normalizedModel;
70
99
  }
71
100
 
72
101
  if (typeof out.input === 'string') {
@@ -82,6 +111,32 @@ function normalizeResponsesPayload(payload, {
82
111
  });
83
112
  }
84
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
+ }
85
140
  return out;
86
141
  }
87
142
 
@@ -161,6 +216,29 @@ function buildHeaders(baseHeaders, bodyLen) {
161
216
  return headers;
162
217
  }
163
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
+
164
242
  export async function createProxyServer(options = {}) {
165
243
  const port = Number(options.port ?? process.env.SUB2API_COMPAT_PORT ?? DEFAULT_PORT);
166
244
  const upstream = options.upstream ?? process.env.SUB2API_UPSTREAM ?? DEFAULT_UPSTREAM;
@@ -183,10 +261,7 @@ export async function createProxyServer(options = {}) {
183
261
  for await (const c of req) chunks.push(c);
184
262
  const rawBody = Buffer.concat(chunks);
185
263
 
186
- const baseHeaders = { ...req.headers };
187
- delete baseHeaders.host;
188
- delete baseHeaders.connection;
189
- baseHeaders['user-agent'] = userAgent;
264
+ const baseHeaders = buildCodexLikeHeaders(req.headers, userAgent);
190
265
 
191
266
  const isResponsesPath = req.method === 'POST' && url.pathname.endsWith('/responses');
192
267
  let body = rawBody;
@@ -227,7 +302,7 @@ export async function createProxyServer(options = {}) {
227
302
  minimalBody = minimalBuf;
228
303
  }
229
304
  }
230
- if (forceModel && inputModel && inputModel !== outputModel) {
305
+ if (inputModel && outputModel && inputModel !== outputModel) {
231
306
  appendLog(logFile, {
232
307
  method: req.method,
233
308
  path: url.pathname,
@@ -288,6 +363,9 @@ export async function createProxyServer(options = {}) {
288
363
  });
289
364
 
290
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
+ : '';
291
369
  appendLog(logFile, {
292
370
  method: req.method,
293
371
  path: url.pathname,
@@ -299,6 +377,7 @@ export async function createProxyServer(options = {}) {
299
377
  reqBytes: rawBody.length,
300
378
  upstreamReqBytes: requestBodies[Math.min(requestBodies.length - 1, attempts - 1)].length,
301
379
  respBytes: outBuffer.length,
380
+ ...(upstreamError ? { upstreamError } : {}),
302
381
  durationMs: Date.now() - startAt
303
382
  });
304
383
  res.end(outBuffer);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autotask/atools-tool",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "ATools CLI for OpenClaw proxy compatibility and interactive model configuration",
5
5
  "type": "module",
6
6
  "license": "MIT",