@jiayunxie/aerial 0.1.8 → 0.1.10
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 +4 -4
- package/docs/usage.md +10 -2
- package/package.json +1 -1
- package/src/cli.js +89 -26
- package/src/config.js +4 -1
- package/src/copilot.js +49 -47
- package/src/doctor.js +191 -11
- package/src/model-catalog.js +69 -0
- package/src/service.js +139 -10
- package/src/setup-selection.js +71 -0
- package/src/setup.js +30 -16
package/README.md
CHANGED
|
@@ -73,18 +73,18 @@ You normally do not need to create, copy, or export an API key yourself.
|
|
|
73
73
|
|
|
74
74
|
## Choosing A Model
|
|
75
75
|
|
|
76
|
-
The easiest path is to let setup show the compatible models:
|
|
76
|
+
The easiest path is to let setup show the compatible models and ask for reasoning effort:
|
|
77
77
|
|
|
78
78
|
```bash
|
|
79
79
|
aerial setup codex
|
|
80
80
|
aerial setup claude
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
-
To
|
|
83
|
+
To skip the prompts:
|
|
84
84
|
|
|
85
85
|
```bash
|
|
86
|
-
aerial setup codex --model <responses-model-id>
|
|
87
|
-
aerial setup claude --model <messages-model-id>
|
|
86
|
+
aerial setup codex --model <responses-model-id> --effort <low|medium|high|xhigh|max>
|
|
87
|
+
aerial setup claude --model <messages-model-id> --effort <low|medium|high|xhigh|max>
|
|
88
88
|
```
|
|
89
89
|
|
|
90
90
|
To inspect the full model matrix:
|
package/docs/usage.md
CHANGED
|
@@ -71,7 +71,7 @@ The local key is generated and stored by Aerial automatically. Users do not need
|
|
|
71
71
|
|
|
72
72
|
For a dry inspection without touching your real config, set `HOME`/`USERPROFILE` to a temporary directory before running this command.
|
|
73
73
|
|
|
74
|
-
To skip the
|
|
74
|
+
To skip the prompts, pass `--model <responses-model-id>` and/or `--effort <low|medium|high|xhigh|max>` (`max` is an alias for `xhigh`). The chosen effort is written into the `[profiles.aerial]` block as `model_reasoning_effort = "<effort>"` and is also persisted as Aerial-wide `defaultEffort` in `~/.aerial/config.json`. Under non-TTY (CI/pipes) the wizard does not prompt and falls back to the default effort `medium`.
|
|
75
75
|
|
|
76
76
|
## 6. Configure Claude Code
|
|
77
77
|
|
|
@@ -85,7 +85,15 @@ If Claude Code was previously pointed at another Anthropic-compatible gateway, s
|
|
|
85
85
|
|
|
86
86
|
For a dry inspection without touching your real config, set `HOME`/`USERPROFILE` to a temporary directory before running this command.
|
|
87
87
|
|
|
88
|
-
To skip the
|
|
88
|
+
To skip the prompts, pass `--model <messages-model-id>` and/or `--effort <low|medium|high|xhigh|max>`. Claude `settings.json` does not store an effort value; instead, `setup claude --effort <value>` updates Aerial-wide `defaultEffort` and the local proxy injects it into outgoing `/v1/messages` payloads. Precedence for the Anthropic effort applied to a Claude Opus 4.7 request, in order:
|
|
89
|
+
|
|
90
|
+
1. An explicit `output_config.effort` in the request body is preserved verbatim.
|
|
91
|
+
2. A legacy `thinking: { type: "enabled", budget_tokens }` is translated to `thinking: { type: "adaptive" }` with a derived `output_config.effort`.
|
|
92
|
+
3. `thinking: { type: "adaptive" }` without an explicit effort is preserved as-is; Aerial does not inject a default.
|
|
93
|
+
4. Otherwise Aerial injects Aerial `defaultEffort` from `~/.aerial/config.json` as `output_config.effort`.
|
|
94
|
+
5. If no Aerial default is set, Aerial falls back to `medium`.
|
|
95
|
+
|
|
96
|
+
Non-Opus-4.7 Claude requests are not modified.
|
|
89
97
|
|
|
90
98
|
## 7. Verify
|
|
91
99
|
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
import { startDeviceFlow, pollDeviceFlow, readGitHubToken, gitHubTokenSource } from "./auth.js";
|
|
4
4
|
import { ensureApiKey, loadConfig, saveConfig } from "./config.js";
|
|
5
|
+
import { configPath } from "./paths.js";
|
|
5
6
|
import { startServer } from "./server.js";
|
|
6
7
|
import { setupClaude, setupCodex, setupStatus, restoreClient, restoreAllClients } from "./setup.js";
|
|
7
8
|
import { serviceInstall, serviceStart, serviceStop, serviceRestart, serviceUninstall, serviceStatus } from "./service.js";
|
|
8
|
-
import { doctor } from "./doctor.js";
|
|
9
|
+
import { doctor, renderDoctorText } from "./doctor.js";
|
|
9
10
|
import { runProbe, formatProbeReport } from "./probe.js";
|
|
10
11
|
import { chooseSetupModel, formatModelChoices } from "./model-selection.js";
|
|
12
|
+
import { chooseSetupEffort, formatEffortSelection, assertValidEffort } from "./setup-selection.js";
|
|
11
13
|
import { printVersion } from "./version.js";
|
|
12
14
|
import { computeAppStatus } from "./app-status.js";
|
|
13
15
|
|
|
@@ -36,8 +38,8 @@ function printHelp() {
|
|
|
36
38
|
Usage:
|
|
37
39
|
aerial --version
|
|
38
40
|
aerial login
|
|
39
|
-
aerial setup codex [--model <id>]
|
|
40
|
-
aerial setup claude [--model <id>]
|
|
41
|
+
aerial setup codex [--model <id>] [--effort <low|medium|high|xhigh|max>]
|
|
42
|
+
aerial setup claude [--model <id>] [--effort <low|medium|high|xhigh|max>]
|
|
41
43
|
aerial service install
|
|
42
44
|
aerial status [--json]
|
|
43
45
|
|
|
@@ -58,8 +60,20 @@ function argValue(args, name) {
|
|
|
58
60
|
return index >= 0 ? args[index + 1] : undefined;
|
|
59
61
|
}
|
|
60
62
|
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
+
function requiredArgValue(args, name) {
|
|
64
|
+
const index = args.indexOf(name);
|
|
65
|
+
if (index < 0) return undefined;
|
|
66
|
+
const value = args[index + 1];
|
|
67
|
+
if (value === undefined || value.startsWith("--")) {
|
|
68
|
+
throw new Error(`${name} requires a value.`);
|
|
69
|
+
}
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function selectSetupOptions(target, route, args) {
|
|
74
|
+
const explicitEffort = requiredArgValue(args, "--effort");
|
|
75
|
+
if (explicitEffort !== undefined) assertValidEffort(explicitEffort);
|
|
76
|
+
const selected = await chooseSetupModel({ target, route, explicitModel: requiredArgValue(args, "--model") });
|
|
63
77
|
if (!selected.displayed) {
|
|
64
78
|
for (const line of formatModelChoices({ target, route, choices: selected.choices, selectedModel: selected.model, source: selected.source, recommended: selected.recommended })) {
|
|
65
79
|
console.log(line);
|
|
@@ -67,14 +81,38 @@ async function selectSetupModel(target, route, args) {
|
|
|
67
81
|
} else {
|
|
68
82
|
console.log(`Selected ${target} model: ${selected.model}`);
|
|
69
83
|
}
|
|
70
|
-
|
|
84
|
+
const effortChoice = await chooseSetupEffort({ target, explicitEffort });
|
|
85
|
+
console.log(formatEffortSelection({ target, effort: effortChoice.effort, source: effortChoice.source }));
|
|
86
|
+
return {
|
|
87
|
+
model: selected.model,
|
|
88
|
+
effort: effortChoice.effort,
|
|
89
|
+
modelSource: selected.source,
|
|
90
|
+
effortSource: effortChoice.source,
|
|
91
|
+
modelDisplayed: Boolean(selected.displayed),
|
|
92
|
+
effortDisplayed: Boolean(effortChoice.displayed)
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function printSetupCompletionSummary({ heading, cli, model, effort, proxy, configFile, aerialConfigFile, aerialDefaultEffort, backup, auth, notes = [] }) {
|
|
97
|
+
console.log(heading);
|
|
98
|
+
console.log(` cli: ${cli}`);
|
|
99
|
+
console.log(` model: ${model}`);
|
|
100
|
+
console.log(` effort: ${effort}`);
|
|
101
|
+
console.log(` proxy: ${proxy}`);
|
|
102
|
+
console.log(` client config: ${configFile}`);
|
|
103
|
+
console.log(` aerial config: ${aerialConfigFile}`);
|
|
104
|
+
console.log(` aerial defaultEffort: ${aerialDefaultEffort}`);
|
|
105
|
+
console.log(` backup: ${backup || "none"}`);
|
|
106
|
+
console.log(` auth: ${auth}`);
|
|
107
|
+
for (const note of notes) console.log(` note: ${note}`);
|
|
71
108
|
}
|
|
72
109
|
|
|
73
110
|
function printSetupSummary(status) {
|
|
74
111
|
console.log("clients:");
|
|
75
112
|
for (const client of Object.values(status.clients)) {
|
|
76
113
|
const model = client.model ? ` model=${client.model}` : "";
|
|
77
|
-
|
|
114
|
+
const effort = ` effort=${client.effort || "missing"}`;
|
|
115
|
+
console.log(` ${client.target}: ${client.state}${model}${effort}`);
|
|
78
116
|
}
|
|
79
117
|
console.log(`api key: ${status.auth.api_key.exists ? "present" : "missing"}`);
|
|
80
118
|
const ghSource = status.auth.github_token.source;
|
|
@@ -186,21 +224,41 @@ async function main() {
|
|
|
186
224
|
|
|
187
225
|
if (command === "setup") {
|
|
188
226
|
if (subcommand === "codex") {
|
|
189
|
-
const
|
|
190
|
-
const result = setupCodex({ model, authCommand: codexAuthCommand() });
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
227
|
+
const options = await selectSetupOptions("Codex", "responses", rest);
|
|
228
|
+
const result = setupCodex({ model: options.model, effort: options.effort, authCommand: codexAuthCommand() });
|
|
229
|
+
const config = loadConfig();
|
|
230
|
+
printSetupCompletionSummary({
|
|
231
|
+
heading: "Configured Codex",
|
|
232
|
+
cli: "Codex",
|
|
233
|
+
model: result.model,
|
|
234
|
+
effort: result.effort || "missing",
|
|
235
|
+
proxy: `http://${config.host}:${config.port}/v1`,
|
|
236
|
+
configFile: result.file,
|
|
237
|
+
aerialConfigFile: configPath(),
|
|
238
|
+
aerialDefaultEffort: config.defaultEffort || "missing",
|
|
239
|
+
backup: result.backup,
|
|
240
|
+
auth: "command-backed local Aerial key",
|
|
241
|
+
notes: ["restart Codex if it was already running so it reloads the profile."]
|
|
242
|
+
});
|
|
195
243
|
return;
|
|
196
244
|
}
|
|
197
245
|
if (subcommand === "claude") {
|
|
198
|
-
const
|
|
199
|
-
const result = setupClaude({ model, apiKeyHelper: claudeApiKeyHelper() });
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
246
|
+
const options = await selectSetupOptions("Claude Code", "messages", rest);
|
|
247
|
+
const result = setupClaude({ model: options.model, effort: options.effort, apiKeyHelper: claudeApiKeyHelper() });
|
|
248
|
+
const config = loadConfig();
|
|
249
|
+
printSetupCompletionSummary({
|
|
250
|
+
heading: "Configured Claude Code",
|
|
251
|
+
cli: "Claude Code",
|
|
252
|
+
model: result.model || "preserved",
|
|
253
|
+
effort: result.effort || config.defaultEffort || "missing",
|
|
254
|
+
proxy: `http://${config.host}:${config.port}`,
|
|
255
|
+
configFile: result.file,
|
|
256
|
+
aerialConfigFile: configPath(),
|
|
257
|
+
aerialDefaultEffort: config.defaultEffort || "missing",
|
|
258
|
+
backup: result.backup,
|
|
259
|
+
auth: "apiKeyHelper local Aerial key",
|
|
260
|
+
notes: ["effort is applied via Aerial defaultEffort and proxy fallback; Claude settings.json does not store an effort value."]
|
|
261
|
+
});
|
|
204
262
|
return;
|
|
205
263
|
}
|
|
206
264
|
if (subcommand === "all") {
|
|
@@ -222,7 +280,8 @@ async function main() {
|
|
|
222
280
|
console.log(`GitHub token: ${ghSourceText}`);
|
|
223
281
|
for (const cs of Object.values(status.clients)) {
|
|
224
282
|
const head = `${cs.target.padEnd(7)} state=${cs.state}`;
|
|
225
|
-
|
|
283
|
+
const effortText = ` effort=${cs.effort || "missing"}`;
|
|
284
|
+
console.log(`${head}${effortText} file=${cs.file}`);
|
|
226
285
|
if (cs.backups.length) console.log(` backups=${cs.backups.length}`);
|
|
227
286
|
if (cs.error) console.log(` error=${cs.error}`);
|
|
228
287
|
}
|
|
@@ -449,11 +508,10 @@ async function main() {
|
|
|
449
508
|
}
|
|
450
509
|
|
|
451
510
|
if (command === "doctor") {
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
process.exitCode = result.ok ? 0 : 1;
|
|
511
|
+
const report = await doctor();
|
|
512
|
+
if (args.includes("--json")) console.log(JSON.stringify(report, null, 2));
|
|
513
|
+
else console.log(renderDoctorText(report));
|
|
514
|
+
process.exitCode = report.ok ? 0 : 1;
|
|
457
515
|
return;
|
|
458
516
|
}
|
|
459
517
|
|
|
@@ -470,9 +528,14 @@ async function main() {
|
|
|
470
528
|
if (subcommand === "set") {
|
|
471
529
|
const [key, value] = rest;
|
|
472
530
|
if (!key || value === undefined) throw new Error("Usage: aerial config set <key> <value>");
|
|
473
|
-
if (!["host", "port", "defaultModel", "logLevel", "promptCacheRetention", "promptCacheKey"].includes(key)) throw new Error(`Unsupported config key: ${key}`);
|
|
531
|
+
if (!["host", "port", "defaultModel", "defaultEffort", "logLevel", "promptCacheRetention", "promptCacheKey"].includes(key)) throw new Error(`Unsupported config key: ${key}`);
|
|
474
532
|
if (key === "promptCacheRetention" && !["in_memory", "24h", "off"].includes(value)) throw new Error("promptCacheRetention must be one of: in_memory, 24h, off");
|
|
475
533
|
if (key === "promptCacheKey" && !value.trim()) throw new Error("promptCacheKey must be auto, off, or a non-empty string");
|
|
534
|
+
if (key === "defaultEffort") {
|
|
535
|
+
config.defaultEffort = assertValidEffort(value);
|
|
536
|
+
saveConfig(config);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
476
539
|
config[key] = key === "port" ? Number(value) : value;
|
|
477
540
|
saveConfig(config);
|
|
478
541
|
return;
|
package/src/config.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import { CONFIG_VERSION, DEFAULT_HOST, DEFAULT_PORT, DEFAULT_VERSIONS } from "./constants.js";
|
|
3
3
|
import { apiKeyPath, configPath, readJsonIfExists, writeJsonPrivate, writePrivateFile } from "./paths.js";
|
|
4
4
|
import { hashApiKey, randomApiKey, verifyApiKey } from "./crypto.js";
|
|
5
|
+
import { DEFAULT_EFFORT, normalizeEffort } from "./setup-selection.js";
|
|
5
6
|
|
|
6
7
|
export function defaultConfig() {
|
|
7
8
|
return {
|
|
@@ -10,6 +11,7 @@ export function defaultConfig() {
|
|
|
10
11
|
port: DEFAULT_PORT,
|
|
11
12
|
apiKeyHash: undefined,
|
|
12
13
|
defaultModel: undefined,
|
|
14
|
+
defaultEffort: DEFAULT_EFFORT,
|
|
13
15
|
logLevel: "info",
|
|
14
16
|
promptCacheRetention: "in_memory",
|
|
15
17
|
promptCacheKey: "auto",
|
|
@@ -24,7 +26,8 @@ export function loadConfig() {
|
|
|
24
26
|
const defaults = defaultConfig();
|
|
25
27
|
const promptCacheRetention = envRetention === undefined ? (loaded.promptCacheRetention ?? defaults.promptCacheRetention) : envRetention;
|
|
26
28
|
const promptCacheKey = envCacheKey === undefined ? (loaded.promptCacheKey ?? defaults.promptCacheKey) : envCacheKey;
|
|
27
|
-
|
|
29
|
+
const defaultEffort = normalizeEffort(loaded.defaultEffort) || DEFAULT_EFFORT;
|
|
30
|
+
return { ...defaults, ...loaded, defaultEffort, promptCacheRetention, promptCacheKey, versions: { ...DEFAULT_VERSIONS, ...(loaded.versions || {}) } };
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
export function saveConfig(config) {
|
package/src/copilot.js
CHANGED
|
@@ -4,6 +4,12 @@ import { loadConfig } from "./config.js";
|
|
|
4
4
|
import { getCopilotToken } from "./auth.js";
|
|
5
5
|
import { logEvent } from "./log.js";
|
|
6
6
|
import { isResponsesWebSocketOptIn, proxyResponsesWebSocket, shouldUseResponsesWebSocket } from "./responses-websocket.js";
|
|
7
|
+
import {
|
|
8
|
+
fetchModelsCatalog as fetchModelsCatalogShared,
|
|
9
|
+
findCompatibleModel as findCompatibleModelShared,
|
|
10
|
+
canonicalClaudeFamily,
|
|
11
|
+
tokenFingerprintOf
|
|
12
|
+
} from "./model-catalog.js";
|
|
7
13
|
|
|
8
14
|
function upstreamHeaders(token, extra = {}) {
|
|
9
15
|
const config = loadConfig();
|
|
@@ -174,51 +180,22 @@ function withDefaultAnthropicCache(payload) {
|
|
|
174
180
|
return next;
|
|
175
181
|
}
|
|
176
182
|
|
|
177
|
-
function canonicalClaudeOpus47Family(model) {
|
|
178
|
-
return /^claude-opus-4[.-]7(?:-|$)/.test(model) ? "claude-opus-4.7" : undefined;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function modelHasMessagesRoute(model) {
|
|
182
|
-
const endpoints = Array.isArray(model?.supported_endpoints) ? model.supported_endpoints : [];
|
|
183
|
-
const routes = Array.isArray(model?.aerial?.routes) ? model.aerial.routes : [];
|
|
184
|
-
return endpoints.includes("/v1/messages") || routes.includes("messages");
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function supportedReasoningEfforts(model) {
|
|
188
|
-
const supports = model?.capabilities?.supports;
|
|
189
|
-
const values = supports?.reasoning_effort ?? supports?.reasoning_efforts;
|
|
190
|
-
if (Array.isArray(values)) return values.map(String);
|
|
191
|
-
if (typeof values === "string") return [values];
|
|
192
|
-
return [];
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function modelSupportsAdaptiveThinking(model) {
|
|
196
|
-
const supports = model?.capabilities?.supports;
|
|
197
|
-
return supports?.adaptive_thinking === true || supports?.thinking?.adaptive === true;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function findAnthropicEffortModel(modelId, effort, models) {
|
|
201
|
-
if (!Array.isArray(models)) return undefined;
|
|
202
|
-
const family = canonicalClaudeOpus47Family(modelId);
|
|
203
|
-
if (!family) return undefined;
|
|
204
|
-
const candidates = models.filter((model) => {
|
|
205
|
-
const id = typeof model?.id === "string" ? model.id : "";
|
|
206
|
-
return canonicalClaudeOpus47Family(id) === family &&
|
|
207
|
-
modelHasMessagesRoute(model) &&
|
|
208
|
-
modelSupportsAdaptiveThinking(model) &&
|
|
209
|
-
supportedReasoningEfforts(model).includes(effort);
|
|
210
|
-
});
|
|
211
|
-
return candidates.find((model) => model.id === modelId) || candidates[0];
|
|
212
|
-
}
|
|
213
|
-
|
|
214
183
|
function withSupportedAnthropicEffort(payload, models) {
|
|
215
184
|
const effort = payload?.output_config?.effort;
|
|
216
185
|
if (effort === undefined) return payload;
|
|
217
186
|
const model = typeof payload?.model === "string" ? payload.model : "";
|
|
218
|
-
|
|
187
|
+
const family = canonicalClaudeFamily(model);
|
|
188
|
+
if (!family) return payload;
|
|
219
189
|
const nextEffort = effort === "max" ? "xhigh" : effort;
|
|
220
190
|
if (!["low", "medium", "high", "xhigh"].includes(nextEffort)) return payload;
|
|
221
|
-
const routed =
|
|
191
|
+
const routed = findCompatibleModelShared({
|
|
192
|
+
models,
|
|
193
|
+
family,
|
|
194
|
+
route: "/v1/messages",
|
|
195
|
+
adaptiveThinking: true,
|
|
196
|
+
effort: nextEffort,
|
|
197
|
+
preferredId: model
|
|
198
|
+
});
|
|
222
199
|
const nextModel = routed?.id || model;
|
|
223
200
|
if (model === nextModel && effort === nextEffort) return payload;
|
|
224
201
|
logEvent("anthropic_effort_route", { model, effort, routedModel: nextModel, routedEffort: nextEffort });
|
|
@@ -258,13 +235,42 @@ function withSupportedAnthropicThinking(payload) {
|
|
|
258
235
|
function shouldLoadAnthropicCatalog(payload) {
|
|
259
236
|
const effort = payload?.output_config?.effort;
|
|
260
237
|
const model = typeof payload?.model === "string" ? payload.model : "";
|
|
261
|
-
return effort !== undefined && Boolean(
|
|
238
|
+
return effort !== undefined && Boolean(canonicalClaudeFamily(model));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function fetchModelsCatalogForCopilot() {
|
|
242
|
+
const token = await getCopilotToken();
|
|
243
|
+
return fetchModelsCatalogShared({
|
|
244
|
+
tokenFingerprint: tokenFingerprintOf(token),
|
|
245
|
+
fetchImpl: async () => {
|
|
246
|
+
const response = await fetch(`${COPILOT_API_ORIGIN}/models`, { method: "GET", headers: upstreamHeaders(token) });
|
|
247
|
+
if (!response.ok) return undefined;
|
|
248
|
+
const payload = await response.json().catch(() => ({}));
|
|
249
|
+
return Array.isArray(payload?.data) ? payload.data : undefined;
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function withDefaultAnthropicEffort(payload) {
|
|
255
|
+
if (payload?.output_config?.effort !== undefined) return payload;
|
|
256
|
+
if (payload?.thinking?.type === "adaptive") return payload;
|
|
257
|
+
const model = typeof payload?.model === "string" ? payload.model : "";
|
|
258
|
+
if (!canonicalClaudeFamily(model)) return payload;
|
|
259
|
+
const config = loadConfig();
|
|
260
|
+
const effort = config.defaultEffort || "medium";
|
|
261
|
+
const outputConfig = payload?.output_config && typeof payload.output_config === "object" && !Array.isArray(payload.output_config)
|
|
262
|
+
? payload.output_config
|
|
263
|
+
: {};
|
|
264
|
+
logEvent("anthropic_default_effort", { model, effort });
|
|
265
|
+
return { ...payload, output_config: { ...outputConfig, effort } };
|
|
262
266
|
}
|
|
263
267
|
|
|
264
268
|
async function withAnthropicDefaults(payload) {
|
|
265
|
-
const
|
|
269
|
+
const cached = withDefaultAnthropicCache(payload);
|
|
270
|
+
const thinkingApplied = withSupportedAnthropicThinking(cached);
|
|
271
|
+
const next = withDefaultAnthropicEffort(thinkingApplied);
|
|
266
272
|
const models = shouldLoadAnthropicCatalog(next)
|
|
267
|
-
? await
|
|
273
|
+
? await fetchModelsCatalogForCopilot().catch(() => undefined)
|
|
268
274
|
: undefined;
|
|
269
275
|
return withSupportedAnthropicEffort(next, models);
|
|
270
276
|
}
|
|
@@ -480,11 +486,7 @@ async function proxyFetch(path, request, { extraHeaders = {}, bodyOverride } = {
|
|
|
480
486
|
}
|
|
481
487
|
|
|
482
488
|
async function fetchModelsCatalog() {
|
|
483
|
-
|
|
484
|
-
const response = await fetch(`${COPILOT_API_ORIGIN}/models`, { method: "GET", headers: upstreamHeaders(token) });
|
|
485
|
-
if (!response.ok) return undefined;
|
|
486
|
-
const payload = await response.json().catch(() => ({}));
|
|
487
|
-
return Array.isArray(payload.data) ? payload.data : undefined;
|
|
489
|
+
return fetchModelsCatalogForCopilot();
|
|
488
490
|
}
|
|
489
491
|
|
|
490
492
|
function responseModelFromCatalog(modelId, models) {
|
package/src/doctor.js
CHANGED
|
@@ -1,15 +1,195 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { setupStatus } from "./setup.js";
|
|
2
|
+
import { serviceStatus } from "./service.js";
|
|
3
|
+
import { computeAppStatus } from "./app-status.js";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
const REPAIRS = Object.freeze({
|
|
6
|
+
LOGIN: Object.freeze({ command: "aerial", args: ["login"] }),
|
|
7
|
+
SETUP_CODEX: Object.freeze({ command: "aerial", args: ["setup", "codex"] }),
|
|
8
|
+
SERVICE_INSTALL: Object.freeze({ command: "aerial", args: ["service", "install"] }),
|
|
9
|
+
SERVICE_STATUS_JSON: Object.freeze({ command: "aerial", args: ["service", "status", "--json"] })
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
function buildAuthChecks(setup) {
|
|
13
|
+
const checks = [];
|
|
14
|
+
const apiKey = setup.auth.api_key;
|
|
15
|
+
if (!apiKey.exists) {
|
|
16
|
+
checks.push({
|
|
17
|
+
id: "auth.api_key",
|
|
18
|
+
ok: false,
|
|
19
|
+
severity: "fail",
|
|
20
|
+
message: "Aerial local API key is missing. Run setup for the client you use; this repair shows the Codex path.",
|
|
21
|
+
repair: REPAIRS.SETUP_CODEX
|
|
22
|
+
});
|
|
23
|
+
} else {
|
|
24
|
+
checks.push({ id: "auth.api_key", ok: true, severity: "info", message: "Aerial local API key is present." });
|
|
25
|
+
}
|
|
26
|
+
const gh = setup.auth.github_token;
|
|
27
|
+
if (gh.source === "missing") {
|
|
28
|
+
checks.push({
|
|
29
|
+
id: "auth.github_token",
|
|
30
|
+
ok: false,
|
|
31
|
+
severity: "fail",
|
|
32
|
+
message: "GitHub token is not configured. Aerial cannot reach Copilot until you log in.",
|
|
33
|
+
repair: REPAIRS.LOGIN
|
|
34
|
+
});
|
|
35
|
+
} else if (gh.source === "env") {
|
|
36
|
+
checks.push({
|
|
37
|
+
id: "auth.github_token",
|
|
38
|
+
ok: true,
|
|
39
|
+
severity: "warn",
|
|
40
|
+
message: "AERIAL_GITHUB_TOKEN is set in this shell only. The background service does not inherit it; run `aerial login` to persist a service-readable token.",
|
|
41
|
+
repair: REPAIRS.LOGIN
|
|
42
|
+
});
|
|
43
|
+
} else {
|
|
44
|
+
checks.push({ id: "auth.github_token", ok: true, severity: "info", message: "GitHub token is present in the persisted credential file." });
|
|
45
|
+
}
|
|
46
|
+
return checks;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function buildClientChecks(setup) {
|
|
50
|
+
const checks = [];
|
|
51
|
+
const codex = setup.clients.codex;
|
|
52
|
+
const claude = setup.clients.claude;
|
|
53
|
+
const aerialCodex = codex.state === "aerial";
|
|
54
|
+
const aerialClaude = claude.state === "aerial";
|
|
55
|
+
if (!aerialCodex && !aerialClaude) {
|
|
56
|
+
checks.push({
|
|
57
|
+
id: "clients.aerial_client",
|
|
58
|
+
ok: false,
|
|
59
|
+
severity: "fail",
|
|
60
|
+
message: "No client is wired to Aerial. Run setup for the client you use; this repair shows the Codex path.",
|
|
61
|
+
repair: REPAIRS.SETUP_CODEX
|
|
62
|
+
});
|
|
63
|
+
} else {
|
|
64
|
+
const wired = [aerialCodex && "codex", aerialClaude && "claude"].filter(Boolean).join(", ");
|
|
65
|
+
checks.push({ id: "clients.aerial_client", ok: true, severity: "info", message: `Client wired to Aerial: ${wired}.` });
|
|
66
|
+
}
|
|
67
|
+
return checks;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function buildServiceChecks(service) {
|
|
71
|
+
const checks = [];
|
|
72
|
+
if (service.supported === false) {
|
|
73
|
+
checks.push({
|
|
74
|
+
id: "service.supported",
|
|
75
|
+
ok: false,
|
|
76
|
+
severity: "warn",
|
|
77
|
+
message: `Background service is not supported on ${service.platform}; Aerial must run in the foreground.`
|
|
78
|
+
});
|
|
79
|
+
return checks;
|
|
80
|
+
}
|
|
81
|
+
const installed = Boolean(service.service?.installed);
|
|
82
|
+
const loaded = Boolean(service.service?.loaded);
|
|
83
|
+
const healthy = service.health?.aerial === true;
|
|
84
|
+
if (healthy && !installed) {
|
|
85
|
+
checks.push({
|
|
86
|
+
id: "service.foreground_only",
|
|
87
|
+
ok: true,
|
|
88
|
+
severity: "warn",
|
|
89
|
+
message: "Aerial is running in the foreground but no background service is installed; it will not start on reboot.",
|
|
90
|
+
repair: REPAIRS.SERVICE_INSTALL
|
|
91
|
+
});
|
|
92
|
+
} else if (installed && !healthy) {
|
|
93
|
+
checks.push({
|
|
94
|
+
id: "service.health",
|
|
95
|
+
ok: false,
|
|
96
|
+
severity: "fail",
|
|
97
|
+
message: loaded
|
|
98
|
+
? "Service manager reports the task running, but Aerial /health is not responding."
|
|
99
|
+
: "Service is installed but not running.",
|
|
100
|
+
repair: REPAIRS.SERVICE_STATUS_JSON
|
|
101
|
+
});
|
|
102
|
+
} else if (!installed && !healthy) {
|
|
103
|
+
checks.push({
|
|
104
|
+
id: "service.health",
|
|
105
|
+
ok: false,
|
|
106
|
+
severity: "fail",
|
|
107
|
+
message: "Aerial is not running and no background service is installed.",
|
|
108
|
+
repair: REPAIRS.SERVICE_INSTALL
|
|
109
|
+
});
|
|
110
|
+
} else {
|
|
111
|
+
checks.push({ id: "service.health", ok: true, severity: "info", message: "Aerial /health is responding." });
|
|
112
|
+
}
|
|
113
|
+
if (service.health?.portConflict) {
|
|
114
|
+
checks.push({
|
|
115
|
+
id: "service.port_conflict",
|
|
116
|
+
ok: false,
|
|
117
|
+
severity: "fail",
|
|
118
|
+
message: `Port ${service.config?.port} is held by a non-Aerial process: ${service.health.conflictReason || "unknown"}.`,
|
|
119
|
+
repair: REPAIRS.SERVICE_STATUS_JSON
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
const wrapper = service.service?.wrapper;
|
|
123
|
+
if (wrapper && wrapper.stale === true) {
|
|
124
|
+
const reasons = Array.isArray(wrapper.staleReasons) ? wrapper.staleReasons.join(", ") : "unknown";
|
|
125
|
+
checks.push({
|
|
126
|
+
id: "service.wrapper_stale",
|
|
127
|
+
ok: true,
|
|
128
|
+
severity: "warn",
|
|
129
|
+
message: `Installed service wrapper is stale (${reasons}); reinstall to regenerate it.`,
|
|
130
|
+
repair: REPAIRS.SERVICE_INSTALL
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
return checks;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function summarize(ok, checks) {
|
|
137
|
+
const fails = checks.filter((c) => c.severity === "fail").length;
|
|
138
|
+
const warns = checks.filter((c) => c.severity === "warn").length;
|
|
139
|
+
if (ok && warns === 0) return "All checks passed.";
|
|
140
|
+
if (ok && warns > 0) return `Aerial is functional with ${warns} warning(s).`;
|
|
141
|
+
return `${fails} check(s) failed; ${warns} warning(s).`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function doctor({ run, healthFetch, setup, service } = {}) {
|
|
145
|
+
const setupOut = setup ?? setupStatus();
|
|
146
|
+
const serviceOut = service ?? await serviceStatus({ ...(run ? { run } : {}), ...(healthFetch ? { healthFetch } : {}) });
|
|
147
|
+
const app = computeAppStatus(setupOut, serviceOut);
|
|
7
148
|
const checks = [
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
149
|
+
...buildAuthChecks(setupOut),
|
|
150
|
+
...buildClientChecks(setupOut),
|
|
151
|
+
...buildServiceChecks(serviceOut)
|
|
152
|
+
];
|
|
153
|
+
const ok = app.ok && checks.every((c) => c.severity !== "fail");
|
|
154
|
+
return {
|
|
155
|
+
schema: "aerial.doctor.v1",
|
|
156
|
+
ok,
|
|
157
|
+
summary: summarize(ok, checks),
|
|
158
|
+
checks,
|
|
159
|
+
status: {
|
|
160
|
+
schema: app.schema,
|
|
161
|
+
ok: app.ok,
|
|
162
|
+
setup: app.setup,
|
|
163
|
+
service: app.service
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function renderRepairCommand(repair) {
|
|
169
|
+
if (!repair || typeof repair !== "object") return "";
|
|
170
|
+
const args = Array.isArray(repair.args) ? repair.args : [];
|
|
171
|
+
return [repair.command, ...args].join(" ");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function renderDoctorText(report) {
|
|
175
|
+
const lines = [];
|
|
176
|
+
lines.push(`aerial doctor: ${report.summary}`);
|
|
177
|
+
const groups = [
|
|
178
|
+
["fail", "Failures"],
|
|
179
|
+
["warn", "Warnings"],
|
|
180
|
+
["info", "Info"]
|
|
13
181
|
];
|
|
14
|
-
|
|
182
|
+
for (const [severity, label] of groups) {
|
|
183
|
+
const items = report.checks.filter((c) => c.severity === severity);
|
|
184
|
+
if (items.length === 0) continue;
|
|
185
|
+
lines.push("");
|
|
186
|
+
lines.push(`${label}:`);
|
|
187
|
+
for (const check of items) {
|
|
188
|
+
const tag = severity.toUpperCase();
|
|
189
|
+
let line = ` ${tag} ${check.id}: ${check.message}`;
|
|
190
|
+
if (check.repair) line += ` -> run: ${renderRepairCommand(check.repair)}`;
|
|
191
|
+
lines.push(line);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return lines.join("\n");
|
|
15
195
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
|
|
3
|
+
const TTL_MS = 30000;
|
|
4
|
+
const cache = new Map();
|
|
5
|
+
|
|
6
|
+
export function tokenFingerprintOf(token) {
|
|
7
|
+
if (typeof token !== "string" || token.length === 0) return "anonymous";
|
|
8
|
+
return crypto.createHash("sha256").update(token).digest("hex").slice(0, 16);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function fetchModelsCatalog({ fetchImpl, tokenFingerprint } = {}) {
|
|
12
|
+
if (typeof fetchImpl !== "function") return undefined;
|
|
13
|
+
const key = tokenFingerprint || "anonymous";
|
|
14
|
+
const cached = cache.get(key);
|
|
15
|
+
const now = Date.now();
|
|
16
|
+
if (cached && cached.expiresAt > now) return cached.models;
|
|
17
|
+
const models = await fetchImpl();
|
|
18
|
+
if (!Array.isArray(models)) return undefined;
|
|
19
|
+
cache.set(key, { models, expiresAt: now + TTL_MS });
|
|
20
|
+
return models;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function clearModelCatalogCacheForTests() {
|
|
24
|
+
cache.clear();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function canonicalClaudeFamily(modelId) {
|
|
28
|
+
if (typeof modelId !== "string") return undefined;
|
|
29
|
+
if (/^claude-opus-4[.-]7(?:-|$)/.test(modelId)) return "claude-opus-4.7";
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function modelHasRoute(model, route) {
|
|
34
|
+
const endpoints = Array.isArray(model?.supported_endpoints) ? model.supported_endpoints : [];
|
|
35
|
+
const routes = Array.isArray(model?.aerial?.routes) ? model.aerial.routes : [];
|
|
36
|
+
if (route === "/v1/messages") return endpoints.includes("/v1/messages") || routes.includes("messages");
|
|
37
|
+
return endpoints.includes(route);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function modelSupportsAdaptiveThinking(model) {
|
|
41
|
+
const supports = model?.capabilities?.supports;
|
|
42
|
+
return supports?.adaptive_thinking === true || supports?.thinking?.adaptive === true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function supportedReasoningEfforts(model) {
|
|
46
|
+
const supports = model?.capabilities?.supports;
|
|
47
|
+
const values = supports?.reasoning_effort ?? supports?.reasoning_efforts;
|
|
48
|
+
if (Array.isArray(values)) return values.map(String);
|
|
49
|
+
if (typeof values === "string") return [values];
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function findCompatibleModel({ models, family, route, adaptiveThinking, effort, preferredId } = {}) {
|
|
54
|
+
if (!Array.isArray(models) || !family) return undefined;
|
|
55
|
+
const candidates = models.filter((model) => {
|
|
56
|
+
const id = typeof model?.id === "string" ? model.id : "";
|
|
57
|
+
if (canonicalClaudeFamily(id) !== family) return false;
|
|
58
|
+
if (route && !modelHasRoute(model, route)) return false;
|
|
59
|
+
if (adaptiveThinking === true && !modelSupportsAdaptiveThinking(model)) return false;
|
|
60
|
+
if (effort && !supportedReasoningEfforts(model).includes(effort)) return false;
|
|
61
|
+
return true;
|
|
62
|
+
});
|
|
63
|
+
if (candidates.length === 0) return undefined;
|
|
64
|
+
if (preferredId) {
|
|
65
|
+
const preferred = candidates.find((model) => model.id === preferredId);
|
|
66
|
+
if (preferred) return preferred;
|
|
67
|
+
}
|
|
68
|
+
return candidates[0];
|
|
69
|
+
}
|
package/src/service.js
CHANGED
|
@@ -358,21 +358,147 @@ async function pollForAerialUp(host, port, healthFetch, deadlineMs = HEALTH_STAR
|
|
|
358
358
|
return { cls: lastCls || { mode: "absent" }, probe: lastProbe, attempts, elapsedMs: Date.now() - start };
|
|
359
359
|
}
|
|
360
360
|
|
|
361
|
-
function
|
|
362
|
-
if (!
|
|
361
|
+
function unescapeShSingleQuoted(line, prefix) {
|
|
362
|
+
if (!line.startsWith(prefix)) return undefined;
|
|
363
|
+
const rest = line.slice(prefix.length);
|
|
364
|
+
if (!rest.startsWith("'")) return undefined;
|
|
365
|
+
let i = 1;
|
|
366
|
+
let out = "";
|
|
367
|
+
while (i < rest.length) {
|
|
368
|
+
const ch = rest[i];
|
|
369
|
+
if (ch === "'") {
|
|
370
|
+
if (rest.slice(i, i + 4) === "'\\''") {
|
|
371
|
+
out += "'";
|
|
372
|
+
i += 4;
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
return out;
|
|
376
|
+
}
|
|
377
|
+
out += ch;
|
|
378
|
+
i += 1;
|
|
379
|
+
}
|
|
380
|
+
return undefined;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function unescapePsSingleQuoted(line, prefix) {
|
|
384
|
+
if (!line.startsWith(prefix)) return undefined;
|
|
385
|
+
const rest = line.slice(prefix.length);
|
|
386
|
+
if (!rest.startsWith("'")) return undefined;
|
|
387
|
+
let i = 1;
|
|
388
|
+
let out = "";
|
|
389
|
+
while (i < rest.length) {
|
|
390
|
+
const ch = rest[i];
|
|
391
|
+
if (ch === "'") {
|
|
392
|
+
if (rest[i + 1] === "'") {
|
|
393
|
+
out += "'";
|
|
394
|
+
i += 2;
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
return out;
|
|
398
|
+
}
|
|
399
|
+
out += ch;
|
|
400
|
+
i += 1;
|
|
401
|
+
}
|
|
402
|
+
return undefined;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function parseWrapperPaths(wrapperFile) {
|
|
406
|
+
if (!wrapperFile) return { node: undefined, cli: undefined };
|
|
363
407
|
try {
|
|
364
|
-
if (!fs.existsSync(wrapperFile)) return undefined;
|
|
408
|
+
if (!fs.existsSync(wrapperFile)) return { node: undefined, cli: undefined };
|
|
365
409
|
const data = fs.readFileSync(wrapperFile, "utf8");
|
|
410
|
+
const lines = data.split(/\r?\n/);
|
|
366
411
|
if (wrapperFile.endsWith(".sh")) {
|
|
367
|
-
|
|
368
|
-
|
|
412
|
+
let node;
|
|
413
|
+
let cli;
|
|
414
|
+
for (const line of lines) {
|
|
415
|
+
if (node === undefined) {
|
|
416
|
+
const candidate = unescapeShSingleQuoted(line, "NODE_BIN=");
|
|
417
|
+
if (candidate !== undefined) node = candidate;
|
|
418
|
+
}
|
|
419
|
+
if (cli === undefined) {
|
|
420
|
+
const candidate = unescapeShSingleQuoted(line, "CLI_ENTRY=");
|
|
421
|
+
if (candidate !== undefined) cli = candidate;
|
|
422
|
+
}
|
|
423
|
+
if (node !== undefined && cli !== undefined) break;
|
|
424
|
+
}
|
|
425
|
+
return { node, cli };
|
|
369
426
|
}
|
|
370
427
|
if (wrapperFile.endsWith(".ps1")) {
|
|
371
|
-
|
|
372
|
-
|
|
428
|
+
let node;
|
|
429
|
+
let cli;
|
|
430
|
+
for (const line of lines) {
|
|
431
|
+
if (node === undefined) {
|
|
432
|
+
const m = line.match(/^\$node\s*=\s*(.*)$/);
|
|
433
|
+
if (m) {
|
|
434
|
+
const candidate = unescapePsSingleQuoted(m[1].trim(), "");
|
|
435
|
+
if (candidate !== undefined) node = candidate;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
if (cli === undefined) {
|
|
439
|
+
const m = line.match(/^\$cli\s*=\s*(.*)$/);
|
|
440
|
+
if (m) {
|
|
441
|
+
const candidate = unescapePsSingleQuoted(m[1].trim(), "");
|
|
442
|
+
if (candidate !== undefined) cli = candidate;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (node !== undefined && cli !== undefined) break;
|
|
446
|
+
}
|
|
447
|
+
return { node, cli };
|
|
373
448
|
}
|
|
374
449
|
} catch {}
|
|
375
|
-
return undefined;
|
|
450
|
+
return { node: undefined, cli: undefined };
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function readWrapperNodePath(wrapperFile) {
|
|
454
|
+
return parseWrapperPaths(wrapperFile).node;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export const STALE_REASONS = Object.freeze({
|
|
458
|
+
WRAPPER_MISSING: "wrapper_missing",
|
|
459
|
+
WRAPPER_NODE_MISSING: "wrapper_node_missing",
|
|
460
|
+
WRAPPER_CLI_MISSING: "wrapper_cli_missing",
|
|
461
|
+
WRAPPER_LOG_CONFIG_UNPARSEABLE: "wrapper_log_config_unparseable"
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
function wrapperBlock(state) {
|
|
465
|
+
if (!state || state.installed !== true) {
|
|
466
|
+
return { stale: false, staleReasons: [] };
|
|
467
|
+
}
|
|
468
|
+
let wrapperPath;
|
|
469
|
+
if (process.platform === "darwin") wrapperPath = darwinWrapperPath();
|
|
470
|
+
else if (process.platform === "win32") wrapperPath = winWrapperPath();
|
|
471
|
+
const wrapperFileExists = wrapperPath ? fs.existsSync(wrapperPath) : false;
|
|
472
|
+
const staleReasons = [];
|
|
473
|
+
if (!wrapperFileExists) {
|
|
474
|
+
return {
|
|
475
|
+
path: wrapperPath,
|
|
476
|
+
nodePath: undefined,
|
|
477
|
+
nodeExists: undefined,
|
|
478
|
+
cliPath: undefined,
|
|
479
|
+
cliExists: undefined,
|
|
480
|
+
logConfigParseable: undefined,
|
|
481
|
+
stale: true,
|
|
482
|
+
staleReasons: [STALE_REASONS.WRAPPER_MISSING]
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
const { node: nodePath, cli: cliPath } = parseWrapperPaths(wrapperPath);
|
|
486
|
+
const nodeExists = nodePath ? fs.existsSync(nodePath) : false;
|
|
487
|
+
const cliExists = cliPath ? fs.existsSync(cliPath) : false;
|
|
488
|
+
const logConfigParseable = parseWrapperLogValues(wrapperPath) !== null;
|
|
489
|
+
if (!nodeExists) staleReasons.push(STALE_REASONS.WRAPPER_NODE_MISSING);
|
|
490
|
+
if (!cliExists) staleReasons.push(STALE_REASONS.WRAPPER_CLI_MISSING);
|
|
491
|
+
if (!logConfigParseable) staleReasons.push(STALE_REASONS.WRAPPER_LOG_CONFIG_UNPARSEABLE);
|
|
492
|
+
return {
|
|
493
|
+
path: wrapperPath,
|
|
494
|
+
nodePath,
|
|
495
|
+
nodeExists,
|
|
496
|
+
cliPath,
|
|
497
|
+
cliExists,
|
|
498
|
+
logConfigParseable,
|
|
499
|
+
stale: staleReasons.length > 0,
|
|
500
|
+
staleReasons
|
|
501
|
+
};
|
|
376
502
|
}
|
|
377
503
|
|
|
378
504
|
function healthFailedDiagnostics({ wrapper, probe, attempts, elapsedMs }) {
|
|
@@ -904,7 +1030,7 @@ export async function serviceStatus({ run = defaultRunCommand, healthFetch } = {
|
|
|
904
1030
|
platform: process.platform,
|
|
905
1031
|
supported: false,
|
|
906
1032
|
config: { host: config.host, port: config.port },
|
|
907
|
-
service: { installed: false, loaded: false, reason: "unsupported_platform", platform: process.platform },
|
|
1033
|
+
service: { installed: false, loaded: false, reason: "unsupported_platform", platform: process.platform, wrapper: wrapperBlock({ installed: false }) },
|
|
908
1034
|
health: { ok: false, error: "unsupported_platform" },
|
|
909
1035
|
logs: logsBlock(),
|
|
910
1036
|
auth: authBlock(),
|
|
@@ -914,6 +1040,7 @@ export async function serviceStatus({ run = defaultRunCommand, healthFetch } = {
|
|
|
914
1040
|
const state = serviceState(ctx);
|
|
915
1041
|
const probe = await (healthFetch || defaultHealthFetch)(config.host, config.port);
|
|
916
1042
|
const cls = classifyHealth(probe);
|
|
1043
|
+
const wrapper = wrapperBlock(state);
|
|
917
1044
|
let supervisor;
|
|
918
1045
|
if (cls.mode === "aerial_running") {
|
|
919
1046
|
supervisor = state.installed && state.loaded ? "service-managed" : "foreground";
|
|
@@ -939,7 +1066,7 @@ export async function serviceStatus({ run = defaultRunCommand, healthFetch } = {
|
|
|
939
1066
|
platform: process.platform,
|
|
940
1067
|
supported: true,
|
|
941
1068
|
config: { host: config.host, port: config.port },
|
|
942
|
-
service: { platform: process.platform, ...state },
|
|
1069
|
+
service: { platform: process.platform, ...state, wrapper },
|
|
943
1070
|
health,
|
|
944
1071
|
logs: logsBlock(),
|
|
945
1072
|
auth: authBlock(),
|
|
@@ -956,6 +1083,8 @@ export const _internal = {
|
|
|
956
1083
|
aerialLogPath,
|
|
957
1084
|
stdioLogPath,
|
|
958
1085
|
parseWrapperLogValues,
|
|
1086
|
+
parseWrapperPaths,
|
|
1087
|
+
wrapperBlock,
|
|
959
1088
|
buildSchtasksArgs,
|
|
960
1089
|
quoteSchtasksTR,
|
|
961
1090
|
classifyHealth
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
3
|
+
|
|
4
|
+
export const EFFORT_VALUES = Object.freeze(["low", "medium", "high", "xhigh"]);
|
|
5
|
+
export const DEFAULT_EFFORT = "medium";
|
|
6
|
+
|
|
7
|
+
export function normalizeEffort(value) {
|
|
8
|
+
if (value === undefined || value === null) return undefined;
|
|
9
|
+
const trimmed = String(value).trim().toLowerCase();
|
|
10
|
+
if (!trimmed) return undefined;
|
|
11
|
+
if (trimmed === "max") return "xhigh";
|
|
12
|
+
if (EFFORT_VALUES.includes(trimmed)) return trimmed;
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function assertValidEffort(raw) {
|
|
17
|
+
const normalized = normalizeEffort(raw);
|
|
18
|
+
if (!normalized) {
|
|
19
|
+
throw new Error(`Invalid --effort ${JSON.stringify(raw)}. Allowed: ${EFFORT_VALUES.join(", ")} (or alias 'max' for xhigh).`);
|
|
20
|
+
}
|
|
21
|
+
return normalized;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parseChoice(value, max, defaultIndex) {
|
|
25
|
+
const trimmed = String(value || "").trim();
|
|
26
|
+
if (!trimmed) return defaultIndex;
|
|
27
|
+
if (!/^\d+$/.test(trimmed)) return undefined;
|
|
28
|
+
const n = Number(trimmed);
|
|
29
|
+
return n >= 1 && n <= max ? n - 1 : undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function chooseSetupEffort({
|
|
33
|
+
target,
|
|
34
|
+
explicitEffort,
|
|
35
|
+
prompt,
|
|
36
|
+
input: inputStream = input,
|
|
37
|
+
output: outputStream = output
|
|
38
|
+
} = {}) {
|
|
39
|
+
if (explicitEffort !== undefined) {
|
|
40
|
+
return { effort: assertValidEffort(explicitEffort), source: "explicit", displayed: false };
|
|
41
|
+
}
|
|
42
|
+
const shouldPrompt = prompt === undefined ? Boolean(inputStream.isTTY) : prompt;
|
|
43
|
+
if (!shouldPrompt) {
|
|
44
|
+
return { effort: DEFAULT_EFFORT, source: "default_non_tty", displayed: false };
|
|
45
|
+
}
|
|
46
|
+
const defaultIndex = EFFORT_VALUES.indexOf(DEFAULT_EFFORT);
|
|
47
|
+
const rl = createInterface({ input: inputStream, output: outputStream });
|
|
48
|
+
try {
|
|
49
|
+
outputStream.write(`Choose ${target} reasoning effort:\n`);
|
|
50
|
+
for (const [index, value] of EFFORT_VALUES.entries()) {
|
|
51
|
+
const marker = index === defaultIndex ? " (default)" : "";
|
|
52
|
+
outputStream.write(` ${index + 1}. ${value}${marker}\n`);
|
|
53
|
+
}
|
|
54
|
+
while (true) {
|
|
55
|
+
const answer = await rl.question(`Choose effort [1-${EFFORT_VALUES.length}, default ${defaultIndex + 1} = ${DEFAULT_EFFORT}]: `);
|
|
56
|
+
const selectedIndex = parseChoice(answer, EFFORT_VALUES.length, defaultIndex);
|
|
57
|
+
if (selectedIndex !== undefined) {
|
|
58
|
+
return { effort: EFFORT_VALUES[selectedIndex], source: "prompt", displayed: true };
|
|
59
|
+
}
|
|
60
|
+
outputStream.write(`Enter a number from 1 to ${EFFORT_VALUES.length}, or press Enter for ${defaultIndex + 1}.\n`);
|
|
61
|
+
}
|
|
62
|
+
} finally {
|
|
63
|
+
rl.close();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function formatEffortSelection({ target, effort, source }) {
|
|
68
|
+
if (source === "explicit") return `Selected ${target} effort: ${effort}`;
|
|
69
|
+
if (source === "prompt") return `Selected ${target} effort: ${effort}`;
|
|
70
|
+
return `No interactive terminal detected; selected ${target} effort: ${effort}. Pass --effort <low|medium|high|xhigh|max> to choose a different effort.`;
|
|
71
|
+
}
|
package/src/setup.js
CHANGED
|
@@ -2,10 +2,11 @@ import fs from "node:fs";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { parse as parseToml } from "smol-toml";
|
|
5
|
-
import { ensureApiKey, loadConfig } from "./config.js";
|
|
5
|
+
import { ensureApiKey, loadConfig, saveConfig } from "./config.js";
|
|
6
6
|
import { apiKeyPath, githubTokenPath } from "./paths.js";
|
|
7
7
|
import { gitHubTokenSource } from "./auth.js";
|
|
8
8
|
import { logEvent } from "./log.js";
|
|
9
|
+
import { assertValidEffort, normalizeEffort } from "./setup-selection.js";
|
|
9
10
|
|
|
10
11
|
const BACKUP_PREFIX = ".aerial-backup-";
|
|
11
12
|
const PRE_RESTORE_PREFIX = ".aerial-pre-restore-";
|
|
@@ -75,7 +76,8 @@ function claudeEnvForAerial(currentEnv, config) {
|
|
|
75
76
|
};
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
export function setupCodex({ model, authCommand = DEFAULT_CODEX_AUTH } = {}) {
|
|
79
|
+
export function setupCodex({ model, effort, authCommand = DEFAULT_CODEX_AUTH } = {}) {
|
|
80
|
+
const normalizedEffort = effort === undefined ? undefined : assertValidEffort(effort);
|
|
79
81
|
ensureApiKey();
|
|
80
82
|
const config = loadConfig();
|
|
81
83
|
const selectedModel = model || config.defaultModel;
|
|
@@ -89,7 +91,7 @@ export function setupCodex({ model, authCommand = DEFAULT_CODEX_AUTH } = {}) {
|
|
|
89
91
|
content = setTomlString(content, "model_provider", "aerial");
|
|
90
92
|
content = setTomlString(content, "model", selectedModel);
|
|
91
93
|
content = upsertTomlSection(content, "model_providers.aerial", {
|
|
92
|
-
name: "Aerial
|
|
94
|
+
name: "Aerial",
|
|
93
95
|
base_url: `http://${config.host}:${config.port}/v1`,
|
|
94
96
|
wire_api: "responses"
|
|
95
97
|
});
|
|
@@ -99,13 +101,19 @@ export function setupCodex({ model, authCommand = DEFAULT_CODEX_AUTH } = {}) {
|
|
|
99
101
|
timeout_ms: authCommand.timeout_ms || DEFAULT_CODEX_AUTH.timeout_ms,
|
|
100
102
|
refresh_interval_ms: authCommand.refresh_interval_ms ?? DEFAULT_CODEX_AUTH.refresh_interval_ms
|
|
101
103
|
});
|
|
102
|
-
|
|
104
|
+
const profileValues = { model_provider: "aerial", model: selectedModel };
|
|
105
|
+
if (normalizedEffort) profileValues.model_reasoning_effort = normalizedEffort;
|
|
106
|
+
content = upsertTomlSection(content, "profiles.aerial", profileValues);
|
|
103
107
|
fs.writeFileSync(file, content, "utf8");
|
|
104
|
-
|
|
105
|
-
|
|
108
|
+
if (normalizedEffort && config.defaultEffort !== normalizedEffort) {
|
|
109
|
+
saveConfig({ ...config, defaultEffort: normalizedEffort });
|
|
110
|
+
}
|
|
111
|
+
logEvent("setup_write", { target: "codex", file, backup, auth: "command", effort: normalizedEffort });
|
|
112
|
+
return { file, backup, model: selectedModel, effort: normalizedEffort, auth: { type: "command", command: authCommand.command, args: authCommand.args || [] } };
|
|
106
113
|
}
|
|
107
114
|
|
|
108
|
-
export function setupClaude({ model, apiKeyHelper = DEFAULT_CLAUDE_API_KEY_HELPER } = {}) {
|
|
115
|
+
export function setupClaude({ model, effort, apiKeyHelper = DEFAULT_CLAUDE_API_KEY_HELPER } = {}) {
|
|
116
|
+
const normalizedEffort = effort === undefined ? undefined : assertValidEffort(effort);
|
|
109
117
|
ensureApiKey();
|
|
110
118
|
const config = loadConfig();
|
|
111
119
|
const selectedModel = model || config.defaultModel;
|
|
@@ -121,8 +129,11 @@ export function setupClaude({ model, apiKeyHelper = DEFAULT_CLAUDE_API_KEY_HELPE
|
|
|
121
129
|
};
|
|
122
130
|
if (selectedModel) next.model = selectedModel;
|
|
123
131
|
fs.writeFileSync(file, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
124
|
-
|
|
125
|
-
|
|
132
|
+
if (normalizedEffort && config.defaultEffort !== normalizedEffort) {
|
|
133
|
+
saveConfig({ ...config, defaultEffort: normalizedEffort });
|
|
134
|
+
}
|
|
135
|
+
logEvent("setup_write", { target: "claude", file, backup, model: selectedModel, effort: normalizedEffort });
|
|
136
|
+
return { file, backup, model: selectedModel, effort: normalizedEffort, apiKeyHelper };
|
|
126
137
|
}
|
|
127
138
|
|
|
128
139
|
function codexConfigFile() {
|
|
@@ -182,25 +193,27 @@ export function codexStatus() {
|
|
|
182
193
|
const expectedBaseUrl = `http://${config.host}:${config.port}/v1`;
|
|
183
194
|
const file = codexConfigFile();
|
|
184
195
|
const backups = backupPathsFor(file);
|
|
185
|
-
if (!fs.existsSync(file)) return { target: "codex", state: "missing", file, backups };
|
|
196
|
+
if (!fs.existsSync(file)) return { target: "codex", state: "missing", file, backups, effort: "missing" };
|
|
186
197
|
let content;
|
|
187
198
|
try {
|
|
188
199
|
content = fs.readFileSync(file, "utf8");
|
|
189
200
|
} catch (err) {
|
|
190
|
-
return { target: "codex", state: "invalid", file, backups, error: err.message };
|
|
201
|
+
return { target: "codex", state: "invalid", file, backups, error: err.message, effort: "missing" };
|
|
191
202
|
}
|
|
192
203
|
let doc;
|
|
193
204
|
try {
|
|
194
205
|
doc = parseToml(content);
|
|
195
206
|
} catch (err) {
|
|
196
|
-
return { target: "codex", state: "invalid", file, backups, error: err.message };
|
|
207
|
+
return { target: "codex", state: "invalid", file, backups, error: err.message, effort: "missing" };
|
|
197
208
|
}
|
|
198
209
|
const state = codexStateFromDoc(doc, expectedBaseUrl);
|
|
199
210
|
const model = typeof doc?.model === "string" ? doc.model : undefined;
|
|
200
211
|
const baseUrl = typeof doc?.model_providers?.aerial?.base_url === "string"
|
|
201
212
|
? doc.model_providers.aerial.base_url
|
|
202
213
|
: undefined;
|
|
203
|
-
|
|
214
|
+
const profileEffort = doc?.profiles?.aerial?.model_reasoning_effort;
|
|
215
|
+
const effort = typeof profileEffort === "string" ? (normalizeEffort(profileEffort) || "missing") : "missing";
|
|
216
|
+
return { target: "codex", state, file, backups, model, baseUrl, effort };
|
|
204
217
|
}
|
|
205
218
|
|
|
206
219
|
function claudeStateFromDoc(doc, expectedBaseUrl) {
|
|
@@ -220,17 +233,18 @@ export function claudeStatus() {
|
|
|
220
233
|
const expectedBaseUrl = `http://${config.host}:${config.port}`;
|
|
221
234
|
const file = claudeSettingsFile();
|
|
222
235
|
const backups = backupPathsFor(file);
|
|
223
|
-
|
|
236
|
+
const effort = typeof config.defaultEffort === "string" && config.defaultEffort.trim() ? config.defaultEffort.trim() : "missing";
|
|
237
|
+
if (!fs.existsSync(file)) return { target: "claude", state: "missing", file, backups, effort };
|
|
224
238
|
let doc;
|
|
225
239
|
try {
|
|
226
240
|
doc = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
227
241
|
} catch (err) {
|
|
228
|
-
return { target: "claude", state: "invalid", file, backups, error: err.message };
|
|
242
|
+
return { target: "claude", state: "invalid", file, backups, error: err.message, effort };
|
|
229
243
|
}
|
|
230
244
|
const state = claudeStateFromDoc(doc, expectedBaseUrl);
|
|
231
245
|
const model = typeof doc?.model === "string" ? doc.model : undefined;
|
|
232
246
|
const baseUrl = doc?.env?.ANTHROPIC_BASE_URL;
|
|
233
|
-
return { target: "claude", state, file, backups, model, baseUrl };
|
|
247
|
+
return { target: "claude", state, file, backups, model, baseUrl, effort };
|
|
234
248
|
}
|
|
235
249
|
|
|
236
250
|
export function setupStatus() {
|